eth-compress 0.0.0-security → 0.2.1

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/index.ts ADDED
@@ -0,0 +1,114 @@
1
+ const _sup_enc = new Map<string, string[] | -1>();
2
+ const _enc = ['deflate-raw', 'deflate', 'gzip'];
3
+ let supported: string | -1 | null = typeof CompressionStream === 'undefined' ? -1 : null;
4
+
5
+ export type PayloadTransform = (payload: unknown) => unknown;
6
+
7
+ /**
8
+ * Fetch-compatible function that applies HTTP compression (gzip/deflate) to requests.
9
+ * Optionally transforms request payloads before sending.
10
+ *
11
+ * @param input - The resource URL, Request object, or URL string
12
+ * @param init - Optional request initialization options
13
+ * @param transformPayload - Optional function to transform the request payload
14
+ * @returns A Promise that resolves to the Response
15
+ */
16
+ //! @__PURE__
17
+ export async function compressModule(
18
+ input: string | URL | Request,
19
+ init?: RequestInit,
20
+ ): Promise<Response>;
21
+
22
+ //! @__PURE__
23
+ export async function compressModule(
24
+ input: string | URL | Request,
25
+ init: RequestInit | undefined,
26
+ transformPayload?: PayloadTransform,
27
+ ): Promise<Response>;
28
+
29
+ //! @__PURE__
30
+ export async function compressModule(
31
+ input: string | URL | Request,
32
+ init?: RequestInit,
33
+ transformPayload?: PayloadTransform,
34
+ ): Promise<Response> {
35
+ const url =
36
+ typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
37
+
38
+ const cached = _sup_enc.get(url);
39
+ supported = supported === -1 ? -1 : cached === -1 ? -1 : (cached?.[0] ?? null);
40
+
41
+ // Only apply the optional payload transform
42
+ // when native HTTP compression is not available for this URL.
43
+ if (transformPayload && init?.body && typeof init.body === 'string') {
44
+ if (supported === -1 || supported === null) {
45
+ try {
46
+ const parsed = JSON.parse(init.body as string);
47
+ const next = transformPayload(parsed);
48
+ if (next !== undefined) {
49
+ init = {
50
+ ...init,
51
+ body: JSON.stringify(next),
52
+ };
53
+ }
54
+ } catch {
55
+ // Non-JSON bodies are left untouched.
56
+ }
57
+ }
58
+ }
59
+
60
+ if (supported && supported !== -1 && init?.body) {
61
+ const compressed = await new Response(
62
+ new Blob([init.body as string])
63
+ .stream()
64
+ .pipeThrough(new CompressionStream(supported as CompressionFormat)),
65
+ ).blob();
66
+ init = {
67
+ ...init,
68
+ body: compressed,
69
+ headers: { ...(init && init.headers), 'Content-Encoding': supported },
70
+ };
71
+ }
72
+ const response = await fetch(url, init);
73
+
74
+ if (supported === null) {
75
+ const encodings = response.headers
76
+ .get('Accept-Encoding')
77
+ ?.split(',')
78
+ .filter((e) => _enc.includes(e));
79
+ _sup_enc.set(url, encodings?.length ? encodings : -1);
80
+ }
81
+
82
+ return response;
83
+ }
84
+
85
+ /**
86
+ * Combines HTTP compression with EVM JIT compression.
87
+ * Just pass this as `fetchFn` to viem's http transport.
88
+ *
89
+ * @param input - The resource URL or Request object
90
+ * @param init - Optional request initialization options
91
+ * @returns A Promise that resolves to the Response
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * const client = createPublicClient({
96
+ * transport: http(rpcUrl, { fetchFn: compressModuleWithJIT })
97
+ * })
98
+ * ```
99
+ *
100
+ * If the target RPC endpoint and runtime support native HTTP compression,
101
+ * this helper prefers that path and will not apply JIT calldata compression;
102
+ * the JIT-based transform is used as a legacy/fallback path when HTTP
103
+ * content-encoding is unavailable.
104
+ * @pure
105
+ */
106
+ //! @__PURE__
107
+ export const compressModuleWithJIT = (
108
+ input: RequestInfo | URL,
109
+ init?: RequestInit,
110
+ ): Promise<Response> => {
111
+ return import('./jit-compressor').then(({ compress_call }) =>
112
+ compressModule(input, init, compress_call),
113
+ );
114
+ };
@@ -0,0 +1,443 @@
1
+ import { LibZip } from 'solady';
2
+
3
+ const MAX_160_BIT = (1n << 128n) - 1n;
4
+
5
+ const _normHex = (hex: string): string => hex.replace(/^0x/, '').toLowerCase();
6
+
7
+ const _hexToUint8Array = (hex: string): Uint8Array => {
8
+ const normalized = _normHex(hex);
9
+ const len = normalized.length;
10
+ const bytes = new Uint8Array(len / 2);
11
+ for (let i = 0; i < len; i += 2) {
12
+ bytes[i / 2] = Number.parseInt(normalized.slice(i, i + 2), 16);
13
+ }
14
+ return bytes;
15
+ };
16
+
17
+ const _uint8ArrayToHex = (bytes: Uint8Array): string => {
18
+ let hex = '';
19
+ for (let i = 0; i < bytes.length; i++) {
20
+ hex += bytes[i].toString(16).padStart(2, '0');
21
+ }
22
+ return hex;
23
+ };
24
+
25
+ /**
26
+ * Generates FastLZ (LZ77) decompressor bytecode. The generated code decompresses incoming calldata and forwards it to the target address.
27
+ * @param address - Target contract address
28
+ * @see {@link https://github.com/Vectorized/solady/blob/main/src/utils/LibZip.sol}
29
+ * @pure
30
+ */
31
+ //! @__PURE__
32
+ export const flzFwdBytecode = (address: string): string =>
33
+ `0x365f73${_normHex(address)}815b838110602f575f80848134865af1503d5f803e3d5ff35b803590815f1a8060051c908115609857600190600783149285831a6007018118840218600201948383011a90601f1660081b0101808603906020811860208211021890815f5b80830151818a015201858110609257505050600201019201916018565b82906075565b6001929350829150019101925f5b82811060b3575001916018565b85851060c1575b60010160a6565b936001818192355f1a878501530194905060ba56`;
34
+
35
+ /**
36
+ * Generates RLE (run-length encoded) decompressor bytecode. The generated code decompresses incoming calldata and forwards it to the target address.
37
+ * @param address - Target contract address
38
+ * @see {@link https://github.com/Vectorized/solady/blob/main/src/utils/LibZip.sol}
39
+ * @pure
40
+ */
41
+ //! @__PURE__
42
+ export const rleFwdBytecode = (address: string): string =>
43
+ `0x5f5f5b368110602d575f8083813473${_normHex(address)}5af1503d5f803e3d5ff35b600180820192909160031981019035185f1a8015604c57815301906002565b505f19815282820192607f9060031981019035185f1a818111156072575b160101906002565b838101368437606a56`;
44
+
45
+ /**
46
+ * JIT Compiles decompressor bytecode
47
+ * @param calldata - Calldata to compress
48
+ * @pure
49
+ */
50
+ //! @__PURE__
51
+ export const jitBytecode = function (calldata: string): string {
52
+ return _jitDecompressor('0x' + _normHex(calldata));
53
+ };
54
+
55
+ const _jitDecompressor = function (calldata: string): string {
56
+ const hex = _normHex(calldata);
57
+ const originalBuf = _hexToUint8Array(hex);
58
+
59
+ // Right‑align the 4‑byte selector in the first 32‑byte slot (offset 28),
60
+ // so that everything after the selector is reconstructed on mostly
61
+ // word‑aligned boundaries. This keeps the ABI words (and therefore most
62
+ // calldata reconstruction) 32‑byte aligned in memory.
63
+ // That way we avoid encoding offsets for writes (most of the time),
64
+ const padding = 28;
65
+ const buf = new Uint8Array(padding + originalBuf.length);
66
+ buf.set(originalBuf, padding);
67
+
68
+ const n = buf.length;
69
+
70
+ let ops: number[] = [];
71
+ let data: (number[] | null)[] = [];
72
+ let stack: bigint[] = [];
73
+ let trackedMemSize = 0;
74
+ let mem = new Map<number, bigint>();
75
+ const getStackIdx = (val: bigint): number => {
76
+ const idx = stack.lastIndexOf(val);
77
+ return idx === -1 ? -1 : stack.length - 1 - idx;
78
+ };
79
+
80
+ const opFreq = new Map<number, number>();
81
+ const dataFreq = new Map<number[] | null, number>();
82
+ const stackFreq = new Map<bigint, number>();
83
+ const wordCache = new Map<string, number>();
84
+ const wordCacheCost = new Map<string, number>();
85
+ const roundUp32 = (x: number) => (x + 31) & ~31;
86
+
87
+ let pushCounter = 0;
88
+ const stackCnt = new Map<bigint, number>();
89
+
90
+ const pop2 = (): [bigint, bigint] => [stack.pop()!, stack.pop()!];
91
+ const MASK32 = (1n << 256n) - 1n;
92
+
93
+ const ctr = <K>(m: Map<K, number>, k: K, delta: number) => m.set(k, (m.get(k) || 0) + delta);
94
+ const inc = <K>(m: Map<K, number>, k: K) => ctr(m, k, 1);
95
+ const dec = <K>(m: Map<K, number>, k: K) => ctr(m, k, -1);
96
+ const pushOp = (op: number) => {
97
+ ops.push(op);
98
+ inc(opFreq, op);
99
+ };
100
+ const pushD = (d: number[] | null) => {
101
+ data.push(d || null);
102
+ inc(dataFreq, d || null);
103
+ };
104
+ const pushS = (v: bigint, freq: number = 1) => {
105
+ stack.push(v);
106
+ ctr(stackFreq, v, freq);
107
+ ++pushCounter;
108
+ stackCnt.set(v, pushCounter);
109
+ };
110
+
111
+ const trackMem = (offset: number, size: number) => {
112
+ trackedMemSize = roundUp32(offset + size);
113
+ };
114
+
115
+ const addOp = (op: number, imm?: number[]) => {
116
+ if (op === 0x36) {
117
+ pushS(32n, 0);
118
+ } else if (op === 0x59) {
119
+ pushS(BigInt(trackedMemSize), 0);
120
+ } else if (op === 0x1b) {
121
+ let [shift, val] = pop2();
122
+ if (ops[ops.length - 1] == 144) {
123
+ ops.pop();
124
+ data.pop();
125
+ [shift, val] = [val, shift];
126
+ }
127
+ pushS((val << BigInt(shift)) & MASK32);
128
+ } else if (op === 0x17) {
129
+ // OR
130
+ const [a, b] = pop2();
131
+ if (ops[ops.length - 1] == 144) {
132
+ ops.pop();
133
+ data.pop();
134
+ }
135
+ pushS((a | b) & MASK32);
136
+ } else if ((op >= 0x60 && op <= 0x7f) || op === 0x5f) {
137
+ // PUSH
138
+ let v = 0n;
139
+ for (const b of imm || []) v = (v << 8n) | BigInt(b);
140
+ if (v == 224n) {
141
+ // Special‑case the literal 0xe0 (224):
142
+ // the decompressor is always deployed at 0x...00e0, so the final
143
+ // byte of ADDRESS is exactly 0xe0. Since we must send our own
144
+ // address with the eth_call anyway, we can synthesize this value
145
+ // with a single opcode instead of encoding a literal, effectively
146
+ // giving us one more hot constant slot on the stack.
147
+ pushS(v);
148
+ pushOp(0x30); // ADDRESS
149
+ pushD(null);
150
+ return;
151
+ }
152
+ const idx = getStackIdx(v);
153
+ if (idx !== -1 && op != 0x5f) {
154
+ const last = stackFreq.get(v) == 0;
155
+ if (idx == 0 && last) {
156
+ dec(stackFreq, v);
157
+ return;
158
+ }
159
+ if (idx == 1 && last) {
160
+ pushOp(144);
161
+ const [a, b] = pop2();
162
+ stack.push(b);
163
+ stack.push(a);
164
+ pushD(null);
165
+ dec(stackFreq, v);
166
+ return;
167
+ }
168
+ pushS(v, -1);
169
+ pushOp(128 + idx);
170
+ pushD(null);
171
+ return;
172
+ }
173
+ pushS(v);
174
+ } else if (op === 0x51) {
175
+ // MLOAD
176
+ const k = Number(stack.pop()!);
177
+ pushS(mem.has(k) ? mem.get(k)! : 0n);
178
+ } else if (op === 0x52) {
179
+ // MSTORE
180
+ const [offset, value] = pop2();
181
+ const k = Number(offset);
182
+ mem.set(k, value & MASK32);
183
+ trackMem(k, 32);
184
+ } else if (op === 0x53) {
185
+ // MSTORE8
186
+ const [offset, _] = pop2();
187
+ trackMem(Number(offset), 1);
188
+ } else if (op === 0xf3) {
189
+ // RETURN
190
+ pop2();
191
+ }
192
+ pushOp(op);
193
+ pushD(imm || null);
194
+ };
195
+ const isInStack = (w) => stack.includes(w) || w == 0xe0 || w == 32n;
196
+ const op = (opcode: number) => addOp(opcode);
197
+ const pushN = (value: number | bigint) => {
198
+ if (value > 0 && value === trackedMemSize) return addOp(0x59);
199
+ if (value == 32n) return addOp(0x36);
200
+ if (!value) return addOp(0x5f, undefined); // PUSH0
201
+ let v = BigInt(value);
202
+ let bytes: number[] = [];
203
+ while (v) {
204
+ bytes.unshift(Number(v & 0xffn));
205
+ v >>= 8n;
206
+ }
207
+ return addOp(0x5f + bytes.length, bytes);
208
+ };
209
+ const pushB = (buf: Uint8Array) => addOp(0x5f + buf.length, Array.from(buf));
210
+ const cntWords = (hex: string, wordHex: string) =>
211
+ (hex.match(new RegExp(wordHex, 'g')) || []).length;
212
+
213
+ // Rough cost model
214
+ const estShlCost = (seg: Array<{ s: number; e: number }>) => {
215
+ let cost = 0;
216
+ let first = true;
217
+ for (const { s, e } of seg) {
218
+ cost += 1 + e - s + 1; // PUSH segLen bytes
219
+ if (31 - e > 0) cost += 1 /* PUSH1 */ + 1 /* shift byte */ + 1 /* SHL */;
220
+ if (!first) cost += 1; // OR
221
+ first = false;
222
+ }
223
+ return cost;
224
+ };
225
+
226
+ type PlanStep =
227
+ | { t: 'num'; v: number | bigint }
228
+ | { t: 'bytes'; b: Uint8Array }
229
+ | { t: 'op'; o: number };
230
+
231
+ const plan: PlanStep[] = [];
232
+ const emitPushN = (v: number | bigint) => (plan.push({ t: 'num', v }), pushN(v));
233
+ const emitPushB = (b: Uint8Array) => (plan.push({ t: 'bytes', b }), pushB(b));
234
+ const emitOp = (o: number) => (plan.push({ t: 'op', o }), op(o));
235
+ pushN(1n);
236
+ // First pass: decide how to build each 32-byte word without emitting bytecode
237
+ for (let base = 0; base < n; base += 32) {
238
+ const word = new Uint8Array(32);
239
+ word.set(buf.slice(base, Math.min(base + 32, n)), 0);
240
+
241
+ const seg: Array<{ s: number; e: number }> = [];
242
+ for (let i = 0; i < 32; ) {
243
+ while (i < 32 && word[i] === 0) ++i;
244
+ if (i >= 32) break;
245
+ const s = i;
246
+ while (i < 32 && word[i] !== 0) ++i;
247
+ seg.push({ s, e: i - 1 });
248
+ }
249
+
250
+ if (!seg.length) continue;
251
+
252
+ // Decide whether to build this word via SHL/OR or as a single literal word
253
+ const literal = word.slice(seg[0].s);
254
+ const literalCost = 1 + literal.length;
255
+ let literalVal = 0n;
256
+ for (const b of literal) literalVal = (literalVal << 8n) | BigInt(b);
257
+ const baseBytes = Math.ceil(Math.log2(base + 1) / 8);
258
+ const wordHex = _uint8ArrayToHex(word);
259
+ if (literalCost > 8) {
260
+ if (wordCache.has(wordHex)) {
261
+ if (literalCost > wordCacheCost.get(wordHex)! + baseBytes) {
262
+ emitPushN(wordCache.get(wordHex)!);
263
+ emitOp(0x51);
264
+ emitPushN(base);
265
+ emitOp(0x52); // MSTORE
266
+ continue;
267
+ }
268
+ } else if (wordCacheCost.get(wordHex) != -1) {
269
+ const reuseCost = baseBytes + 3;
270
+ const freq = cntWords(hex, wordHex);
271
+ wordCacheCost.set(wordHex, freq * 32 > freq * reuseCost ? reuseCost : -1);
272
+ wordCache.set(wordHex, base);
273
+ }
274
+ }
275
+
276
+ // Convert literal bytes to bigint for stack comparison
277
+
278
+ const byte8s = seg.every(({ s, e }) => s === e);
279
+ if (isInStack(literal)) {
280
+ emitPushB(literal);
281
+ } else if (byte8s) {
282
+ for (const { s } of seg) {
283
+ emitPushN(word[s]);
284
+ emitPushN(base + s);
285
+ emitOp(0x53); // MSTORE8
286
+ }
287
+ continue;
288
+ } else if (literalCost <= estShlCost(seg)) {
289
+ emitPushB(literal);
290
+ } else {
291
+ let first = true;
292
+ for (const { s, e } of seg) {
293
+ const suffix0s = 31 - e;
294
+ emitPushB(word.slice(s, e + 1));
295
+ if (suffix0s > 0) {
296
+ emitPushN(suffix0s * 8);
297
+ emitOp(0x1b); // SHL
298
+ }
299
+ if (!first) emitOp(0x17); // OR
300
+ first = false;
301
+ }
302
+ }
303
+ emitPushN(base);
304
+ emitOp(0x52); // MSTORE
305
+ }
306
+
307
+ ops = [];
308
+ data = [];
309
+ stack = [];
310
+ trackedMemSize = 0;
311
+ mem = new Map();
312
+ // Pre 2nd pass. Push most frequent literals into stack.
313
+ Array.from(stackFreq.entries())
314
+ .filter(([val, freq]) => freq > 1 && val > 1n && val !== 32n && val !== 224n)
315
+ .sort((a, b) => stackCnt.get(b[0])! - stackCnt.get(a[0])!)
316
+ .filter(([val, _]) => {
317
+ return typeof val === 'number' ? BigInt(val) : val <= MAX_160_BIT;
318
+ })
319
+ .slice(0, 13)
320
+ .forEach(([val, _]) => {
321
+ pushN(val);
322
+ });
323
+ pushN(1n);
324
+ // Second pass: emit ops and track mem/stack
325
+ for (const step of plan) {
326
+ if (step.t === 'num') pushN(step.v);
327
+ else if (step.t === 'bytes') pushB(step.b);
328
+ else if (step.t === 'op') op(step.o);
329
+ }
330
+
331
+ // CALL stack layout (top to bottom): gas, address, value, argsOffset, argsSize, retOffset, retSize
332
+ //
333
+ // Opcodes breakdown:
334
+ // - 0x5f5f: PUSH0 PUSH0 (retSize=0, retOffset=0)
335
+ // - pushN(originalBuf.length): argsSize = actual data length
336
+ // - pushN(padding): argsOffset (skip leading alignment bytes)
337
+ // - 0x34: CALLVALUE (value)
338
+ // - 0x5f35: PUSH0 CALLDATALOAD (address from calldata[0])
339
+ // - 0x5a: GAS (remaining gas)
340
+ // - 0xf1: CALL
341
+ //
342
+ // RETURNDATACOPY(destOffset=0, offset=0, length=RETURNDATASIZE):
343
+ // - 0x3d5f5f3e: RETURNDATASIZE PUSH0 PUSH0 RETURNDATACOPY
344
+ //
345
+ // RETURN(offset=0, size=RETURNDATASIZE):
346
+ // - 0x3d5ff3: RETURNDATASIZE PUSH0 RETURN
347
+
348
+ op(0x5f); // PUSH0 (retSize)
349
+ op(0x5f); // PUSH0 (retOffset)
350
+ pushN(originalBuf.length); // argsSize = actual data length
351
+ pushN(padding); // argsOffset = padding
352
+
353
+ const out: number[] = [];
354
+ for (let i = 0; i < ops.length; ++i) {
355
+ out.push(ops[i]);
356
+ if (ops[i] >= 0x60 && ops[i] <= 0x7f && data[i]) out.push(...data[i]!);
357
+ }
358
+
359
+ // - CALLVALUE, load target address from calldata[0], GAS, CALL
360
+ // - RETURNDATACOPY(0, 0, RETURNDATASIZE)
361
+ // - RETURN(0, RETURNDATASIZE)
362
+ return '0x' + _uint8ArrayToHex(new Uint8Array(out)) + '345f355af13d5f5f3e3d5ff3';
363
+ };
364
+
365
+ const MIN_SIZE_FOR_COMPRESSION = 1150;
366
+ const DECOMPRESSOR_ADDRESS = '0x00000000000000000000000000000000000000e0';
367
+
368
+ const _jit = 'jit';
369
+ const _flz = 'flz';
370
+ const _cd = 'cd';
371
+
372
+ /**
373
+ * Compresses eth_call payload using JIT, FastLZ (FLZ), or calldata RLE (CD) compression.
374
+ * Auto-selects best algorithm if not specified. Only compresses if >800 bytes and beneficial.
375
+ * @param payload - eth_call RPC payload
376
+ * @param alg - 'jit' | 'flz' | 'cd' | undefined (auto)
377
+ * @returns (un)compressed eth_call payload
378
+ * @pure
379
+ */
380
+ //! @__PURE__
381
+ export const compress_call = function (payload: any, alg?: string): any {
382
+ const rpcMethod = payload.params?.[0]?.method || payload.method;
383
+ if (rpcMethod && rpcMethod !== 'eth_call') return payload;
384
+
385
+ // Extract data and target address from params[0] if available, otherwise top-level
386
+ const txObj = payload.params?.[0] || payload;
387
+ const blockParam = payload.params?.[1] || 'latest';
388
+ const existingStateOverride = payload.params?.[2] || {};
389
+
390
+ // If there are any existing state overrides for the decompressor address, do not compress
391
+ const hex = _normHex(txObj.data || '0x');
392
+ const originalSize = (txObj.data || '0x').length;
393
+ if (originalSize < MIN_SIZE_FOR_COMPRESSION || (existingStateOverride[DECOMPRESSOR_ADDRESS])) return payload;
394
+
395
+ const targetAddress = txObj.to || '';
396
+ const data = '0x' + hex;
397
+
398
+ const autoSelect = !alg && originalSize < 1150;
399
+ const flz = alg === _flz || autoSelect ? LibZip.flzCompress(data) : null;
400
+ const cd = alg === _cd || autoSelect ? LibZip.cdCompress(data) : null;
401
+ let selectedMethod = alg;
402
+ if (!selectedMethod) {
403
+ selectedMethod =
404
+ originalSize >= 1150 && (originalSize < 3000 || originalSize >= 8000)
405
+ ? _jit
406
+ : originalSize >= 3000 && originalSize < 8000
407
+ ? flz!.length < cd!.length
408
+ ? _flz
409
+ : _cd
410
+ : _cd;
411
+ }
412
+
413
+ let bytecode: string;
414
+ let calldata: string;
415
+
416
+ if (selectedMethod === _jit) {
417
+ bytecode = _jitDecompressor(data);
418
+ calldata = '0x' + _normHex(targetAddress).padStart(64, '0');
419
+ } else {
420
+ const isFlz = selectedMethod === _flz;
421
+ calldata = isFlz ? flz! : cd!;
422
+ bytecode = isFlz ? flzFwdBytecode(targetAddress) : rleFwdBytecode(targetAddress);
423
+ }
424
+
425
+ const compressedSize = bytecode.length + calldata.length;
426
+ if (compressedSize >= originalSize) return payload;
427
+
428
+ return {
429
+ ...payload,
430
+ params: [
431
+ {
432
+ ...txObj,
433
+ to: DECOMPRESSOR_ADDRESS,
434
+ data: calldata,
435
+ },
436
+ blockParam,
437
+ {
438
+ ...existingStateOverride,
439
+ [DECOMPRESSOR_ADDRESS]: { code: bytecode },
440
+ },
441
+ ],
442
+ };
443
+ };
package/package.json CHANGED
@@ -1,13 +1,135 @@
1
1
  {
2
- "name": "eth-compress",
3
- "version": "0.0.0-security",
4
- "description": "SECURITY HOLDING",
5
- "repository": { "type": "git", "url": "git+https://github.com/tadpole-labs/eth-compress.git" },
6
- "homepage": "https://github.com/tadpole-labs/eth-compress",
7
- "bugs": "https://github.com/tadpole-labs/eth-compress/issues",
8
- "license": "Apache-2.0 OR MIT",
9
- "engines": { "node": ">=22" },
10
- "files": [],
11
- "main": "index.js"
12
- }
13
-
2
+ "name": "eth-compress",
3
+ "version": "0.2.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Client-to-server compression (viem-compatible) module for compressed, gas-efficient, low-latency eth_call requests.",
7
+ "keywords": [
8
+ "eth_call compress",
9
+ "compression",
10
+ "json-rpc compress",
11
+ "calldata compress",
12
+ "latency",
13
+ "viem"
14
+ ],
15
+ "bugs": "https://github.com/tadpole-labs/eth-compress/issues",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/tadpole-labs/eth-compress.git"
19
+ },
20
+ "homepage": "https://github.com/tadpole-labs/eth-compress",
21
+ "license": "Apache-2.0 OR MIT",
22
+ "main": "./_esm/index.node.js",
23
+ "types": "./_types/index.d.ts",
24
+ "typings": "./_types/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./_types/index.d.ts",
28
+ "node": {
29
+ "types": "./_types/index.d.ts",
30
+ "import": "./_esm/index.node.js",
31
+ "require": "./_cjs/index.node.cjs",
32
+ "default": "./_esm/index.node.js"
33
+ },
34
+ "browser": {
35
+ "types": "./_types/index.d.ts",
36
+ "import": "./_esm/index.js",
37
+ "require": "./_cjs/index.cjs",
38
+ "default": "./_esm/index.js"
39
+ },
40
+ "development": "./index.node.ts",
41
+ "import": "./_esm/index.node.js",
42
+ "require": "./_cjs/index.node.cjs",
43
+ "default": "./_esm/index.node.js"
44
+ },
45
+ "./compressor": {
46
+ "types": "./_types/jit-compressor.d.ts",
47
+ "development": "./jit-compressor.ts",
48
+ "import": "./_esm/jit-compressor.js",
49
+ "require": "./_cjs/jit-compressor.cjs",
50
+ "default": "./_esm/jit-compressor.js"
51
+ },
52
+ "./types": {
53
+ "types": "./_types/index.d.ts",
54
+ "default": "./_types/index.d.ts"
55
+ },
56
+ "./package.json": "./package.json"
57
+ },
58
+ "typesVersions": {
59
+ "*": {
60
+ "*": [
61
+ "./_types/*"
62
+ ],
63
+ "compressor": [
64
+ "./_types/jit-compressor.d.ts"
65
+ ],
66
+ "types": [
67
+ "./_types/index.d.ts"
68
+ ]
69
+ }
70
+ },
71
+ "files": [
72
+ "*.ts",
73
+ "*.d.ts",
74
+ "_esm/**/*.js",
75
+ "_esm/**/*.js.map",
76
+ "_cjs/**/*.cjs",
77
+ "_cjs/**/*.cjs.map",
78
+ "_types/**/*.d.ts",
79
+ "_types/**/*.d.ts.map",
80
+ "README.md",
81
+ "LICENSE"
82
+ ],
83
+ "scripts": {
84
+ "build": "pnpm run clean && bun scripts/build.ts",
85
+ "clean": "rm -rf dist *.tgz",
86
+ "test:jit": "vitest run test/jit-compress.test.ts --config test/vitest.config.ts",
87
+ "test:demo": "vitest run test/demo.test.ts --config test/vitest.config.ts",
88
+ "test:viem": "vitest run test/viem-multicall.test.ts --config test/vitest.config.ts",
89
+ "test": "pnpm run build && pnpm run test:jit && pnpm run test:demo && pnpm run test:viem",
90
+ "lint": "biome lint .",
91
+ "lint:fix": "biome lint --write .",
92
+ "format": "biome format --write .",
93
+ "format:check": "biome format .",
94
+ "check": "biome check .",
95
+ "check:fix": "biome check --write .",
96
+ "ci:install": "pnpm install --frozen-lockfile --ignore-scripts"
97
+ },
98
+ "engines": {
99
+ "node": ">=22",
100
+ "pnpm": ">=10"
101
+ },
102
+ "packageManager": "pnpm@10.22.0",
103
+ "pnpm": {
104
+ "peerDependencyRules": {
105
+ "ignoreMissing": [
106
+ "@types/react"
107
+ ]
108
+ }
109
+ },
110
+ "dependencies": {
111
+ "solady": "0.1.26"
112
+ },
113
+ "devDependencies": {
114
+ "@biomejs/biome": "2.3.6",
115
+ "@ethereumjs/common": "10.1.0",
116
+ "@ethereumjs/util": "10.1.0",
117
+ "@ethereumjs/vm": "10.1.0",
118
+ "@types/bun": "1.3.2",
119
+ "@types/node": "24.10.1",
120
+ "esbuild": "0.27.0",
121
+ "typescript": "5.9.3",
122
+ "viem": "2.39.3",
123
+ "vitest": "4.0.10"
124
+ },
125
+ "browserslist": [
126
+ ">0.3%",
127
+ "chrome >= 80",
128
+ "edge >= 80",
129
+ "firefox >= 113",
130
+ "safari >= 16.4",
131
+ "ios_saf >= 16.4",
132
+ "not dead"
133
+ ],
134
+ "sideEffects": false
135
+ }