@chainfoundry/chaincodec 0.1.1 → 0.1.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/README.md CHANGED
@@ -26,7 +26,7 @@ No build step required — pre-built native binaries are bundled for all major p
26
26
  ## Platform support
27
27
 
28
28
  | Platform | Architecture | Supported |
29
- |----------|-------------|-----------|
29
+ | --- | --- | --- |
30
30
  | Linux (glibc) | x64 | ✅ |
31
31
  | Linux (glibc) | arm64 | ✅ |
32
32
  | Linux (musl / Alpine) | x64 | ✅ |
@@ -36,14 +36,34 @@ No build step required — pre-built native binaries are bundled for all major p
36
36
 
37
37
  ---
38
38
 
39
- ## Quick start
39
+ ## Quick start — decode an EVM event log
40
40
 
41
41
  ```typescript
42
42
  import { EvmDecoder, MemoryRegistry } from '@chainfoundry/chaincodec';
43
43
 
44
- // 1. Load schemas
44
+ // 1. Load your schemas (CSDL YAML format)
45
45
  const registry = new MemoryRegistry();
46
- registry.loadDirectory('./node_modules/@chainfoundry/chaincodec/schemas');
46
+
47
+ // Load from inline CSDL string
48
+ registry.loadCsdl(`
49
+ schema ERC20Transfer:
50
+ version: 1
51
+ description: "ERC-20 standard Transfer event"
52
+ chains: [ethereum, arbitrum, base, polygon, optimism]
53
+ event: Transfer
54
+ fingerprint: "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
55
+ fields:
56
+ from: { type: address, indexed: true }
57
+ to: { type: address, indexed: true }
58
+ value: { type: uint256, indexed: false }
59
+ meta:
60
+ protocol: erc20
61
+ category: token
62
+ `);
63
+
64
+ // Or load from files on disk
65
+ // registry.loadFile('./schemas/erc20.csdl');
66
+ // registry.loadDirectory('./schemas');
47
67
 
48
68
  // 2. Decode a raw log from eth_getLogs
49
69
  const decoder = new EvmDecoder();
@@ -51,8 +71,8 @@ const decoder = new EvmDecoder();
51
71
  const rawLog = {
52
72
  chain: 'ethereum',
53
73
  txHash: '0xabc123...',
54
- blockNumber: 19500000n,
55
- blockTimestamp: 1710000000n,
74
+ blockNumber: 19500000,
75
+ blockTimestamp: 1710000000,
56
76
  logIndex: 0,
57
77
  address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
58
78
  topics: [
@@ -63,16 +83,13 @@ const rawLog = {
63
83
  data: '0x00000000000000000000000000000000000000000000000000000000000f4240',
64
84
  };
65
85
 
66
- const fp = decoder.fingerprint(rawLog);
67
- const schema = registry.getByFingerprint(fp);
86
+ const event = decoder.decodeEvent(rawLog, registry);
68
87
 
69
- if (schema) {
70
- const event = decoder.decodeEvent(rawLog, schema);
71
- console.log(event.schemaName); // "ERC20Transfer"
72
- console.log(event.fields.from); // "0xd8da6bf2..."
73
- console.log(event.fields.to); // "0xab5801a7..."
74
- console.log(event.fields.value); // "1000000" (1 USDC)
75
- }
88
+ console.log(event.schema); // "ERC20Transfer"
89
+ console.log(event.fields.from); // { type: 'address', value: '0xd8da6bf2...' }
90
+ console.log(event.fields.to); // { type: 'address', value: '0xab5801a7...' }
91
+ console.log(event.fields.value); // { type: 'biguint', value: '1000000' }
92
+ console.log(event.fingerprint); // "0xddf252ad..."
76
93
  ```
77
94
 
78
95
  ---
@@ -84,159 +101,276 @@ import { EvmCallDecoder } from '@chainfoundry/chaincodec';
84
101
  import { readFileSync } from 'fs';
85
102
 
86
103
  // Load ABI JSON (from Etherscan, Hardhat artifacts, Foundry out/, etc.)
87
- const abiJson = readFileSync('./abi/uniswap_v3_router.json', 'utf-8');
104
+ const abiJson = readFileSync('./abi/erc20.json', 'utf-8');
88
105
  const decoder = EvmCallDecoder.fromAbiJson(abiJson);
89
106
 
90
107
  // Decode raw calldata from a transaction's `input` field
91
- const calldata = '0x414bf389...';
108
+ const calldata = '0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000000000000000000a';
92
109
  const call = decoder.decodeCall(calldata);
93
110
 
94
- console.log(call.functionName); // "exactInputSingle"
95
- for (const [name, value] of Object.entries(call.inputs)) {
96
- console.log(` ${name}: ${value}`);
111
+ console.log(call.functionName); // "transfer"
112
+ console.log(call.selector); // "0xa9059cbb"
113
+
114
+ for (const [name, value] of call.inputs) {
115
+ console.log(` ${name}:`, value);
97
116
  }
98
- // tokenIn: 0xC02aaA39b... (WETH)
99
- // tokenOut: 0xA0b8699... (USDC)
100
- // amountIn: 1000000000000000000
101
- // amountOutMin: 1800000000
117
+ // to: { type: 'address', value: '0xd8da6bf2...' }
118
+ // amount: { type: 'biguint', value: '10' }
119
+
120
+ // List all functions in the ABI
121
+ console.log(decoder.functionNames()); // ['transfer', 'approve', ...]
122
+ console.log(decoder.selectorFor('transfer')); // "0xa9059cbb"
102
123
  ```
103
124
 
104
125
  ---
105
126
 
127
+ ## ABI encode a function call
128
+
129
+ `encodeCall` takes `argsJson` — a JSON string of `NormalizedValue[]`.
130
+ Each `NormalizedValue` has `{ type, value }` matching the Solidity type.
131
+
132
+ ```typescript
133
+ import { EvmEncoder } from '@chainfoundry/chaincodec';
134
+
135
+ const abiJson = JSON.stringify([{
136
+ name: 'transfer',
137
+ type: 'function',
138
+ inputs: [
139
+ { name: 'to', type: 'address' },
140
+ { name: 'amount', type: 'uint256' },
141
+ ],
142
+ outputs: [{ name: '', type: 'bool' }],
143
+ stateMutability: 'nonpayable',
144
+ }]);
145
+
146
+ const encoder = EvmEncoder.fromAbiJson(abiJson);
147
+
148
+ const calldata = encoder.encodeCall(
149
+ 'transfer',
150
+ JSON.stringify([
151
+ { type: 'address', value: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' },
152
+ { type: 'uint', value: 1000000 },
153
+ ])
154
+ );
155
+
156
+ console.log(calldata);
157
+ // "0xa9059cbb000000000000000000000000d8da6bf2...000f4240"
158
+ // First 4 bytes (0xa9059cbb) = transfer(address,uint256) selector
159
+ ```
160
+
161
+ ### NormalizedValue input types for encoding
162
+
163
+ | Solidity type | `NormalizedValue` JSON |
164
+ | --- | --- |
165
+ | `address` | `{ "type": "address", "value": "0x..." }` |
166
+ | `uint256` | `{ "type": "uint", "value": 1000000 }` |
167
+ | `uint256` (large) | `{ "type": "biguint", "value": "99999999999999999999" }` |
168
+ | `int256` | `{ "type": "int", "value": -100 }` |
169
+ | `bool` | `{ "type": "bool", "value": true }` |
170
+ | `bytes` | `{ "type": "bytes", "value": [0xde, 0xad, 0xbe, 0xef] }` |
171
+ | `string` | `{ "type": "str", "value": "hello" }` |
172
+ | `address[]` | `{ "type": "array", "value": [{ "type": "address", "value": "0x..." }] }` |
173
+
174
+ ---
175
+
106
176
  ## Batch decode
107
177
 
178
+ Decode thousands of logs in parallel using Rayon (Rust's parallel iterator):
179
+
108
180
  ```typescript
109
- import { BatchEngine, BatchRequest, ErrorMode } from '@chainfoundry/chaincodec';
181
+ import { EvmDecoder, MemoryRegistry } from '@chainfoundry/chaincodec';
182
+
183
+ const registry = new MemoryRegistry();
184
+ registry.loadCsdl(/* your CSDL schemas */);
185
+
186
+ const decoder = new EvmDecoder();
187
+
188
+ // Decode a batch of raw logs — returns { events, errors }
189
+ const { events, errors } = decoder.decodeBatch(rawLogs, registry);
190
+
191
+ console.log(`decoded: ${events.length} events`);
192
+ console.log(`errors: ${errors.length}`);
110
193
 
111
- const engine = new BatchEngine(registry);
112
- engine.addDecoder('ethereum', new EvmDecoder());
194
+ for (const event of events) {
195
+ console.log(event.schema, event.fields);
196
+ }
113
197
 
114
- const logs = await fetchLogsFromRpc(fromBlock, toBlock); // RawLog[]
198
+ for (const { index, error } of errors) {
199
+ console.warn(`log[${index}] failed: ${error}`);
200
+ }
201
+ ```
202
+
203
+ ---
115
204
 
116
- const result = engine.decode({
205
+ ## Compute topic0 fingerprint
206
+
207
+ ```typescript
208
+ const decoder = new EvmDecoder();
209
+
210
+ const fp = decoder.fingerprint({
117
211
  chain: 'ethereum',
118
- logs,
119
- chunkSize: 10_000,
120
- errorMode: ErrorMode.Collect,
121
- onProgress: (decoded, total) => {
122
- process.stdout.write(`\r${decoded}/${total}`);
212
+ txHash: '0x0',
213
+ blockNumber: 0,
214
+ blockTimestamp: 0,
215
+ logIndex: 0,
216
+ address: '0x0000000000000000000000000000000000000000',
217
+ topics: ['0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'],
218
+ data: '0x',
219
+ });
220
+ console.log(fp); // "0xddf252ad..."
221
+ ```
222
+
223
+ ---
224
+
225
+ ## EIP-712 typed data
226
+
227
+ ```typescript
228
+ import { Eip712Parser } from '@chainfoundry/chaincodec';
229
+
230
+ const parser = new Eip712Parser();
231
+
232
+ const typedDataJson = JSON.stringify({
233
+ types: {
234
+ EIP712Domain: [
235
+ { name: 'name', type: 'string' },
236
+ { name: 'version', type: 'string' },
237
+ { name: 'chainId', type: 'uint256' },
238
+ ],
239
+ Transfer: [
240
+ { name: 'to', type: 'address' },
241
+ { name: 'amount', type: 'uint256' },
242
+ ],
123
243
  },
244
+ primaryType: 'Transfer',
245
+ domain: { name: 'MyToken', version: '1', chainId: 1 },
246
+ message: { to: '0xd8dA...', amount: '1000000' },
124
247
  });
125
248
 
126
- console.log(`decoded: ${result.events.length}`);
127
- console.log(`errors: ${result.errors.length}`);
249
+ const parsed = parser.parse(typedDataJson);
250
+ console.log(parsed.primary_type); // "Transfer" (snake_case from Rust serde)
251
+
252
+ const domainHash = parser.domainSeparator(typedDataJson);
253
+ console.log(domainHash); // "0x..."
128
254
  ```
129
255
 
130
256
  ---
131
257
 
132
- ## Compute topic0 fingerprint
258
+ ## Using CommonJS
133
259
 
134
- ```typescript
135
- import { computeFingerprint } from '@chainfoundry/chaincodec';
260
+ ```javascript
261
+ const { EvmDecoder, MemoryRegistry, EvmCallDecoder, EvmEncoder, Eip712Parser } = require('@chainfoundry/chaincodec');
136
262
 
137
- const fp = computeFingerprint('Transfer(address,address,uint256)');
138
- // "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
263
+ const registry = new MemoryRegistry();
264
+ registry.loadCsdl(csdlString);
139
265
 
140
- const fp2 = computeFingerprint('Swap(address,uint256,uint256,uint256,uint256,address)');
141
- // Uniswap V2 Swap topic0
266
+ const decoder = new EvmDecoder();
267
+ const event = decoder.decodeEvent(rawLog, registry);
142
268
  ```
143
269
 
144
270
  ---
145
271
 
146
272
  ## API reference
147
273
 
274
+ ### `MemoryRegistry`
275
+
276
+ | Method / Property | Signature | Description |
277
+ | --- | --- | --- |
278
+ | `constructor` | `new MemoryRegistry()` | Create empty registry |
279
+ | `loadCsdl` | `(csdl: string) => number` | Load schemas from CSDL YAML string; returns count loaded |
280
+ | `loadFile` | `(path: string) => number` | Load a single `.csdl` file |
281
+ | `loadDirectory` | `(path: string) => number` | Load all `.csdl` files in a directory |
282
+ | `schemaCount` | `readonly number` | Number of schemas registered |
283
+ | `schemaNames` | `() => string[]` | List all schema names |
284
+
148
285
  ### `EvmDecoder`
149
286
 
150
- | Method | Description |
151
- |--------|-------------|
152
- | `fingerprint(log)` | Compute topic0 fingerprint from a raw log |
153
- | `decodeEvent(log, schema)` | Decode an EVM log into named fields |
287
+ | Method | Signature | Description |
288
+ | --- | --- | --- |
289
+ | `constructor` | `new EvmDecoder()` | Create decoder |
290
+ | `decodeEvent` | `(raw: RawEvent, registry: MemoryRegistry) => DecodedEvent` | Decode a single EVM log |
291
+ | `decodeBatch` | `(raws: RawEvent[], registry: MemoryRegistry) => BatchDecodeResult` | Parallel-decode many logs |
292
+ | `fingerprint` | `(raw: RawEvent) => string` | Get topic0 fingerprint hex string |
154
293
 
155
294
  ### `EvmCallDecoder`
156
295
 
157
- | Method | Description |
158
- |--------|-------------|
159
- | `EvmCallDecoder.fromAbiJson(json)` | Create decoder from ABI JSON string |
160
- | `decodeCall(calldata, blockNumber?)` | Decode function calldata |
161
- | `encodeCall(functionName, args)` | ABI-encode a function call |
296
+ | Method | Signature | Description |
297
+ | --- | --- | --- |
298
+ | `fromAbiJson` | `(abiJson: string) => EvmCallDecoder` | Create from ABI JSON (static factory) |
299
+ | `decodeCall` | `(calldata: string, functionName?: string or null) => DecodedCall` | Decode calldata |
300
+ | `functionNames` | `() => string[]` | List all function names in ABI |
301
+ | `selectorFor` | `(functionName: string) => string or null` | Get 4-byte selector hex |
162
302
 
163
- ### `MemoryRegistry`
303
+ ### `EvmEncoder`
164
304
 
165
- | Method | Description |
166
- |--------|-------------|
167
- | `loadFile(path)` | Load a CSDL schema file |
168
- | `loadDirectory(path)` | Load all `.csdl` files in a directory |
169
- | `getByFingerprint(fp)` | Look up schema by topic0 hash |
170
- | `getByName(name)` | Look up schema by name |
171
- | `allSchemas()` | Return all registered schemas |
305
+ | Method | Signature | Description |
306
+ | --- | --- | --- |
307
+ | `fromAbiJson` | `(abiJson: string) => EvmEncoder` | Create from ABI JSON (static factory) |
308
+ | `encodeCall` | `(functionName: string, argsJson: string) => string` | Encode; `argsJson` = `JSON.stringify(NormalizedValue[])`, returns `0x`-hex |
172
309
 
173
- ### `BatchEngine`
310
+ ### `Eip712Parser`
174
311
 
175
- | Method | Description |
176
- |--------|-------------|
177
- | `addDecoder(chainSlug, decoder)` | Register a decoder for a chain |
178
- | `decode(request)` | Batch decode a list of raw logs |
312
+ | Method | Signature | Description |
313
+ | --- | --- | --- |
314
+ | `constructor` | `new Eip712Parser()` | Create parser |
315
+ | `parse` | `(json: string) => TypedData` | Parse EIP-712 typed data JSON |
316
+ | `domainSeparator` | `(json: string) => string` | Compute domain separator hash |
179
317
 
180
318
  ---
181
319
 
182
320
  ## TypeScript types
183
321
 
184
322
  ```typescript
185
- interface RawLog {
186
- chain: string;
323
+ interface RawEvent {
324
+ chain: string; // "ethereum" | "arbitrum" | "base" | "polygon" | "optimism" | numeric id
187
325
  txHash: string;
188
- blockNumber: bigint;
189
- blockTimestamp: bigint;
326
+ blockNumber: number;
327
+ blockTimestamp: number; // Unix seconds
190
328
  logIndex: number;
191
- address: string;
192
- topics: string[];
193
- data: string;
329
+ address: string; // contract address (hex, lowercase ok)
330
+ topics: string[]; // topics[0] = event signature hash
331
+ data: string; // hex with 0x prefix
194
332
  }
195
333
 
196
334
  interface DecodedEvent {
197
- schemaName: string;
335
+ schema: string; // schema name, e.g. "ERC20Transfer"
336
+ schemaVersion: number;
198
337
  chain: string;
199
- blockNumber: bigint;
200
338
  txHash: string;
339
+ blockNumber: number;
340
+ blockTimestamp: number;
201
341
  logIndex: number;
202
- fields: Record<string, string>; // NormalizedValue as string
342
+ address: string;
343
+ fields: Record<string, NormalizedValue>; // decoded fields by name
344
+ fingerprint: string; // keccak256 of topics[0]
345
+ decodeErrors: Record<string, string>; // fields that failed to decode
203
346
  }
204
347
 
205
348
  interface DecodedCall {
206
349
  functionName: string;
207
- inputs: Record<string, string>;
350
+ selector: string | null; // "0xaabbccdd"
351
+ inputs: Array<[string, NormalizedValue]>; // [name, value] pairs
352
+ decodeErrors: Record<string, string>;
208
353
  }
209
- ```
210
354
 
211
- ---
212
-
213
- ## Using CommonJS
214
-
215
- ```javascript
216
- const { EvmDecoder, MemoryRegistry } = require('@chainfoundry/chaincodec');
217
-
218
- const registry = new MemoryRegistry();
219
- registry.loadDirectory('./schemas');
220
-
221
- const decoder = new EvmDecoder();
222
- ```
223
-
224
- ---
225
-
226
- ## Bundled schemas
227
-
228
- The package ships with 50+ CSDL schemas covering ERC-20/721/1155, Uniswap, Aave, Compound, ChainLink, and more. Schemas are in the `schemas/` subdirectory of the installed package.
229
-
230
- ```typescript
231
- import { join } from 'path';
232
- import { createRequire } from 'module';
233
-
234
- const require = createRequire(import.meta.url);
235
- const schemasDir = join(
236
- require.resolve('@chainfoundry/chaincodec/package.json'),
237
- '../schemas'
238
- );
239
- registry.loadDirectory(schemasDir);
355
+ type NormalizedValue =
356
+ | { type: 'uint'; value: number }
357
+ | { type: 'biguint'; value: string } // large uint256 as decimal string
358
+ | { type: 'int'; value: number }
359
+ | { type: 'bigint'; value: string }
360
+ | { type: 'bool'; value: boolean }
361
+ | { type: 'bytes'; value: number[] }
362
+ | { type: 'str'; value: string }
363
+ | { type: 'address'; value: string } // "0x..." lowercase
364
+ | { type: 'hash256'; value: string }
365
+ | { type: 'timestamp'; value: number }
366
+ | { type: 'array'; value: NormalizedValue[] }
367
+ | { type: 'tuple'; value: Array<[string, NormalizedValue]> }
368
+ | { type: 'null' }
369
+
370
+ interface BatchDecodeResult {
371
+ events: DecodedEvent[];
372
+ errors: Array<{ index: number; error: string }>;
373
+ }
240
374
  ```
241
375
 
242
376
  ---
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chainfoundry/chaincodec",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Universal blockchain ABI decoder — production-grade event & call decoding for Ethereum and EVM chains",
5
5
  "keywords": [
6
6
  "ethereum",