@logrix/sdk 0.3.0 → 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
 
@@ -40,40 +40,66 @@ const fs = __importStar(require("fs"));
40
40
  const path = __importStar(require("path"));
41
41
  const yaml_1 = require("yaml");
42
42
  const templates_1 = require("../templates");
43
+ const EXPLORER_APIS = {
44
+ 1: "https://api.etherscan.io/api",
45
+ 42161: "https://api.arbiscan.io/api",
46
+ 10: "https://api-optimistic.etherscan.io/api",
47
+ 8453: "https://api.basescan.org/api",
48
+ 137: "https://api.polygonscan.com/api",
49
+ 56: "https://api.bscscan.com/api",
50
+ 43114: "https://api.snowtrace.io/api",
51
+ 59144: "https://api.lineascan.build/api",
52
+ 534352: "https://api.scrollscan.com/api",
53
+ 81457: "https://api.blastscan.io/api",
54
+ 100: "https://api.gnosisscan.io/api",
55
+ 250: "https://api.ftmscan.com/api",
56
+ 42220: "https://api.celoscan.io/api",
57
+ 1101: "https://api-zkevm.polygonscan.com/api",
58
+ 11155111: "https://api-sepolia.etherscan.io/api",
59
+ 17000: "https://api-holesky.etherscan.io/api",
60
+ 421614: "https://api-sepolia.arbiscan.io/api",
61
+ 84532: "https://api-sepolia.basescan.org/api",
62
+ 11155420: "https://api-sepolia-optimistic.etherscan.io/api",
63
+ 80002: "https://api-amoy.polygonscan.com/api",
64
+ 97: "https://api-testnet.bscscan.com/api",
65
+ 43113: "https://api-testnet.snowtrace.io/api",
66
+ 59141: "https://api-sepolia.lineascan.build/api",
67
+ 534351: "https://api-sepolia.scrollscan.com/api",
68
+ 168587773: "https://api-sepolia.blastscan.io/api",
69
+ };
43
70
  /**
44
- * Fetch ABI from Etherscan (mainnet) or Sourcify as fallback.
45
- * Returns the parsed ABI array, or null if both fail.
71
+ * Fetch ABI from Etherscan/Etherscan-compatible chain explorer or Sourcify as fallback.
72
+ * Supports all 26+ major EVM mainnets and testnets.
46
73
  */
47
- async function fetchAbi(address, etherscanKey) {
74
+ async function fetchAbi(address, chainId = 1, apiKey) {
48
75
  const addr = address.toLowerCase();
49
- // Try Etherscan first if API key provided
50
- if (etherscanKey) {
51
- try {
52
- const url = `https://api.etherscan.io/api?module=contract&action=getabi&address=${addr}&apikey=${etherscanKey}`;
53
- const resp = await fetch(url);
54
- const json = await resp.json();
55
- if (json.status === "1" && json.result) {
56
- const parsed = JSON.parse(json.result);
57
- if (Array.isArray(parsed) && parsed.length > 0) {
58
- console.log(` Fetched ABI from Etherscan for ${address}`);
59
- return parsed;
60
- }
76
+ // Try Etherscan-compatible explorer API
77
+ const baseExplorer = EXPLORER_APIS[chainId] || EXPLORER_APIS[1];
78
+ try {
79
+ const keyParam = apiKey ? `&apikey=${apiKey}` : "";
80
+ const url = `${baseExplorer}?module=contract&action=getabi&address=${addr}${keyParam}`;
81
+ const resp = await fetch(url);
82
+ const json = await resp.json();
83
+ if (json.status === "1" && json.result) {
84
+ const parsed = JSON.parse(json.result);
85
+ if (Array.isArray(parsed) && parsed.length > 0) {
86
+ console.log(` Fetched verified ABI from block explorer for ${address}`);
87
+ return parsed;
61
88
  }
62
89
  }
63
- catch {
64
- // fall through
65
- }
66
90
  }
67
- // Try Sourcify (no key required)
91
+ catch {
92
+ // fall through to Sourcify
93
+ }
94
+ // Try Sourcify (supports any chainId, no API key required)
68
95
  try {
69
- // Sourcify returns verified contracts at chainId 1 (mainnet)
70
- const url = `https://repo.sourcify.dev/contracts/full_match/1/${addr}/metadata.json`;
96
+ const url = `https://repo.sourcify.dev/contracts/full_match/${chainId}/${addr}/metadata.json`;
71
97
  const resp = await fetch(url);
72
98
  if (resp.ok) {
73
99
  const json = await resp.json();
74
100
  const abi = json?.output?.abi;
75
101
  if (Array.isArray(abi) && abi.length > 0) {
76
- console.log(` Fetched ABI from Sourcify for ${address}`);
102
+ console.log(` Fetched verified ABI from Sourcify for ${address}`);
77
103
  return abi;
78
104
  }
79
105
  }
@@ -91,8 +117,8 @@ function addCommand() {
91
117
  .argument("[name]", "Name of the component")
92
118
  .option("--address <address>", "Contract address")
93
119
  .option("--abi <path>", "Path to ABI JSON file")
94
- .option("--fetch-abi", "Auto-fetch ABI from Etherscan / Sourcify")
95
- .option("--etherscan-key <key>", "Etherscan API key for ABI fetch (optional)")
120
+ .option("--fetch-abi", "Auto-fetch ABI from block explorer / Sourcify")
121
+ .option("--etherscan-key <key>", "Explorer API key for ABI fetch (optional)")
96
122
  .option("--start-block <block>", "Starting block number", "0")
97
123
  .action(async (type, nameArg, opts) => {
98
124
  const projectDir = process.cwd();
@@ -116,6 +142,7 @@ function addCommand() {
116
142
  if (!config.dataSources) {
117
143
  config.dataSources = [];
118
144
  }
145
+ const chainId = config.network?.chainId ?? 1;
119
146
  // 1. Resolve contract name
120
147
  let contractName = nameArg;
121
148
  if (!contractName) {
@@ -168,7 +195,6 @@ function addCommand() {
168
195
  fs.mkdirSync(abisDir, { recursive: true });
169
196
  const targetAbiFile = path.join(abisDir, `${contractName}.json`);
170
197
  if (opts.abi) {
171
- // Explicit local file
172
198
  const sourceAbi = path.resolve(projectDir, opts.abi);
173
199
  if (!fs.existsSync(sourceAbi)) {
174
200
  console.error(`Error: Specified ABI file not found: ${opts.abi}`);
@@ -178,9 +204,8 @@ function addCommand() {
178
204
  console.log(` Copied ABI from ${opts.abi} to abis/${contractName}.json`);
179
205
  }
180
206
  else if (opts.fetchAbi) {
181
- // Auto-fetch from Etherscan / Sourcify
182
- console.log(` Fetching ABI for ${contractAddress} ...`);
183
- const abi = await fetchAbi(contractAddress, opts.etherscanKey);
207
+ console.log(` Fetching ABI for ${contractAddress} (Chain ID: ${chainId}) ...`);
208
+ const abi = await fetchAbi(contractAddress, chainId, opts.etherscanKey);
184
209
  if (abi) {
185
210
  fs.writeFileSync(targetAbiFile, JSON.stringify(abi, null, 2));
186
211
  console.log(` Saved fetched ABI to abis/${contractName}.json`);
@@ -191,7 +216,6 @@ function addCommand() {
191
216
  }
192
217
  }
193
218
  else if (!fs.existsSync(targetAbiFile)) {
194
- // Default placeholder
195
219
  fs.writeFileSync(targetAbiFile, JSON.stringify(templates_1.ERC20_ABI, null, 2));
196
220
  console.log(` Created default ERC-20 placeholder ABI at abis/${contractName}.json`);
197
221
  }
@@ -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
  }
@@ -33,19 +33,42 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.NETWORKS = void 0;
36
37
  exports.initCommand = initCommand;
37
38
  const commander_1 = require("commander");
38
39
  const prompts_1 = require("@inquirer/prompts");
39
40
  const fs = __importStar(require("fs"));
40
41
  const path = __importStar(require("path"));
41
42
  const templates_1 = require("../templates");
42
- const NETWORKS = {
43
- "ethereum": { name: "ethereum", chainId: 1, rpcUrl: "https://eth.llamarpc.com" },
44
- "arbitrum-one": { name: "arbitrum-one", chainId: 42161, rpcUrl: "https://arb1.arbitrum.io/rpc" },
45
- "base": { name: "base", chainId: 8453, rpcUrl: "https://mainnet.base.org" },
46
- "polygon": { name: "polygon", chainId: 137, rpcUrl: "https://polygon-rpc.com" },
47
- "arbitrum-sepolia": { name: "arbitrum-sepolia", chainId: 421614, rpcUrl: "https://sepolia-rollup.arbitrum.io/rpc" },
48
- "sepolia": { name: "sepolia", chainId: 11155111, rpcUrl: "https://rpc.sepolia.org" },
43
+ exports.NETWORKS = {
44
+ // Mainnets
45
+ "ethereum": { name: "Ethereum Mainnet", chainId: 1, rpcUrl: "https://eth.llamarpc.com", category: "mainnet" },
46
+ "arbitrum-one": { name: "Arbitrum One", chainId: 42161, rpcUrl: "https://arb1.arbitrum.io/rpc", category: "mainnet" },
47
+ "base": { name: "Base Mainnet", chainId: 8453, rpcUrl: "https://mainnet.base.org", category: "mainnet" },
48
+ "optimism": { name: "Optimism Mainnet", chainId: 10, rpcUrl: "https://mainnet.optimism.io", category: "mainnet" },
49
+ "polygon": { name: "Polygon PoS", chainId: 137, rpcUrl: "https://polygon-rpc.com", category: "mainnet" },
50
+ "bsc": { name: "BNB Smart Chain", chainId: 56, rpcUrl: "https://binance.llamarpc.com", category: "mainnet" },
51
+ "avalanche": { name: "Avalanche C-Chain", chainId: 43114, rpcUrl: "https://api.avax.network/ext/bc/C/rpc", category: "mainnet" },
52
+ "linea": { name: "Linea Mainnet", chainId: 59144, rpcUrl: "https://rpc.linea.build", category: "mainnet" },
53
+ "scroll": { name: "Scroll Mainnet", chainId: 534352, rpcUrl: "https://rpc.scroll.io", category: "mainnet" },
54
+ "blast": { name: "Blast Mainnet", chainId: 81457, rpcUrl: "https://rpc.blast.io", category: "mainnet" },
55
+ "zksync": { name: "ZKsync Era", chainId: 324, rpcUrl: "https://mainnet.era.zksync.io", category: "mainnet" },
56
+ "gnosis": { name: "Gnosis Chain", chainId: 100, rpcUrl: "https://rpc.gnosischain.com", category: "mainnet" },
57
+ "fantom": { name: "Fantom Opera", chainId: 250, rpcUrl: "https://rpcapi.fantom.network", category: "mainnet" },
58
+ "celo": { name: "Celo Mainnet", chainId: 42220, rpcUrl: "https://forno.celo.org", category: "mainnet" },
59
+ "polygon-zkevm": { name: "Polygon zkEVM", chainId: 1101, rpcUrl: "https://zkevm-rpc.com", category: "mainnet" },
60
+ // Testnets
61
+ "sepolia": { name: "Ethereum Sepolia", chainId: 11155111, rpcUrl: "https://rpc.sepolia.org", category: "testnet" },
62
+ "holesky": { name: "Ethereum Holesky", chainId: 17000, rpcUrl: "https://ethereum-holesky-rpc.publicnode.com", category: "testnet" },
63
+ "arbitrum-sepolia": { name: "Arbitrum Sepolia", chainId: 421614, rpcUrl: "https://sepolia-rollup.arbitrum.io/rpc", category: "testnet" },
64
+ "base-sepolia": { name: "Base Sepolia", chainId: 84532, rpcUrl: "https://sepolia.base.org", category: "testnet" },
65
+ "optimism-sepolia": { name: "Optimism Sepolia", chainId: 11155420, rpcUrl: "https://sepolia.optimism.io", category: "testnet" },
66
+ "polygon-amoy": { name: "Polygon Amoy", chainId: 80002, rpcUrl: "https://rpc-amoy.polygon.technology", category: "testnet" },
67
+ "bsc-testnet": { name: "BNB Smart Chain Testnet", chainId: 97, rpcUrl: "https://data-seed-prebsc-1-s1.binance.org:8545", category: "testnet" },
68
+ "avalanche-fuji": { name: "Avalanche Fuji", chainId: 43113, rpcUrl: "https://api.avax-test.network/ext/bc/C/rpc", category: "testnet" },
69
+ "linea-sepolia": { name: "Linea Sepolia", chainId: 59141, rpcUrl: "https://rpc.sepolia.linea.build", category: "testnet" },
70
+ "scroll-sepolia": { name: "Scroll Sepolia", chainId: 534351, rpcUrl: "https://sepolia-rpc.scroll.io", category: "testnet" },
71
+ "blast-sepolia": { name: "Blast Sepolia", chainId: 168587773, rpcUrl: "https://sepolia.blast.io", category: "testnet" },
49
72
  };
50
73
  function initCommand() {
51
74
  const cmd = new commander_1.Command("init");
@@ -53,7 +76,7 @@ function initCommand() {
53
76
  .description("Scaffold a new Logrix indexer project")
54
77
  .argument("[name]", "Project directory name")
55
78
  .option("-y, --yes", "Skip prompts and use defaults (non-interactive)")
56
- .option("--network <network>", "Network preset name (e.g. arbitrum-one, ethereum, base)")
79
+ .option("--network <network>", "Network preset name (e.g. arbitrum-one, ethereum, base, sepolia)")
57
80
  .option("--rpc <url>", "Custom RPC endpoint URL")
58
81
  .option("--contract-name <name>", "Contract name")
59
82
  .option("--address <address>", "Contract target address")
@@ -83,22 +106,22 @@ function initCommand() {
83
106
  }
84
107
  // Network selection
85
108
  let networkKey = opts.network;
86
- if (!networkKey || !NETWORKS[networkKey]) {
109
+ if (!networkKey || !exports.NETWORKS[networkKey]) {
87
110
  if (isNonInteractive) {
88
111
  networkKey = "arbitrum-one";
89
112
  }
90
113
  else {
91
114
  networkKey = await (0, prompts_1.select)({
92
115
  message: "Network:",
93
- choices: Object.entries(NETWORKS).map(([key, net]) => ({
94
- name: `${net.name} (chain ${net.chainId})`,
116
+ choices: Object.entries(exports.NETWORKS).map(([key, net]) => ({
117
+ name: `[${net.category.toUpperCase()}] ${net.name} (Chain ID: ${net.chainId})`,
95
118
  value: key,
96
119
  })),
97
120
  default: "arbitrum-one",
98
121
  });
99
122
  }
100
123
  }
101
- const network = NETWORKS[networkKey] || NETWORKS["arbitrum-one"];
124
+ const network = exports.NETWORKS[networkKey] || exports.NETWORKS["arbitrum-one"];
102
125
  // RPC URL
103
126
  let rpcUrl = opts.rpc;
104
127
  if (!rpcUrl) {
@@ -140,7 +163,7 @@ function initCommand() {
140
163
  contractAddress = await (0, prompts_1.input)({
141
164
  message: "Contract address:",
142
165
  default: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
143
- validate: (v) => /^0x[0-9a-fA-F]{40}$/.test(v.trim()) || "Must be a valid 0x address",
166
+ validate: (v) => /^0x[0-9a-fA-F]{40}$/.test(v.trim()) || "Must be a valid 0x hex address",
144
167
  });
145
168
  }
146
169
  }
@@ -153,41 +176,34 @@ function initCommand() {
153
176
  }
154
177
  else {
155
178
  startBlockStr = await (0, prompts_1.input)({
156
- message: "Start block:",
179
+ message: "Start block (indexing starting point):",
157
180
  default: "0",
158
181
  validate: (v) => /^\d+$/.test(v.trim()) || "Must be a non-negative integer",
159
182
  });
160
183
  }
161
184
  }
185
+ const startBlock = parseInt(startBlockStr.trim(), 10);
162
186
  const answers = {
163
187
  projectName,
164
- networkName: network.name,
188
+ networkName: networkKey,
165
189
  chainId: network.chainId,
166
190
  rpcUrl,
167
191
  contractName,
168
192
  contractAddress,
169
- startBlock: parseInt(startBlockStr.trim(), 10),
193
+ startBlock,
170
194
  };
171
- console.log(`Scaffolding in ./${projectName}/ ...\n`);
172
- // Directory structure
195
+ console.log(`\nScaffolding in ./${projectName}/ ...\n`);
196
+ // Create directories
173
197
  fs.mkdirSync(path.join(targetDir, "abis"), { recursive: true });
174
198
  fs.mkdirSync(path.join(targetDir, "src"), { recursive: true });
175
- fs.mkdirSync(path.join(targetDir, "build"), { recursive: true });
176
- // logrix.yaml
199
+ // Write files
177
200
  fs.writeFileSync(path.join(targetDir, "logrix.yaml"), (0, templates_1.logrixYamlTemplate)(answers));
178
- // schema.graphql
179
201
  fs.writeFileSync(path.join(targetDir, "schema.graphql"), (0, templates_1.schemaGraphqlTemplate)());
180
- // Placeholder ABI
181
- fs.writeFileSync(path.join(targetDir, "abis", `${answers.contractName}.json`), JSON.stringify(templates_1.ERC20_ABI, null, 2));
182
- // src/mapping.ts
183
- fs.writeFileSync(path.join(targetDir, "src", "mapping.ts"), (0, templates_1.mappingTemplate)(answers.contractName));
184
- // package.json
202
+ fs.writeFileSync(path.join(targetDir, "abis", `${contractName}.json`), JSON.stringify(templates_1.ERC20_ABI, null, 2));
203
+ fs.writeFileSync(path.join(targetDir, "src", "mapping.ts"), (0, templates_1.mappingTemplate)(contractName));
185
204
  fs.writeFileSync(path.join(targetDir, "package.json"), JSON.stringify((0, templates_1.packageJsonTemplate)(projectName), null, 2));
186
- // tsconfig.json (AS config)
187
205
  fs.writeFileSync(path.join(targetDir, "tsconfig.json"), JSON.stringify((0, templates_1.tsconfigTemplate)(), null, 2));
188
- // .gitignore
189
206
  fs.writeFileSync(path.join(targetDir, ".gitignore"), (0, templates_1.gitignoreTemplate)());
190
- // README.md
191
207
  fs.writeFileSync(path.join(targetDir, "README.md"), (0, templates_1.readmeTemplate)(projectName));
192
208
  console.log("Done. Next steps:\n");
193
209
  console.log(` cd ${projectName}`);
@@ -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.0");
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.0",
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
+ }