@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.
- package/lib/codec.d.ts +57 -12
- package/lib/codec.d.ts.map +1 -1
- package/lib/codec.js.map +1 -1
- package/lib/codecs/array.d.ts +1 -3
- package/lib/codecs/array.d.ts.map +1 -1
- package/lib/codecs/array.js +13 -9
- package/lib/codecs/array.js.map +1 -1
- package/lib/codecs/primitives.d.ts +2 -17
- package/lib/codecs/primitives.d.ts.map +1 -1
- package/lib/codecs/primitives.js +46 -152
- package/lib/codecs/primitives.js.map +1 -1
- package/lib/codecs/struct.d.ts +16 -9
- package/lib/codecs/struct.d.ts.map +1 -1
- package/lib/codecs/struct.js +53 -40
- 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 +65 -19
- package/src/codecs/array.test.ts +87 -0
- package/src/codecs/array.ts +6 -11
- package/src/codecs/primitives.test.ts +27 -0
- package/src/codecs/primitives.ts +87 -208
- package/src/codecs/struct.test.ts +69 -0
- package/src/codecs/struct.ts +80 -61
- 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,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
|
+
}
|
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
|
+
}
|