Client-to-server compression (viem-compatible) module for compressed, gas-efficient, low-latency eth_call requests.
npm install eth-compressA compact client-side module for compressing Ethereum JSON-RPC requests, targeting lower latency and gas-efficient read-only calls with large calldata.
It combines RFC 9110-compliant negotiation for client-to-server compression, with optional JIT-compiled calldata compression.
_Plug 'n' play with viem and a simple API_
eth_calls.Content-Encoding negotiation (gzip/deflate).eth_calls through a transient decompressor contract.``bash`
npm i eth-compress
---$3
Transparently compresses request bodies using the CompressionStream API.
`ts
import { compressModule } from 'eth-compress';
const response = await compressModule('https://rpc.example.org', {
method: 'POST',
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'eth_call',
params: [/ ... /],
}),
});
`
| Mode | Behavior |
|------|----------|
| 'passive' | Discover support from response Accept-Encoding header |'proactive'
| | Send gzip; discover alternative / lacking support via Accept-Encoding response header, error or success |'gzip'
| / 'deflate' | Use specified encoding directly |(payload) => ...
| | Custom transform; server expected to understand |
----
Passive (default):
`ts
import { createPublicClient, http } from 'viem';
import { compressModule } from 'eth-compress';
const client = createPublicClient({
chain: base,
transport: http(rpcUrl, { fetchFn: compressModule }),
});
`
Known gzip support:
`ts
import { compressModule } from 'eth-compress';
const client = createPublicClient({
chain: base,
transport: http(rpcUrl, {
fetchFn: (url, init) => compressModule(url, init, 'gzip'),
}),
});
`
JIT calldata compression:
`ts
import { compressModule } from 'eth-compress';
import { compress_call } from 'eth-compress/compressor';
const client = createPublicClient({
chain: base,
transport: http(rpcUrl, {
fetchFn: (url, init) => compressModule(url, init, compress_call),
}),
});
`
----$3
- Preserves viem semantics: responses and error handling are unchanged; only the request body is compressed.
- Works in Node and modern browsers that support the CompressionStream API.
Chrome/Edge ≥ 80; Firefox ≥ 113; Safari/iOS ≥ 16.4
----
Eligible eth_calls are compiled into a transient decompressor contract (passed via stateDiff).
`ts
import { compress_call } from 'eth-compress/compressor';
const payload = {
method: 'eth_call',
params: [
{
to: '0x…',
data: '0x…',
},
'latest',
],
};
const compressedPayload = compress_call(payload);
`
compress_call can be passed directly to compressModule as a custom transform. For eligible eth_calls, it chooses between:
- JIT: Compiles just-in-time, a one-off decompressor contract that reconstructs calldata to forward the call.
- FLZ / CD: Uses LibZip.flzCompress and LibZip.cdCompress from solady for FastLZ / RLE compression.
- Size gating (JIT / EVM path):
- < 1150 bytes (effective payload): no EVM-level compression.≥ 1150 bytes
- : compression considered.size ≤ ~3000 bytes or > ~8000 bytes
- : JIT is preferred.~3000 ≤ size ≤ ~8000 bytes
- : Best-of-3.
- Algorithm choice:
- For mid-sized payloads, FLZ and CD are tried and the smaller output is chosen.
- For larger ones, JIT is used directly, prioritizing gas efficiency.
- The thresholds are chosen with request header overhead and latency in mind,
aiming to keep the total request size within the Ethernet MTU.
The JIT calldata compressor is experimental and intended for auxiliary/bulk dApp read-only eth_calls. Use two viem clients to separate concerns.
Excludes txns not compressible to <70% of original size.
JIT mode): Views calldata as a zero‑initialized memory image and synthesizes bytecode that rebuilds it word-by-word in-place.In the first pass it walks the data in 32-byte slices, detects non-zero segments per word, and for each word chooses the cheapest of three strategies: store a literal tail, assemble segments using SHL/OR, or reuse an earlier word via MLOAD/MSTORE.
In the second pass it materializes this plan into concrete PUSH/MSTORE/SHL/OR/DUP opcodes, pre-seeds the stack with frequently used constants, and appends a small CALL/RETURNDATA stub that forwards the reconstructed calldata to the original to address.
The 4‑byte selector is right‑aligned in the first 32‑byte slot so that the rest of the calldata can be reconstructed on mostly word‑aligned boundaries, with the decompressor stateDiff being placed at 0xe0 to obtain this common offset from ADDRESS with a single opcode instead of PUSH1 + literal.
Both the FastLZ and calldata-RLE forwarders are minimally adapted from Solady's LibZip.sol` and inlined as raw bytecode. To avoid Solidity's wrapper overhead the code is compiled from pure yul.