@logrix/sdk 0.3.1 → 0.3.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
@@ -24,11 +24,11 @@ npm test
24
24
 
25
25
  ### `logrix init [name]`
26
26
 
27
- Scaffolds a new indexer project. Run interactively or pass flags for CI/CD automation.
27
+ Scaffolds a new indexer project with built-in presets for 26+ popular mainnets and testnets. Run interactively or pass flags for CI/CD automation.
28
28
 
29
29
  Options:
30
30
  - `-y, --yes` -- non-interactive mode using sensible defaults
31
- - `--network <name>` -- network preset (`arbitrum-one`, `ethereum`, `base`, `polygon`, `sepolia`)
31
+ - `--network <name>` -- network preset (`arbitrum-one`, `ethereum`, `base`, `optimism`, `polygon`, `bsc`, `avalanche`, `sepolia`, etc.)
32
32
  - `--rpc <url>` -- custom RPC endpoint
33
33
  - `--contract-name <name>` -- contract name
34
34
  - `--address <address>` -- target contract 0x address
@@ -51,6 +51,23 @@ my-indexer/
51
51
 
52
52
  ---
53
53
 
54
+ ### `logrix status`
55
+
56
+ Checks live JSON-RPC connectivity, measures round-trip latency, verifies chain ID match, checks latest block height, and verifies that target smart contracts are deployed with on-chain bytecode.
57
+
58
+ ```bash
59
+ npx logrix status
60
+ ```
61
+
62
+ Output includes:
63
+ - Network Name & Chain ID verification
64
+ - RPC reachability and response latency (ms)
65
+ - Latest chain block number
66
+ - Contract deployment verification (`eth_getCode` bytecode inspection)
67
+ - Sync scope and remaining block delta
68
+
69
+ ---
70
+
54
71
  ### `logrix add contract <name>`
55
72
 
56
73
  Adds an additional smart contract to an existing `logrix.yaml` project for multi-contract indexing.
@@ -62,7 +79,7 @@ npx logrix add contract SecondaryToken \
62
79
  --abi ./my-abis/SecondaryToken.json \
63
80
  --start-block 1000000
64
81
 
65
- # Add with automatic ABI fetch from Etherscan / Sourcify
82
+ # Add with automatic ABI fetch from block explorers / Sourcify
66
83
  npx logrix add contract DaiToken \
67
84
  --address 0x6b175474e89094c44da98b954eedeac495271d0f \
68
85
  --fetch-abi
@@ -71,8 +88,8 @@ npx logrix add contract DaiToken \
71
88
  Options:
72
89
  - `--address <address>` -- contract address
73
90
  - `--abi <path>` -- path to local ABI JSON
74
- - `--fetch-abi` -- auto-fetch verified contract ABI from Etherscan / Sourcify
75
- - `--etherscan-key <key>` -- optional Etherscan API key for rate limits
91
+ - `--fetch-abi` -- auto-fetch verified contract ABI from block explorers (Etherscan, Arbiscan, Basescan, Polygonscan, etc.) and Sourcify
92
+ - `--etherscan-key <key>` -- optional block explorer API key for rate limits
76
93
  - `--start-block <block>` -- initial indexing block
77
94
 
78
95
  ---
@@ -96,7 +113,7 @@ npx logrix codegen
96
113
  ```
97
114
 
98
115
  Outputs:
99
- - `src/generated/events.ts` -- typed event and parameter classes (namespaced per contract), multi-contract address constants, and router helpers
116
+ - `src/generated/events.ts` -- typed event and parameter classes (namespaced per contract), multi-contract address constants, router helpers, and dynamic ABI decoding (`string`, `bytes`)
100
117
  - `src/generated/schema.ts` -- entity classes with static `.load(id)`, `.remove(id)`, and `.save()`, handling `@derivedFrom` relationships
101
118
 
102
119
  ---
@@ -165,6 +182,9 @@ Imported via `import { ... } from "@logrix/sdk"` in AssemblyScript handlers:
165
182
  | `Bytes` | Hex byte wrapper: `.fromHexString()`, `.fromUTF8()`, `.toHexString()`, `.toByteArray()`, `.length` |
166
183
  | `crypto.keccak256(data)` | Computes standard EVM Keccak-256 hash |
167
184
  | `formatUnits(val, decimals)` | Formats raw BigInt value into human-readable decimal string (default: 18) |
185
+ | `decodeDynamicString(data, idx)` | Decodes dynamic string parameter from raw ABI event data |
186
+ | `decodeDynamicBytes(data, idx)` | Decodes dynamic bytes parameter from raw ABI event data |
187
+ | `createMockEventLog(config)` | Creates mock EventLog instance for testing mapping logic |
168
188
  | `log` | Structured logging: `log.info()`, `log.warning()`, `log.error()`, `log.debug()` |
169
189
  | `allocate(size)` | Guest memory allocator (must be re-exported by `mapping.ts`) |
170
190
 
@@ -55,6 +55,13 @@ function buildCommand() {
55
55
  console.error(`Error: entry file not found: ${opts.entry}`);
56
56
  process.exit(1);
57
57
  }
58
+ // Check if generated bindings exist
59
+ const generatedSchema = path.join(projectDir, "src", "generated", "schema.ts");
60
+ const generatedEvents = path.join(projectDir, "src", "generated", "events.ts");
61
+ if (!fs.existsSync(generatedSchema) || !fs.existsSync(generatedEvents)) {
62
+ console.warn("Notice: Generated bindings (src/generated/) not found or incomplete.");
63
+ console.warn("Tip: Run `logrix codegen` before `logrix build` to generate typed classes.\n");
64
+ }
58
65
  // Ensure output directory exists
59
66
  fs.mkdirSync(path.dirname(outFile), { recursive: true });
60
67
  // Prefer logrix.yaml mapping.file if present
@@ -81,7 +88,7 @@ function buildCommand() {
81
88
  ascBin = "asc"; // global
82
89
  }
83
90
  const optimizeFlag = opts.debug ? "" : "--optimize";
84
- const cmd = [
91
+ const ascCmd = [
85
92
  ascBin,
86
93
  resolvedEntry,
87
94
  "--config", "tsconfig.json",
@@ -91,14 +98,18 @@ function buildCommand() {
91
98
  ]
92
99
  .filter(Boolean)
93
100
  .join(" ");
94
- console.log(`Running: ${cmd}\n`);
101
+ console.log(`Running: ${ascCmd}\n`);
95
102
  try {
96
- (0, child_process_1.execSync)(cmd, { cwd: projectDir, stdio: "inherit" });
103
+ (0, child_process_1.execSync)(ascCmd, { cwd: projectDir, stdio: "inherit" });
97
104
  const stat = fs.statSync(outFile);
98
105
  console.log(`\nBuild complete: ${path.relative(projectDir, outFile)} (${stat.size} bytes)`);
99
106
  }
100
107
  catch {
101
108
  console.error("\nBuild failed.");
109
+ console.error("Troubleshooting tips:");
110
+ console.error(" 1. Verify AssemblyScript types in src/mapping.ts (e.g. use BigInt / Address / Bytes from @logrix/sdk)");
111
+ console.error(" 2. Run `logrix codegen` to refresh generated bindings");
112
+ console.error(" 3. Run `logrix validate` to check configuration syntax\n");
102
113
  process.exit(1);
103
114
  }
104
115
  });
@@ -57,7 +57,7 @@ function solTypeToAs(solType) {
57
57
  function generateEventsTs(dataSources, projectDir) {
58
58
  const lines = [
59
59
  "// Auto-generated by `logrix codegen`. Do not edit.",
60
- 'import { EventLog, BigInt, Address, Bytes } from "@logrix/sdk";',
60
+ 'import { EventLog, BigInt, Address, Bytes, decodeDynamicString, decodeDynamicBytes } from "@logrix/sdk";',
61
61
  "",
62
62
  ];
63
63
  // Contract address constants for multi-contract indexers
@@ -138,23 +138,31 @@ function generateEventsTs(dataSources, projectDir) {
138
138
  topicIdx++;
139
139
  }
140
140
  else {
141
- lines.push(` {`);
142
- lines.push(` const dataHex = raw.data.startsWith("0x") ? raw.data.slice(2) : raw.data;`);
143
- lines.push(` const offset = ${nonIndexedIdx * 64};`);
144
- lines.push(` const rawWord = dataHex.length >= offset + 64 ? "0x" + dataHex.slice(offset, offset + 64) : (offset == 0 ? raw.data : "");`);
145
- if (asType === "BigInt") {
146
- lines.push(` this.${inp.name} = BigInt.fromString(rawWord);`);
147
- }
148
- else if (asType === "Address") {
149
- lines.push(` this.${inp.name} = Address.fromString(rawWord);`);
141
+ if (inp.type === "string") {
142
+ lines.push(` this.${inp.name} = decodeDynamicString(raw.data, ${nonIndexedIdx});`);
150
143
  }
151
- else if (asType === "Bytes") {
152
- lines.push(` this.${inp.name} = Bytes.fromHexString(rawWord);`);
144
+ else if (inp.type === "bytes") {
145
+ lines.push(` this.${inp.name} = decodeDynamicBytes(raw.data, ${nonIndexedIdx});`);
153
146
  }
154
147
  else {
155
- lines.push(` this.${inp.name} = rawWord;`);
148
+ lines.push(` {`);
149
+ lines.push(` const dataHex = raw.data.startsWith("0x") ? raw.data.slice(2) : raw.data;`);
150
+ lines.push(` const offset = ${nonIndexedIdx * 64};`);
151
+ lines.push(` const rawWord = dataHex.length >= offset + 64 ? "0x" + dataHex.slice(offset, offset + 64) : (offset == 0 ? raw.data : "");`);
152
+ if (asType === "BigInt") {
153
+ lines.push(` this.${inp.name} = BigInt.fromString(rawWord);`);
154
+ }
155
+ else if (asType === "Address") {
156
+ lines.push(` this.${inp.name} = Address.fromString(rawWord);`);
157
+ }
158
+ else if (asType === "Bytes") {
159
+ lines.push(` this.${inp.name} = Bytes.fromHexString(rawWord);`);
160
+ }
161
+ else {
162
+ lines.push(` this.${inp.name} = rawWord;`);
163
+ }
164
+ lines.push(` }`);
156
165
  }
157
- lines.push(` }`);
158
166
  nonIndexedIdx++;
159
167
  }
160
168
  }
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.statusCommand = statusCommand;
37
+ const commander_1 = require("commander");
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const yaml_1 = require("yaml");
41
+ async function jsonRpcCall(url, method, params = []) {
42
+ const start = Date.now();
43
+ try {
44
+ const res = await fetch(url, {
45
+ method: "POST",
46
+ headers: { "Content-Type": "application/json" },
47
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
48
+ });
49
+ const latencyMs = Date.now() - start;
50
+ if (!res.ok) {
51
+ return { error: `HTTP ${res.status}: ${res.statusText}`, latencyMs };
52
+ }
53
+ const json = await res.json();
54
+ return { result: json.result, error: json.error, latencyMs };
55
+ }
56
+ catch (e) {
57
+ return { error: e.message, latencyMs: Date.now() - start };
58
+ }
59
+ }
60
+ function statusCommand() {
61
+ const cmd = new commander_1.Command("status");
62
+ cmd
63
+ .description("Check RPC connectivity, network block height, and contract deployment status")
64
+ .action(async () => {
65
+ const projectDir = process.cwd();
66
+ const configPath = path.join(projectDir, "logrix.yaml");
67
+ if (!fs.existsSync(configPath)) {
68
+ console.error("Error: logrix.yaml not found. Run this command from your project root.");
69
+ process.exit(1);
70
+ }
71
+ let config;
72
+ try {
73
+ config = (0, yaml_1.parse)(fs.readFileSync(configPath, "utf-8"));
74
+ }
75
+ catch (e) {
76
+ console.error(`Error parsing logrix.yaml: ${e.message}`);
77
+ process.exit(1);
78
+ }
79
+ const networkName = config.network?.name || "unspecified";
80
+ const configuredChainId = config.network?.chainId;
81
+ const rpcUrl = config.network?.rpcUrl;
82
+ console.log("\nLogrix Indexer Status");
83
+ console.log("============================================================");
84
+ console.log(`Network Name: ${networkName}`);
85
+ console.log(`Configured Chain: ${configuredChainId ?? "Not set"}`);
86
+ console.log(`RPC Endpoint: ${rpcUrl || "None"}`);
87
+ if (!rpcUrl) {
88
+ console.error("\nError: No RPC URL configured in logrix.yaml.");
89
+ console.log("============================================================\n");
90
+ process.exit(1);
91
+ }
92
+ // 1. Query block height
93
+ process.stdout.write("Connecting to RPC endpoint ... ");
94
+ const blockRes = await jsonRpcCall(rpcUrl, "eth_blockNumber");
95
+ if (blockRes.error || !blockRes.result) {
96
+ console.log("FAILED");
97
+ console.error(`RPC Error: ${JSON.stringify(blockRes.error)} (${blockRes.latencyMs} ms)`);
98
+ console.log("============================================================\n");
99
+ process.exit(1);
100
+ }
101
+ const latestBlock = parseInt(blockRes.result, 16);
102
+ console.log(`ONLINE (${blockRes.latencyMs} ms)`);
103
+ console.log(`Latest Block: #${latestBlock.toLocaleString()}`);
104
+ // 2. Query chain ID
105
+ const chainRes = await jsonRpcCall(rpcUrl, "eth_chainId");
106
+ if (chainRes.result) {
107
+ const reportedChainId = parseInt(chainRes.result, 16);
108
+ const match = configuredChainId === reportedChainId;
109
+ console.log(`Chain ID Check: Reported ${reportedChainId} (${match ? "VERIFIED MATCH" : "MISMATCH WARNING"})`);
110
+ }
111
+ // 3. Inspect contracts
112
+ const dataSources = config.dataSources || [];
113
+ console.log(`\nContracts Monitored (${dataSources.length}):`);
114
+ console.log("------------------------------------------------------------");
115
+ for (const ds of dataSources) {
116
+ const addr = ds.source?.address || "";
117
+ const name = ds.name || "Unnamed";
118
+ const startBlock = ds.source?.startBlock ?? 0;
119
+ if (!addr) {
120
+ console.log(` - ${name}: [NO ADDRESS SPECIFIED]`);
121
+ continue;
122
+ }
123
+ const codeRes = await jsonRpcCall(rpcUrl, "eth_getCode", [addr, "latest"]);
124
+ const code = codeRes.result || "";
125
+ const isDeployed = code.length > 2 && code !== "0x" && code !== "0x0";
126
+ const byteSize = isDeployed ? (code.length - 2) / 2 : 0;
127
+ console.log(` - Name: ${name}`);
128
+ console.log(` Address: ${addr}`);
129
+ console.log(` Deployed: ${isDeployed ? `YES (${byteSize.toLocaleString()} bytes bytecode)` : "NO (Empty bytecode / Not found)"}`);
130
+ console.log(` Start Block: #${startBlock.toLocaleString()}`);
131
+ if (latestBlock > startBlock) {
132
+ const lag = latestBlock - startBlock;
133
+ console.log(` Sync Scope: #${startBlock} -> #${latestBlock} (${lag.toLocaleString()} blocks to process)`);
134
+ }
135
+ console.log("");
136
+ }
137
+ console.log("============================================================\n");
138
+ });
139
+ return cmd;
140
+ }
package/dist/index.js CHANGED
@@ -9,11 +9,12 @@ const add_1 = require("./commands/add");
9
9
  const validate_1 = require("./commands/validate");
10
10
  const export_values_1 = require("./commands/export_values");
11
11
  const test_1 = require("./commands/test");
12
+ const status_1 = require("./commands/status");
12
13
  const program = new commander_1.Command();
13
14
  program
14
15
  .name("logrix")
15
16
  .description("Developer CLI for Logrix blockchain indexers")
16
- .version("0.3.1");
17
+ .version("0.3.2");
17
18
  program.addCommand((0, init_1.initCommand)());
18
19
  program.addCommand((0, add_1.addCommand)());
19
20
  program.addCommand((0, codegen_1.codegenCommand)());
@@ -21,4 +22,5 @@ program.addCommand((0, validate_1.validateCommand)());
21
22
  program.addCommand((0, build_1.buildCommand)());
22
23
  program.addCommand((0, export_values_1.exportValuesCommand)());
23
24
  program.addCommand((0, test_1.testCommand)());
25
+ program.addCommand((0, status_1.statusCommand)());
24
26
  program.parse(process.argv);
package/index.ts CHANGED
@@ -1,3 +1,4 @@
1
- // Root AssemblyScript entry point — re-exports the full runtime.
1
+ // Root entry point -- re-exports runtime and testing utilities.
2
2
  // asc resolves @logrix/sdk to this file when building user handler projects.
3
3
  export * from "./src/runtime/index";
4
+ export * from "./src/testing/index";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logrix/sdk",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Official CLI and AssemblyScript runtime library for Logrix blockchain indexers",
5
5
  "main": "index.ts",
6
6
  "bin": {
@@ -13,6 +13,7 @@
13
13
  "files": [
14
14
  "dist",
15
15
  "src/runtime",
16
+ "src/testing",
16
17
  "index.ts",
17
18
  "tsconfig.as.json",
18
19
  "README.md"
@@ -258,6 +258,58 @@ export class Bytes {
258
258
  }
259
259
  }
260
260
 
261
+ // ---------------------------------------------------------------------------
262
+ // Dynamic ABI Offset Decoding Helpers (Solidity Head/Tail)
263
+ // ---------------------------------------------------------------------------
264
+
265
+ function parseHexI32(hex: string): i32 {
266
+ let val: i32 = 0;
267
+ for (let i = 0; i < hex.length; i++) {
268
+ val = (val << 4) | parseHexChar(hex.charCodeAt(i));
269
+ }
270
+ return val;
271
+ }
272
+
273
+ export function decodeDynamicString(dataHex: string, paramIndex: i32): string {
274
+ let clean = dataHex.startsWith("0x") ? dataHex.slice(2) : dataHex;
275
+ const headOffset = paramIndex * 64;
276
+ if (clean.length < headOffset + 64) return "";
277
+ const ptrWord = clean.slice(headOffset, headOffset + 64);
278
+ const offsetBytes = parseHexI32(ptrWord);
279
+ const offsetHex = offsetBytes * 2;
280
+ if (clean.length < offsetHex + 64) return "";
281
+ const lenWord = clean.slice(offsetHex, offsetHex + 64);
282
+ const byteLen = parseHexI32(lenWord);
283
+ const contentHexStart = offsetHex + 64;
284
+ const contentHexEnd = contentHexStart + byteLen * 2;
285
+ if (clean.length < contentHexEnd) return "";
286
+ const contentHex = clean.slice(contentHexStart, contentHexEnd);
287
+ const bytes = new Uint8Array(byteLen);
288
+ for (let i = 0; i < byteLen; i++) {
289
+ const high = parseHexChar(contentHex.charCodeAt(i * 2));
290
+ const low = parseHexChar(contentHex.charCodeAt(i * 2 + 1));
291
+ bytes[i] = ((high << 4) | low) as u8;
292
+ }
293
+ return String.UTF8.decode(bytes.buffer);
294
+ }
295
+
296
+ export function decodeDynamicBytes(dataHex: string, paramIndex: i32): Bytes {
297
+ let clean = dataHex.startsWith("0x") ? dataHex.slice(2) : dataHex;
298
+ const headOffset = paramIndex * 64;
299
+ if (clean.length < headOffset + 64) return Bytes.fromHexString("0x");
300
+ const ptrWord = clean.slice(headOffset, headOffset + 64);
301
+ const offsetBytes = parseHexI32(ptrWord);
302
+ const offsetHex = offsetBytes * 2;
303
+ if (clean.length < offsetHex + 64) return Bytes.fromHexString("0x");
304
+ const lenWord = clean.slice(offsetHex, offsetHex + 64);
305
+ const byteLen = parseHexI32(lenWord);
306
+ const contentHexStart = offsetHex + 64;
307
+ const contentHexEnd = contentHexStart + byteLen * 2;
308
+ if (clean.length < contentHexEnd) return Bytes.fromHexString("0x");
309
+ const contentHex = clean.slice(contentHexStart, contentHexEnd);
310
+ return Bytes.fromHexString("0x" + contentHex);
311
+ }
312
+
261
313
  // ---------------------------------------------------------------------------
262
314
  // Keccak-256 Implementation & Crypto namespace
263
315
  // ---------------------------------------------------------------------------
@@ -0,0 +1,29 @@
1
+ import { EventLog } from "../runtime";
2
+
3
+ export interface MockEventLogConfig {
4
+ address?: string;
5
+ blockNumber?: number;
6
+ blockHash?: string;
7
+ transactionHash?: string;
8
+ logIndex?: number;
9
+ blockTimestamp?: number;
10
+ topics?: string[];
11
+ data?: string;
12
+ }
13
+
14
+ /**
15
+ * Construct an EventLog instance pre-populated with test values.
16
+ * Useful for writing local unit tests for AssemblyScript/TypeScript mapping handlers.
17
+ */
18
+ export function createMockEventLog(config: MockEventLogConfig = {}): EventLog {
19
+ const log = new EventLog();
20
+ log.address = config.address || "0x0000000000000000000000000000000000000000";
21
+ log.block_number = BigInt(config.blockNumber ?? 1) as any;
22
+ log.block_hash = config.blockHash || "0x0000000000000000000000000000000000000000000000000000000000000000";
23
+ log.transaction_hash = config.transactionHash || "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
24
+ log.log_index = (config.logIndex ?? 0) as any;
25
+ log.block_timestamp = BigInt(config.blockTimestamp ?? 1700000000) as any;
26
+ log.topics = config.topics || [];
27
+ log.data = config.data || "0x";
28
+ return log;
29
+ }