eth-compress 0.2.0 → 0.2.2

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/jit-compressor.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { LibZip } from 'solady';
2
+ import { MIN_BODY_SIZE } from './index';
2
3
 
3
- const MAX_160_BIT = (1n << 160n) - 1n;
4
+ const MAX_128_BIT = (1n << 128n) - 1n;
5
+ const MAX_256_BIT = (1n << 256n) - 1n;
4
6
 
5
7
  const _normHex = (hex: string): string => hex.replace(/^0x/, '').toLowerCase();
6
8
 
@@ -22,6 +24,22 @@ const _uint8ArrayToHex = (bytes: Uint8Array): string => {
22
24
  return hex;
23
25
  };
24
26
 
27
+ const not = (a: bigint): bigint => ~a & MAX_256_BIT;
28
+ const and = (a: bigint, b: bigint): bigint => a & b & MAX_256_BIT;
29
+ const or = (a: bigint, b: bigint): bigint => (a | b) & MAX_256_BIT;
30
+ const xor = (a: bigint, b: bigint): bigint => (a ^ b) & MAX_256_BIT;
31
+ const shl = (shift: bigint, value: bigint): bigint => (value << shift) & MAX_256_BIT;
32
+ const shr = (shift: bigint, value: bigint): bigint => (value >> shift) & MAX_256_BIT;
33
+ const sub = (a: bigint, b: bigint): bigint => (a - b) & MAX_256_BIT;
34
+ const sigext = (byteSize: bigint, value: bigint): bigint => {
35
+ const numBytes = Number(byteSize) + 1;
36
+ const mask = (1n << BigInt(numBytes * 8)) - 1n;
37
+ const signBit = 1n << BigInt(numBytes * 8 - 1);
38
+ const maskedVal = value & mask;
39
+ const extended = maskedVal & signBit ? maskedVal | (~mask & MAX_256_BIT) : maskedVal;
40
+ return extended & MAX_256_BIT;
41
+ };
42
+
25
43
  /**
26
44
  * Generates FastLZ (LZ77) decompressor bytecode. The generated code decompresses incoming calldata and forwards it to the target address.
27
45
  * @param address - Target contract address
@@ -29,7 +47,7 @@ const _uint8ArrayToHex = (bytes: Uint8Array): string => {
29
47
  * @pure
30
48
  */
31
49
  //! @__PURE__
32
- export const flzFwdBytecode = (address: string): string =>
50
+ const flzFwdBytecode = (address: string): string =>
33
51
  `0x365f73${_normHex(address)}815b838110602f575f80848134865af1503d5f803e3d5ff35b803590815f1a8060051c908115609857600190600783149285831a6007018118840218600201948383011a90601f1660081b0101808603906020811860208211021890815f5b80830151818a015201858110609257505050600201019201916018565b82906075565b6001929350829150019101925f5b82811060b3575001916018565b85851060c1575b60010160a6565b936001818192355f1a878501530194905060ba56`;
34
52
 
35
53
  /**
@@ -39,19 +57,9 @@ export const flzFwdBytecode = (address: string): string =>
39
57
  * @pure
40
58
  */
41
59
  //! @__PURE__
42
- export const rleFwdBytecode = (address: string): string =>
60
+ const rleFwdBytecode = (address: string): string =>
43
61
  `0x5f5f5b368110602d575f8083813473${_normHex(address)}5af1503d5f803e3d5ff35b600180820192909160031981019035185f1a8015604c57815301906002565b505f19815282820192607f9060031981019035185f1a818111156072575b160101906002565b838101368437606a56`;
44
62
 
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
63
  const _jitDecompressor = function (calldata: string): string {
56
64
  const hex = _normHex(calldata);
57
65
  const originalBuf = _hexToUint8Array(hex);
@@ -70,12 +78,13 @@ const _jitDecompressor = function (calldata: string): string {
70
78
  let ops: number[] = [];
71
79
  let data: (number[] | null)[] = [];
72
80
  let stack: bigint[] = [];
73
- let stackFreq2 = new Map<bigint, number>();
74
81
  let trackedMemSize = 0;
75
82
  let mem = new Map<number, bigint>();
83
+ let firstPass = true;
76
84
  const getStackIdx = (val: bigint): number => {
77
- const idx = stack.lastIndexOf(val);
78
- return idx === -1 ? -1 : stack.length - 1 - idx;
85
+ let idx = stack.lastIndexOf(val);
86
+ idx = idx === -1 ? -1 : stack.length - 1 - idx;
87
+ return idx > 15 ? -1 : idx;
79
88
  };
80
89
 
81
90
  const opFreq = new Map<number, number>();
@@ -87,23 +96,19 @@ const _jitDecompressor = function (calldata: string): string {
87
96
 
88
97
  let pushCounter = 0;
89
98
  const stackCnt = new Map<bigint, number>();
90
-
91
99
  const pop2 = (): [bigint, bigint] => [stack.pop()!, stack.pop()!];
92
- const MASK32 = (1n << 256n) - 1n;
93
-
94
- const bump = <K>(m: Map<K, number>, k: K) => m.set(k, (m.get(k) || 0) + 1);
95
- const pushOp = (op: number) => {
100
+ const ctr = <K>(m: Map<K, number>, k: K, delta: number) => m.set(k, (m.get(k) || 0) + delta);
101
+ const inc = <K>(m: Map<K, number>, k: K) => ctr(m, k, 1);
102
+ const pushOp = (op: number, d?: number[] | null) => {
96
103
  ops.push(op);
97
- bump(opFreq, op);
98
- };
99
- const pushD = (d: number[] | null) => {
100
- data.push(d || null);
101
- bump(dataFreq, d || null);
104
+ inc(opFreq, op);
105
+ const imm = d ?? null;
106
+ data.push(imm);
107
+ inc(dataFreq, imm);
102
108
  };
103
- const pushS = (v: bigint) => {
109
+ const pushS = (v: bigint, freq: number = 1) => {
104
110
  stack.push(v);
105
- bump(stackFreq, v);
106
- bump(stackFreq2, v);
111
+ ctr(stackFreq, v, freq);
107
112
  ++pushCounter;
108
113
  stackCnt.set(v, pushCounter);
109
114
  };
@@ -113,40 +118,75 @@ const _jitDecompressor = function (calldata: string): string {
113
118
  };
114
119
 
115
120
  const addOp = (op: number, imm?: number[]) => {
116
- if (op === 0x59) {
117
- pushS(BigInt(trackedMemSize));
121
+ if (op === 0x36) {
122
+ pushS(32n, 0);
123
+ } else if (op === 0x59) {
124
+ pushS(BigInt(trackedMemSize), 0);
125
+ } else if (op === 0x0b) {
126
+ // SIGNEXTEND
127
+ const [byteSize, val] = pop2();
128
+ pushS(sigext(byteSize, val), 1);
129
+ } else if (op === 0x19) {
130
+ // NOT
131
+ const val = stack.pop()!;
132
+ pushS(not(val), 0);
133
+ } else if (op === 0x18) {
134
+ // XOR
135
+ const [a, b] = pop2();
136
+ pushS(xor(a, b), 1);
137
+ } else if (op === 0x16) {
138
+ // AND
139
+ const [a, b] = pop2();
140
+ pushS(and(a, b), 1);
141
+ } else if (op === 0x03) {
142
+ // SUB
143
+ const [a, b] = pop2();
144
+ pushS(sub(a, b), 1);
118
145
  } else if (op === 0x1b) {
119
146
  // SHL
120
- const [shift, val] = pop2();
121
- pushS((val << shift) & MASK32);
147
+ let [shift, val] = pop2();
148
+ pushS(shl(shift, val), 1);
149
+ } else if (op === 0x1c) {
150
+ // SHR
151
+ let [shift, val] = pop2();
152
+ pushS(shr(shift, val), 1);
122
153
  } else if (op === 0x17) {
123
154
  // OR
124
- const [a, b] = pop2();
125
- pushS((a | b) & MASK32);
155
+ let [a, b] = pop2();
156
+ pushS(or(a, b), 1);
126
157
  } else if ((op >= 0x60 && op <= 0x7f) || op === 0x5f) {
127
158
  // PUSH
128
159
  let v = 0n;
129
160
  for (const b of imm || []) v = (v << 8n) | BigInt(b);
161
+ if (v == 224n) {
162
+ pushS(v, 0);
163
+ pushOp(0x30); // ADDRESS
164
+ return;
165
+ }
166
+ if (v == 32n) {
167
+ pushS(v, 0);
168
+ pushOp(0x36); // ADDRESS
169
+ return;
170
+ }
171
+ if (v === BigInt(trackedMemSize)) {
172
+ pushS(v, 0);
173
+ pushOp(0x59); // MemSize
174
+ return;
175
+ }
130
176
  const idx = getStackIdx(v);
131
- pushS(v);
132
177
  if (idx !== -1 && op != 0x5f) {
133
- if (stackFreq2.get(v)! * 2 < stackFreq.get(v)!) {
134
- pushOp(128 + idx);
135
- pushD(null);
136
- }
178
+ let pushCtr = firstPass ? 1 : -1;
179
+ pushS(v, pushCtr);
180
+ pushOp(128 + idx);
137
181
  return;
138
182
  }
139
- if (v == 224n) {
140
- // Special‑case the literal 0xe0 (224):
141
- // the decompressor is always deployed at 0x...00e0, so the final
142
- // byte of ADDRESS is exactly 0xe0. Since we must send our own
143
- // address with the eth_call anyway, we can synthesize this value
144
- // with a single opcode instead of encoding a literal, effectively
145
- // giving us one more hot constant slot on the stack.
146
- pushOp(0x30); // ADDRESS
147
- pushD(null);
183
+ if (v == MAX_256_BIT) {
184
+ pushS(v);
185
+ pushOp(0x5f); // 0
186
+ pushOp(0x19); // NOT
148
187
  return;
149
188
  }
189
+ pushS(v);
150
190
  } else if (op === 0x51) {
151
191
  // MLOAD
152
192
  const k = Number(stack.pop()!);
@@ -155,7 +195,7 @@ const _jitDecompressor = function (calldata: string): string {
155
195
  // MSTORE
156
196
  const [offset, value] = pop2();
157
197
  const k = Number(offset);
158
- mem.set(k, value & MASK32);
198
+ mem.set(k, value & MAX_256_BIT);
159
199
  trackMem(k, 32);
160
200
  } else if (op === 0x53) {
161
201
  // MSTORE8
@@ -165,13 +205,12 @@ const _jitDecompressor = function (calldata: string): string {
165
205
  // RETURN
166
206
  pop2();
167
207
  }
168
- pushOp(op);
169
- pushD(imm || null);
208
+ pushOp(op, imm || null);
170
209
  };
171
-
172
210
  const op = (opcode: number) => addOp(opcode);
173
211
  const pushN = (value: number | bigint) => {
174
212
  if (value > 0 && value === trackedMemSize) return addOp(0x59);
213
+ if (value == 32n) return addOp(0x36);
175
214
  if (!value) return addOp(0x5f, undefined); // PUSH0
176
215
  let v = BigInt(value);
177
216
  let bytes: number[] = [];
@@ -207,8 +246,9 @@ const _jitDecompressor = function (calldata: string): string {
207
246
  const emitPushN = (v: number | bigint) => (plan.push({ t: 'num', v }), pushN(v));
208
247
  const emitPushB = (b: Uint8Array) => (plan.push({ t: 'bytes', b }), pushB(b));
209
248
  const emitOp = (o: number) => (plan.push({ t: 'op', o }), op(o));
210
-
249
+ pushN(1n);
211
250
  // First pass: decide how to build each 32-byte word without emitting bytecode
251
+ const _stack = [1n, 32n, 224n];
212
252
  for (let base = 0; base < n; base += 32) {
213
253
  const word = new Uint8Array(32);
214
254
  word.set(buf.slice(base, Math.min(base + 32, n)), 0);
@@ -224,22 +264,22 @@ const _jitDecompressor = function (calldata: string): string {
224
264
 
225
265
  if (!seg.length) continue;
226
266
 
227
- const byte8s = seg.every(({ s, e }) => s === e);
228
- if (byte8s) {
229
- for (const { s } of seg) {
230
- emitPushN(word[s]);
231
- emitPushN(base + s);
232
- emitOp(0x53); // MSTORE8
233
- }
234
- continue;
235
- }
236
-
237
267
  // Decide whether to build this word via SHL/OR or as a single literal word
238
268
  const literal = word.slice(seg[0].s);
239
269
  const literalCost = 1 + literal.length;
240
-
270
+ let literalVal = 0n;
271
+ for (const b of literal) literalVal = (literalVal << 8n) | BigInt(b);
241
272
  const baseBytes = Math.ceil(Math.log2(base + 1) / 8);
242
273
  const wordHex = _uint8ArrayToHex(word);
274
+ const shlCost = estShlCost(seg);
275
+
276
+ const inStack = _stack.includes(literalVal);
277
+ if (inStack) {
278
+ emitPushB(literal);
279
+ emitPushN(base);
280
+ emitOp(0x52); // MSTORE
281
+ continue;
282
+ }
243
283
  if (literalCost > 8) {
244
284
  if (wordCache.has(wordHex)) {
245
285
  if (literalCost > wordCacheCost.get(wordHex)! + baseBytes) {
@@ -257,19 +297,134 @@ const _jitDecompressor = function (calldata: string): string {
257
297
  }
258
298
  }
259
299
 
260
- if (literalCost <= estShlCost(seg)) {
300
+ const byte8s = seg.every(({ s, e }) => s === e);
301
+ const byte8sCost = seg.length * 3; // each: PUSH1 (value), PUSH1 (offset), MSTORE8
302
+ if (inStack) {
261
303
  emitPushB(literal);
262
304
  } else {
263
- let first = true;
264
- for (const { s, e } of seg) {
265
- const suffix0s = 31 - e;
266
- emitPushB(word.slice(s, e + 1));
267
- if (suffix0s > 0) {
268
- emitPushN(suffix0s * 8);
269
- emitOp(0x1b); // SHL
305
+ // Aggregate all costs
306
+ let bestCost = literalCost;
307
+ let bestEmit: (() => void) | null = () => {
308
+ emitPushB(literal);
309
+ };
310
+ if (literalVal == MAX_256_BIT) {
311
+ bestCost = 2;
312
+ bestEmit = () => {
313
+ emitPushN(notVal);
314
+ emitOp(0x19);
315
+ };
316
+ }
317
+ // Try NOT: PUSH<n> ~val, NOT
318
+ const notVal = not(literalVal);
319
+ let notBytes = 0;
320
+ let tmp = notVal;
321
+ while (tmp > 0n) {
322
+ ++notBytes;
323
+ tmp >>= 8n;
324
+ }
325
+ notBytes = 1 + notBytes;
326
+ if (notBytes === 0) notBytes;
327
+ const notCost = notBytes + 1; // PUSH<n> + NOT
328
+ if (notCost < bestCost) {
329
+ bestCost = notCost;
330
+ bestEmit = () => {
331
+ emitPushN(notVal);
332
+ emitOp(0x19);
333
+ };
334
+ }
335
+
336
+ // Try SUB: PUSH1 0, PUSH<n> val, SUB
337
+ const subVal = sub(0n, literalVal);
338
+ let subBytes = 0;
339
+ tmp = subVal;
340
+ while (tmp > 0n) {
341
+ ++subBytes;
342
+ tmp >>= 8n;
343
+ }
344
+ if (subBytes === 0) subBytes = 1;
345
+ if (_stack.includes(subVal)) subBytes = 1;
346
+ const subCost = 1 + (1 + subBytes) + 1; // PUSH0 + PUSH<n> + SUB
347
+ if (subCost < bestCost) {
348
+ bestCost = subCost;
349
+ bestEmit = () => {
350
+ emitPushN(0);
351
+ emitPushN(subVal);
352
+ emitOp(0x03);
353
+ };
354
+ }
355
+
356
+ // Try SIGNEXTEND: PUSH<n> truncated, PUSH1 byteSize, SIGNEXTEND
357
+ for (let numBytes = 1; numBytes < literal.length; numBytes++) {
358
+ const mask = (1n << BigInt(numBytes * 8)) - 1n;
359
+ const truncated = literalVal & mask;
360
+ const extended = sigext(BigInt(numBytes - 1), truncated);
361
+ if (extended === literalVal && (truncated & (1n << BigInt(numBytes * 8 - 1))) !== 0n) {
362
+ let trueByteCost = 1 + numBytes;
363
+ if (_stack.includes(BigInt(extended))) trueByteCost = 1;
364
+ let signCost = trueByteCost + (1 + 1) + 1; // PUSH<n> + PUSH1 + SIGNEXTEND
365
+ if (signCost < bestCost) {
366
+ bestCost = signCost;
367
+ bestEmit = () => {
368
+ emitPushN(truncated);
369
+ emitPushN(numBytes - 1);
370
+ emitOp(0x0b);
371
+ };
372
+ }
373
+ break;
374
+ }
375
+ }
376
+
377
+ // Try SHIFT+NOT: PUSH<n> val, PUSH1 shift, SHL, NOT
378
+ for (let shiftBits = 8; shiftBits <= 248; shiftBits += 8) {
379
+ const shifted = shr(BigInt(shiftBits), literalVal);
380
+ if (shifted === 0n) break;
381
+
382
+ const notShifted = not(shifted);
383
+ const reconstructed = shl(BigInt(shiftBits), notShifted);
384
+
385
+ if (reconstructed === literalVal) {
386
+ let shiftedBytes = 0;
387
+ let tmpShifted = notShifted;
388
+ while (tmpShifted > 0n) {
389
+ ++shiftedBytes;
390
+ tmpShifted >>= 8n;
391
+ }
392
+ if (shiftedBytes === 0) shiftedBytes = 1;
393
+ const shiftNotCost = 1 + shiftedBytes + 2 + 1 + 1; // PUSH<n> + PUSH1 + SHL + NOT
394
+ if (shiftNotCost < bestCost) {
395
+ bestCost = shiftNotCost;
396
+ bestEmit = () => {
397
+ emitPushN(notShifted);
398
+ emitPushN(shiftBits);
399
+ emitOp(0x1b); // SHL
400
+ emitOp(0x19); // NOT
401
+ };
402
+ }
270
403
  }
271
- if (!first) emitOp(0x17); // OR
272
- first = false;
404
+ }
405
+
406
+ if (byte8s && byte8sCost < bestCost && byte8sCost <= shlCost) {
407
+ for (const { s } of seg) {
408
+ emitPushN(word[s]);
409
+ emitPushN(base + s);
410
+ emitOp(0x53); // MSTORE8
411
+ }
412
+ continue; // Skip the single MSTORE at the end
413
+ } else if (shlCost < bestCost) {
414
+ // Use SHL/OR
415
+ let first = true;
416
+ for (const { s, e } of seg) {
417
+ const suffix0s = 31 - e;
418
+ emitPushB(word.slice(s, e + 1));
419
+ if (suffix0s > 0) {
420
+ emitPushN(suffix0s * 8);
421
+ emitOp(0x1b); // SHL
422
+ }
423
+ if (!first) emitOp(0x17); // OR
424
+ first = false;
425
+ }
426
+ } else {
427
+ bestEmit!();
273
428
  }
274
429
  }
275
430
  emitPushN(base);
@@ -281,20 +436,18 @@ const _jitDecompressor = function (calldata: string): string {
281
436
  stack = [];
282
437
  trackedMemSize = 0;
283
438
  mem = new Map();
284
-
285
439
  // Pre 2nd pass. Push most frequent literals into stack.
286
440
  Array.from(stackFreq.entries())
287
- .filter(([val, freq]) => freq > 1 && val !== 0n && val !== 224n)
441
+ .filter(([val, freq]) => freq > 1 && val > 1n && val !== 32n && val !== 224n)
442
+ .sort((a, b) => stackCnt.get(b[0])! - stackCnt.get(a[0])!)
288
443
  .filter(([val, _]) => {
289
- return typeof val === 'number' ? val : Number(val) <= MAX_160_BIT;
444
+ return typeof val === 'number' ? BigInt(val) : val <= MAX_128_BIT;
290
445
  })
291
- .sort((a, b) => stackCnt.get(b[0])! - stackCnt.get(a[0])!)
292
- .slice(0, 14)
446
+ .slice(0, 15)
293
447
  .forEach(([val, _]) => {
294
448
  pushN(val);
295
449
  });
296
-
297
- stackFreq2 = new Map();
450
+ pushN(1n);
298
451
  // Second pass: emit ops and track mem/stack
299
452
  for (const step of plan) {
300
453
  if (step.t === 'num') pushN(step.v);
@@ -312,7 +465,6 @@ const _jitDecompressor = function (calldata: string): string {
312
465
  // - 0x5f35: PUSH0 CALLDATALOAD (address from calldata[0])
313
466
  // - 0x5a: GAS (remaining gas)
314
467
  // - 0xf1: CALL
315
- // - 0x50: POP (discard success value)
316
468
  //
317
469
  // RETURNDATACOPY(destOffset=0, offset=0, length=RETURNDATASIZE):
318
470
  // - 0x3d5f5f3e: RETURNDATASIZE PUSH0 PUSH0 RETURNDATACOPY
@@ -334,19 +486,21 @@ const _jitDecompressor = function (calldata: string): string {
334
486
  // - CALLVALUE, load target address from calldata[0], GAS, CALL
335
487
  // - RETURNDATACOPY(0, 0, RETURNDATASIZE)
336
488
  // - RETURN(0, RETURNDATASIZE)
337
- return '0x' + _uint8ArrayToHex(new Uint8Array(out)) + '345f355af1503d5f5f3e3d5ff3';
489
+ return '0x' + _uint8ArrayToHex(new Uint8Array(out)) + '345f355af13d5f5f3e3d5ff3';
338
490
  };
339
491
 
340
- const MIN_SIZE_FOR_COMPRESSION = 800;
341
492
  const DECOMPRESSOR_ADDRESS = '0x00000000000000000000000000000000000000e0';
342
493
 
343
- const _jit = 'jit';
344
- const _flz = 'flz';
345
- const _cd = 'cd';
346
-
347
494
  /**
348
495
  * Compresses eth_call payload using JIT, FastLZ (FLZ), or calldata RLE (CD) compression.
349
496
  * Auto-selects best algorithm if not specified. Only compresses if >800 bytes and beneficial.
497
+ *
498
+ * Only applies compression to calls that:
499
+ * - target the latest block ID
500
+ * - have no state overrides
501
+ * - have a target address and calldata
502
+ * - have no other properties (nonce, gas, etc.)
503
+ *
350
504
  * @param payload - eth_call RPC payload
351
505
  * @param alg - 'jit' | 'flz' | 'cd' | undefined (auto)
352
506
  * @returns (un)compressed eth_call payload
@@ -354,45 +508,63 @@ const _cd = 'cd';
354
508
  */
355
509
  //! @__PURE__
356
510
  export const compress_call = function (payload: any, alg?: string): any {
357
- const rpcMethod = payload.params?.[0]?.method || payload.method;
358
- if (rpcMethod && rpcMethod !== 'eth_call') return payload;
359
-
360
- const hex = _normHex(payload.data || '0x');
361
- const originalSize = (payload.data || '0x').length;
362
- if (originalSize < MIN_SIZE_FOR_COMPRESSION) return payload;
363
-
364
- const targetAddress = payload.to || '';
365
- const data = '0x' + hex;
511
+ const { method, params } = payload;
512
+ if (method && method !== 'eth_call') return payload;
513
+ const txObj = params?.[0] || payload;
514
+ const blockParam = params?.[1];
515
+ const overrides = params?.[2];
516
+
517
+ // Validation
518
+ if (
519
+ (blockParam && blockParam !== 'latest') ||
520
+ (overrides && Object.keys(overrides).length > 0) ||
521
+ !txObj?.to ||
522
+ !txObj?.data ||
523
+ Object.keys(txObj).some((k) => !['to', 'data', 'from'].includes(k))
524
+ ) {
525
+ return payload;
526
+ }
366
527
 
367
- const autoSelect = !alg && originalSize < 1150;
368
- const flz = alg === _flz || autoSelect ? LibZip.flzCompress(data) : null;
369
- const cd = alg === _cd || autoSelect ? LibZip.cdCompress(data) : null;
528
+ const originalSize = txObj.data.length;
529
+ if (originalSize < MIN_BODY_SIZE) return payload;
370
530
 
371
- const selectedMethod =
372
- alg || (originalSize >= 1150 ? _jit : flz!.length < cd!.length ? _flz : _cd);
531
+ const inputData = '0x' + _normHex(txObj.data);
532
+ const to = txObj.to;
373
533
 
534
+ // Determine compression method and generate bytecode/calldata
374
535
  let bytecode: string;
375
536
  let calldata: string;
376
537
 
377
- if (selectedMethod === _jit) {
378
- bytecode = _jitDecompressor(data);
379
- calldata = '0x' + _normHex(targetAddress).padStart(64, '0');
538
+ if (alg === 'jit' || (!alg && (originalSize < 3000 || originalSize >= 8000))) {
539
+ bytecode = _jitDecompressor(inputData);
540
+ calldata = '0x' + _normHex(to).padStart(64, '0');
380
541
  } else {
381
- const isFlz = selectedMethod === _flz;
382
- calldata = isFlz ? flz! : cd!;
383
- bytecode = isFlz ? flzFwdBytecode(targetAddress) : rleFwdBytecode(targetAddress);
542
+ // Need FLZ and/or CD compression
543
+ const flzData = alg === 'flz' || !alg ? LibZip.flzCompress(inputData) : null;
544
+ const cdData = alg === 'cd' || (!alg && flzData) ? LibZip.cdCompress(inputData) : null;
545
+
546
+ // Pick the best or requested one
547
+ const useFlz =
548
+ alg === 'flz' || (!alg && flzData && (!cdData || flzData.length < cdData.length));
549
+
550
+ if (useFlz) {
551
+ calldata = flzData!;
552
+ bytecode = flzFwdBytecode(to);
553
+ } else {
554
+ calldata = cdData!;
555
+ bytecode = rleFwdBytecode(to);
556
+ }
384
557
  }
385
558
 
386
- const compressedSize = bytecode.length + calldata.length;
387
- if (compressedSize >= originalSize) return payload;
559
+ // Skip if not beneficial
560
+ if (bytecode.length + calldata.length >= originalSize) return payload;
388
561
 
389
562
  return {
390
563
  ...payload,
391
- to: DECOMPRESSOR_ADDRESS,
392
- data: calldata,
393
- stateDiff: {
394
- ...(payload.stateDiff || {}),
395
- [DECOMPRESSOR_ADDRESS]: { code: bytecode },
396
- },
564
+ params: [
565
+ { ...txObj, to: DECOMPRESSOR_ADDRESS, data: calldata },
566
+ blockParam || 'latest',
567
+ { ...overrides, [DECOMPRESSOR_ADDRESS]: { code: bytecode } },
568
+ ],
397
569
  };
398
570
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eth-compress",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Client-to-server compression (viem-compatible) module for compressed, gas-efficient, low-latency eth_call requests.",
@@ -85,7 +85,8 @@
85
85
  "clean": "rm -rf dist *.tgz",
86
86
  "test:jit": "vitest run test/jit-compress.test.ts --config test/vitest.config.ts",
87
87
  "test:demo": "vitest run test/demo.test.ts --config test/vitest.config.ts",
88
- "test": "pnpm run build && vitest run --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",
89
90
  "lint": "biome lint .",
90
91
  "lint:fix": "biome lint --write .",
91
92
  "format": "biome format --write .",
@@ -98,7 +99,7 @@
98
99
  "node": ">=22",
99
100
  "pnpm": ">=10"
100
101
  },
101
- "packageManager": "pnpm@10.20.0",
102
+ "packageManager": "pnpm@10.23.0",
102
103
  "pnpm": {
103
104
  "peerDependencyRules": {
104
105
  "ignoreMissing": [
@@ -110,16 +111,16 @@
110
111
  "solady": "0.1.26"
111
112
  },
112
113
  "devDependencies": {
113
- "@biomejs/biome": "2.3.5",
114
- "@types/bun": "1.3.2",
115
- "@types/node": "24.10.1",
116
- "typescript": "5.9.3",
114
+ "@biomejs/biome": "2.3.7",
117
115
  "@ethereumjs/common": "10.1.0",
118
116
  "@ethereumjs/util": "10.1.0",
119
- "viem": "2.39.0",
120
117
  "@ethereumjs/vm": "10.1.0",
121
- "esbuild": "0.27.00",
122
- "vitest": "4.0.9"
118
+ "@types/bun": "1.3.3",
119
+ "@types/node": "24.10.1",
120
+ "esbuild": "0.27.0",
121
+ "typescript": "5.9.3",
122
+ "viem": "2.40.2",
123
+ "vitest": "4.0.14"
123
124
  },
124
125
  "browserslist": [
125
126
  ">0.3%",