@subsquid/evm-codec 0.2.1 → 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.
- package/lib/codec.d.ts +59 -12
- package/lib/codec.d.ts.map +1 -1
- package/lib/codec.js.map +1 -1
- package/lib/codecs/array.d.ts +3 -3
- package/lib/codecs/array.d.ts.map +1 -1
- package/lib/codecs/array.js +13 -7
- package/lib/codecs/array.js.map +1 -1
- package/lib/codecs/primitives.d.ts +2 -16
- package/lib/codecs/primitives.d.ts.map +1 -1
- package/lib/codecs/primitives.js +52 -141
- package/lib/codecs/primitives.js.map +1 -1
- package/lib/codecs/struct.d.ts +17 -9
- package/lib/codecs/struct.d.ts.map +1 -1
- package/lib/codecs/struct.js +53 -39
- package/lib/codecs/struct.js.map +1 -1
- package/lib/dsl.d.ts +11 -0
- package/lib/dsl.d.ts.map +1 -0
- package/lib/dsl.js +33 -0
- package/lib/dsl.js.map +1 -0
- package/lib/index.d.ts +7 -4
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +7 -6
- package/lib/index.js.map +1 -1
- package/lib/sink/bounds.d.ts +21 -0
- package/lib/sink/bounds.d.ts.map +1 -0
- package/lib/sink/bounds.js +24 -0
- package/lib/sink/bounds.js.map +1 -0
- package/lib/sink/bytes.d.ts +41 -0
- package/lib/sink/bytes.d.ts.map +1 -0
- package/lib/sink/bytes.js +261 -0
- package/lib/sink/bytes.js.map +1 -0
- package/lib/sink/hex.d.ts +33 -0
- package/lib/sink/hex.d.ts.map +1 -0
- package/lib/sink/hex.js +289 -0
- package/lib/sink/hex.js.map +1 -0
- package/lib/{src.d.ts → src/bytes.d.ts} +7 -5
- package/lib/src/bytes.d.ts.map +1 -0
- package/lib/src/bytes.js +161 -0
- package/lib/src/bytes.js.map +1 -0
- package/lib/src/hex.d.ts +33 -0
- package/lib/src/hex.d.ts.map +1 -0
- package/lib/src/hex.js +164 -0
- package/lib/src/hex.js.map +1 -0
- package/lib/util.d.ts +6 -0
- package/lib/util.d.ts.map +1 -0
- package/lib/util.js +20 -0
- package/lib/util.js.map +1 -0
- package/package.json +6 -8
- package/src/codec.ts +67 -18
- package/src/codecs/array.test.ts +87 -0
- package/src/codecs/array.ts +9 -12
- package/src/codecs/primitives.test.ts +27 -0
- package/src/codecs/primitives.ts +87 -191
- package/src/codecs/struct.test.ts +69 -0
- package/src/codecs/struct.ts +80 -60
- package/src/dsl.ts +16 -0
- package/src/index.ts +7 -4
- package/src/sink/bounds.ts +26 -0
- package/src/sink/bytes.test.ts +92 -0
- package/src/sink/bytes.ts +290 -0
- package/src/sink/hex.ts +311 -0
- package/src/src/bytes.test.ts +114 -0
- package/src/src/bytes.ts +187 -0
- package/src/src/hex.ts +191 -0
- package/src/util.ts +19 -0
- package/lib/safeToNumber.d.ts +0 -2
- package/lib/safeToNumber.d.ts.map +0 -1
- package/lib/safeToNumber.js +0 -11
- package/lib/safeToNumber.js.map +0 -1
- package/lib/sink.d.ts +0 -43
- package/lib/sink.d.ts.map +0 -1
- package/lib/sink.js +0 -215
- package/lib/sink.js.map +0 -1
- package/lib/src.d.ts.map +0 -1
- package/lib/src.js +0 -141
- package/lib/src.js.map +0 -1
- package/src/safeToNumber.ts +0 -6
- package/src/sink.ts +0 -241
- package/src/src.ts +0 -158
|
@@ -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
|
+
}
|
package/src/sink/hex.ts
ADDED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest'
|
|
2
|
+
import {BytesSink, BytesSrc} from '..'
|
|
3
|
+
|
|
4
|
+
describe('src', () => {
|
|
5
|
+
it('negative numbers', () => {
|
|
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('positive signed numbers', () => {
|
|
24
|
+
const sink = new BytesSink(6)
|
|
25
|
+
sink.i8(1)
|
|
26
|
+
sink.i16(123)
|
|
27
|
+
sink.i32(123456)
|
|
28
|
+
sink.i64(1234567890n)
|
|
29
|
+
sink.i128(12345678901234567890n)
|
|
30
|
+
sink.i256(1234567890123456789012345678901234567890n)
|
|
31
|
+
|
|
32
|
+
const src = new BytesSrc(sink.result())
|
|
33
|
+
expect(src.i8()).toBe(1)
|
|
34
|
+
expect(src.i16()).toBe(123)
|
|
35
|
+
expect(src.i32()).toBe(123456)
|
|
36
|
+
expect(src.i64()).toBe(1234567890n)
|
|
37
|
+
expect(src.i128()).toBe(12345678901234567890n)
|
|
38
|
+
expect(src.i256()).toBe(1234567890123456789012345678901234567890n)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('mixed static types', () => {
|
|
42
|
+
const sink = new BytesSink(4)
|
|
43
|
+
sink.u8(1)
|
|
44
|
+
sink.i8(-2)
|
|
45
|
+
sink.address('0x1234567890123456789012345678901234567890')
|
|
46
|
+
sink.u256(3n)
|
|
47
|
+
|
|
48
|
+
const src = new BytesSrc(sink.result())
|
|
49
|
+
expect(src.u8()).toBe(1)
|
|
50
|
+
expect(src.i8()).toBe(-2)
|
|
51
|
+
expect(src.address()).toBe('0x1234567890123456789012345678901234567890')
|
|
52
|
+
expect(src.u256()).toBe(3n)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('mixed dynamic types', () => {
|
|
56
|
+
const str1 = 'abc'.repeat(100)
|
|
57
|
+
const bytes1 = Buffer.alloc(100).fill('321')
|
|
58
|
+
const bytes7 = Buffer.from('1234567890abcd', 'hex')
|
|
59
|
+
const str2 = 'hello'
|
|
60
|
+
const addressValue = '0xabc4567890123456789012345678901234567890'
|
|
61
|
+
|
|
62
|
+
// Build a 7-field ABI-encoded tuple: each dynamic field (string/bytes) gets its
|
|
63
|
+
// own head slot (openTail writes the pointer) + tail data, while static fields
|
|
64
|
+
// (u8, staticBytes, i128, address) are written directly into the head.
|
|
65
|
+
const sink = new BytesSink(7)
|
|
66
|
+
sink.u8(69)
|
|
67
|
+
sink.openTail()
|
|
68
|
+
sink.string(str1)
|
|
69
|
+
sink.closeTail()
|
|
70
|
+
sink.staticBytes(7, bytes7)
|
|
71
|
+
sink.i128(-21312312452243312424534213123123123123n)
|
|
72
|
+
sink.openTail()
|
|
73
|
+
sink.bytes(bytes1)
|
|
74
|
+
sink.closeTail()
|
|
75
|
+
sink.address(addressValue)
|
|
76
|
+
sink.openTail()
|
|
77
|
+
sink.string(str2)
|
|
78
|
+
sink.closeTail()
|
|
79
|
+
|
|
80
|
+
const src = new BytesSrc(sink.result())
|
|
81
|
+
expect(src.u8()).toBe(69)
|
|
82
|
+
expect(src.string()).toBe(str1)
|
|
83
|
+
expect(src.staticBytes(7)).toStrictEqual(new Uint8Array(bytes7))
|
|
84
|
+
expect(src.i128()).toBe(-21312312452243312424534213123123123123n)
|
|
85
|
+
expect(src.bytes()).toStrictEqual(new Uint8Array(bytes1))
|
|
86
|
+
expect(src.address()).toBe(addressValue)
|
|
87
|
+
expect(src.string()).toBe(str2)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('string', () => {
|
|
91
|
+
function testString(str: string) {
|
|
92
|
+
const sink = new BytesSink(1)
|
|
93
|
+
sink.openTail()
|
|
94
|
+
sink.string(str)
|
|
95
|
+
sink.closeTail()
|
|
96
|
+
expect(new BytesSrc(sink.result()).string()).toBe(str)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
it('short string', () => testString('hello'))
|
|
100
|
+
it('32 byte string', () => testString('this string length is 32 bytes!!'))
|
|
101
|
+
it('longer string', () => testString('this string length is 33 bytes!!!'))
|
|
102
|
+
it('UTF', () => testString('привет 👍'))
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('bytes', () => {
|
|
106
|
+
const buffer = Buffer.alloc(150)
|
|
107
|
+
buffer.fill('xd')
|
|
108
|
+
const sink = new BytesSink(1)
|
|
109
|
+
sink.openTail()
|
|
110
|
+
sink.bytes(buffer)
|
|
111
|
+
sink.closeTail()
|
|
112
|
+
expect(new BytesSrc(sink.result()).bytes()).toStrictEqual(new Uint8Array(buffer))
|
|
113
|
+
})
|
|
114
|
+
})
|