{"id":47206751,"url":"https://github.com/abinnovision/seljs","last_synced_at":"2026-04-26T13:02:09.683Z","repository":{"id":344166526,"uuid":"1180748040","full_name":"abinnovision/seljs","owner":"abinnovision","description":"🔍 Query EVM smart contracts using CEL expressions!","archived":false,"fork":false,"pushed_at":"2026-04-23T17:01:26.000Z","size":1246,"stargazers_count":2,"open_issues_count":10,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-23T17:28:06.104Z","etag":null,"topics":["cel-lang","evm","solidty"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/abinnovision.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-13T11:17:24.000Z","updated_at":"2026-04-23T16:59:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/abinnovision/seljs","commit_stats":null,"previous_names":["abinnovision/seljs"],"tags_count":36,"template":false,"template_full_name":null,"purl":"pkg:github/abinnovision/seljs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abinnovision%2Fseljs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abinnovision%2Fseljs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abinnovision%2Fseljs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abinnovision%2Fseljs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/abinnovision","download_url":"https://codeload.github.com/abinnovision/seljs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abinnovision%2Fseljs/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32259472,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-25T09:15:33.318Z","status":"ssl_error","status_checked_at":"2026-04-25T09:15:31.997Z","response_time":59,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cel-lang","evm","solidty"],"created_at":"2026-03-13T14:02:28.981Z","updated_at":"2026-04-26T13:02:09.666Z","avatar_url":"https://github.com/abinnovision.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SEL — Solidity Expression Language\n\n[![npm version](https://badge.fury.io/js/@seljs%2Fruntime.svg)](https://www.npmjs.com/package/@seljs/runtime)\n[![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n\nSEL is a TypeScript library for querying EVM smart contracts using [CEL](https://cel.dev/) expressions. It provides a declarative, type-safe way to fetch and evaluate on-chain data with automatic multicall batching, dependency resolution, and atomic reads pinned to a single block.\n\n## Packages\n\n| Package                                              | Description                                                         |\n| ---------------------------------------------------- | ------------------------------------------------------------------- |\n| [`@seljs/runtime`](./packages/sel-runtime)           | Core runtime — expression evaluation, contract execution, multicall |\n| [`@seljs/env`](./packages/sel-env)                   | Schema builder — contracts and context definitions                  |\n| [`@seljs/checker`](./packages/sel-checker)           | Expression checker — parse, type-check, and infer types             |\n| [`@seljs/schema`](./packages/sel-schema)             | Schema types and JSON schema for editor integrations                |\n| [`@seljs/types`](./packages/sel-types)               | Solidity ↔ CEL type system and conversions                          |\n| [`@seljs/common`](./packages/sel-common)             | Shared utilities and error base classes                             |\n| [`@seljs/editor`](./packages/sel-editor)             | CodeMirror language support (syntax, autocomplete, linting)         |\n| [`@seljs/editor-react`](./packages/sel-editor-react) | React component for the SEL editor                                  |\n\n## Installation\n\n```bash\nnpm install @seljs/runtime @seljs/env\n```\n\nPeer dependencies: `typescript@^5`.\n\n## Quick Start\n\n```typescript\nimport { createSEL } from \"@seljs/runtime\";\nimport { buildSchema } from \"@seljs/env\";\nimport { createPublicClient, http, parseAbi } from \"viem\";\nimport { mainnet } from \"viem/chains\";\n\nconst client = createPublicClient({\n  chain: mainnet,\n  transport: http(),\n});\n\nconst ERC20_ABI = parseAbi([\n  \"function balanceOf(address account) view returns (uint256)\",\n  \"function totalSupply() view returns (uint256)\",\n]);\n\nconst schema = buildSchema({\n  contracts: {\n    token: {\n      address: \"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48\",\n      abi: ERC20_ABI,\n    },\n  },\n  context: {\n    user: \"sol_address\",\n  },\n});\n\nconst env = createSEL({ schema, client });\n\n// parseUnits inside the expression handles decimal scaling (USDC has 6 decimals)\nconst result = await env.evaluate(\n  \"token.balanceOf(user) \u003e parseUnits(1000, 6)\",\n  { user: \"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045\" },\n);\n\nconsole.log(result.value); // true or false\n```\n\n## Core Concepts\n\n### CEL Expressions\n\nSEL uses [Common Expression Language](https://cel.dev/) as its query syntax:\n\n```typescript\n// Arithmetic and comparisons\n\"token.balanceOf(user) \u003e threshold\";\n\n// Logical operators\n\"balance \u003e minBalance \u0026\u0026 balance \u003c maxBalance\";\n\n// String operations\n'name.startsWith(\"Crypto\")';\n\n// List macros\n\"tokens.all(t, t.balance \u003e 0)\";\n\n// Map literals\n'{\"hasAccess\": token.balanceOf(user) \u003e threshold}';\n```\n\n### Built-in Functions\n\n#### Type Casting\n\nContract calls return custom CEL types (`sol_int`, `sol_address`). To compare against literals, cast them with the corresponding functions:\n\n```typescript\n// Cast integer literals to sol_int\n\"token.balanceOf(user) \u003e solInt(0)\";\n\n// Large constants as decimal strings\n'token.balanceOf(user) \u003e= solInt(\"1000000000000000000000\")';\n\n// Cast string literals to sol_address\n'token.balanceOf(solAddress(\"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045\"))';\n```\n\n#### Unit Conversion\n\n`parseUnits` and `formatUnits` convert between human-readable values and scaled `sol_int` values, mirroring viem/ethers behavior:\n\n```typescript\n// 1000 USDC (6 decimals) → sol_int(1000000000)\n\"token.balanceOf(user) \u003e parseUnits(1000, 6)\";\n\n// Decimal strings for precise amounts\n'token.balanceOf(user) \u003e= parseUnits(\"1.5\", 18)';\n\n// Format a sol_int back to a human-readable double\n\"formatUnits(token.balanceOf(user), 18) \u003e 1.0\";\n```\n\n#### Math and Address Utilities\n\n```typescript\n// min / max — return the smaller or larger of two values\n\"min(token.balanceOf(user), cap)\";\n\"max(balance, solInt(0))\";\n\n// abs — absolute value\n\"abs(priceChange)\";\n\n// isZeroAddress — check if an address is the zero address\n\"isZeroAddress(token.ownerOf(tokenId))\";\n```\n\n### Schema Builder\n\nThe schema is built separately from the runtime using `buildSchema` from `@seljs/env`. This decouples schema definition from execution — the same schema can be used for type-checking, editor support, and runtime evaluation.\n\n```typescript\nimport { buildSchema } from \"@seljs/env\";\n\nconst schema = buildSchema({\n  contracts: {\n    token: { address: \"0x...\", abi: ERC20_ABI },\n  },\n  context: {\n    user: \"sol_address\", // Ethereum address\n    amount: \"sol_int\", // non-negative bigint (uint256)\n    balance: \"sol_int\", // bigint (int256)\n    active: \"bool\", // boolean\n    name: \"string\", // string\n    data: \"bytes\", // raw bytes\n  },\n});\n```\n\nContext fields can also be defined as objects with a description, which is surfaced in editor tooling:\n\n```typescript\nconst schema = buildSchema({\n  contracts: {\n    token: { address: \"0x...\", abi: ERC20_ABI },\n  },\n  context: {\n    user: { type: \"sol_address\", description: \"The wallet address to check\" },\n    threshold: {\n      type: \"sol_int\",\n      description: \"Minimum token balance required\",\n    },\n  },\n});\n```\n\nContext keys are mapped directly to CEL types for type-checking and runtime evaluation.\n\n### Lint Rules\n\nLint rules analyze the expression AST **before** any on-chain calls happen. They are passed via the `rules` option and come in two severities:\n\n- **Error** — enforcement rules that cause the runtime to throw `SELLintError` before execution\n- **Warning / Info** — advisory rules that surface in `result.diagnostics` without blocking execution\n\n```typescript\nimport { createSEL } from \"@seljs/runtime\";\nimport { expressionComplexity, requireType, rules } from \"@seljs/checker\";\n\nconst env = createSEL({\n  schema,\n  client,\n  rules: [\n    // Enforcement — throws SELLintError if violated\n    requireType(\"bool\"),\n    expressionComplexity({ maxAstNodes: 50, maxDepth: 8 }),\n\n    // Advisory — warnings/info in result.diagnostics\n    ...rules.builtIn,\n  ],\n});\n\ntry {\n  const result = await env.evaluate(\"token.balanceOf(user) \u003e solInt(0)\", {\n    user: \"0x...\",\n  });\n  console.log(result.diagnostics); // advisory warnings, if any\n} catch (error) {\n  if (error instanceof SELLintError) {\n    console.log(error.diagnostics); // which rules were violated\n  }\n}\n```\n\n#### Expression Complexity\n\nThe `expressionComplexity` rule measures five AST metrics. Each can be configured independently — set to `Infinity` to disable a metric:\n\n| Metric         | What it measures                                     | Default |\n| -------------- | ---------------------------------------------------- | ------- |\n| `maxAstNodes`  | Total AST node count                                 | 50      |\n| `maxDepth`     | Maximum nesting depth                                | 8       |\n| `maxCalls`     | Contract method call nodes in the AST                | 10      |\n| `maxOperators` | Arithmetic, comparison, and membership operators     | 15      |\n| `maxBranches`  | Ternary (`?:`) and logical (`\u0026\u0026`, `\\|\\| `) branching | 6       |\n\n`maxOperators` and `maxBranches` are distinct — `\u0026\u0026`/`||` count as branches only, not operators.\n\n#### Built-in Advisory Rules\n\n| Rule                    | Severity | What it catches                              |\n| ----------------------- | -------- | -------------------------------------------- |\n| `no-redundant-bool`     | warning  | `x == true` — simplify to `x`                |\n| `no-constant-condition` | warning  | `true \u0026\u0026 x` — likely a mistake               |\n| `no-self-comparison`    | warning  | `x == x` — always true                       |\n| `no-mixed-operators`    | info     | `a \u0026\u0026 b \\|\\| c` — add parens for clarity     |\n| `deferred-call`         | info     | Contract call can't be batched via multicall |\n\n### Automatic Multicall Batching\n\nIndependent contract calls within the same expression are batched into a single Multicall3 RPC call:\n\n```typescript\n// Both calls are independent — batched into 1 RPC request\nconst result = await env.evaluate(\n  \"token.balanceOf(user) + nft.balanceOf(user)\",\n  { user: \"0x...\" },\n);\n```\n\n### Multi-Round Execution\n\nDependent calls are automatically detected and executed in rounds:\n\n```typescript\n// Round 1: staking.stakedTokenId(user)\n// Round 2: nft.ownerOf(\u003cresult from round 1\u003e)\nconst result = await env.evaluate(\"nft.ownerOf(staking.stakedTokenId(user))\", {\n  user: \"0x...\",\n});\n```\n\nAll rounds execute against the same block number, ensuring atomicity.\n\n## Examples\n\n### Access Control\n\n```typescript\nconst schema = buildSchema({\n  contracts: {\n    membership: { address: MEMBERSHIP_ADDR, abi: ERC721_ABI },\n    token: { address: TOKEN_ADDR, abi: ERC20_ABI },\n  },\n  context: { user: \"sol_address\" },\n});\n\nconst env = createSEL({ schema, client });\n\nconst { value: hasAccess } = await env.evaluate(\n  'membership.balanceOf(user) \u003e= solInt(1) || token.balanceOf(user) \u003e= solInt(\"1000000000000000000000\")',\n  { user: \"0x...\" },\n);\n```\n\n### Dependent Contract Calls\n\n```typescript\nconst schema = buildSchema({\n  contracts: {\n    staking: { address: STAKING_ADDR, abi: STAKING_ABI },\n    nft: { address: NFT_ADDR, abi: NFT_ABI },\n  },\n  context: { user: \"sol_address\" },\n});\n\nconst env = createSEL({ schema, client });\n\n// Automatically resolves: staking call first, then nft call with the result\nconst { value: tokenOwner } = await env.evaluate(\n  \"nft.ownerOf(staking.stakedTokenId(user))\",\n  { user: \"0x...\" },\n);\n```\n\n### Dashboard Data Fetching\n\n```typescript\nconst schema = buildSchema({\n  contracts: {\n    usdc: { address: USDC_ADDR, abi: ERC20_ABI },\n    weth: { address: WETH_ADDR, abi: ERC20_ABI },\n    nft: { address: BAYC_ADDR, abi: ERC721_ABI },\n  },\n  context: { user: \"sol_address\" },\n});\n\nconst env = createSEL({ schema, client });\n\n// All independent calls batched into a single RPC request\nconst { value } = await env.evaluate(\n  `{\n    \"usdcBalance\": usdc.balanceOf(user),\n    \"wethBalance\": weth.balanceOf(user),\n    \"nftCount\": nft.balanceOf(user),\n    \"hasTokens\": usdc.balanceOf(user) \u003e solInt(0) || weth.balanceOf(user) \u003e solInt(0)\n  }`,\n  { user: \"0x...\" },\n);\n```\n\n## Execution Limits\n\n`SELLimits` controls how many resources the runtime can consume during contract call execution:\n\n```typescript\nconst env = createSEL({\n  schema,\n  client,\n  limits: {\n    maxRounds: 10, // max dependency-ordered execution rounds (default: 10)\n    maxCalls: 100, // max total contract calls across all rounds (default: 100)\n  },\n});\n```\n\nThese are hard limits — exceeding them throws `ExecutionLimitError`. They protect against runaway execution when expressions contain deeply chained or recursive contract calls.\n\nFor static complexity analysis (AST node count, nesting depth, etc.), use the [`expressionComplexity` lint rule](#expression-complexity) instead — it rejects overly complex expressions before any on-chain calls happen.\n\n### Recommended Defaults for Untrusted Input\n\nWhen evaluating user-authored expressions (e.g., from a frontend editor), use both layers together:\n\n```typescript\nimport { expressionComplexity, requireType, rules } from \"@seljs/checker\";\n\nconst env = createSEL({\n  schema,\n  client,\n  limits: {\n    maxRounds: 5, // tighter than default — limits chained RPC calls\n    maxCalls: 20, // limits total on-chain calls\n  },\n  rules: [\n    requireType(\"bool\"), // expressions must resolve to a boolean\n    expressionComplexity({\n      maxAstNodes: 40, // reject overly large expressions\n      maxDepth: 6, // prevent deeply nested logic\n      maxCalls: 8, // limit contract call complexity\n      maxOperators: 12, // cap arithmetic/comparison density\n      maxBranches: 4, // limit branching complexity\n    }),\n    ...rules.builtIn, // no-redundant-bool, no-constant-condition, etc.\n  ],\n});\n```\n\n**Execution limits** are a safety net that catches runaway execution at the RPC level. **Lint rules** reject bad expressions early with actionable error messages — before any gas is spent.\n\n## Error Handling\n\nAll errors extend `SELError`. Catch specific types for granular handling:\n\n| Error                     | When                                                          |\n| ------------------------- | ------------------------------------------------------------- |\n| `SELEvaluationError`      | Expression evaluation fails (undefined variables, etc.)       |\n| `SELLintError`            | Lint rule with error severity violated (`.diagnostics`)       |\n| `SELContractError`        | Contract call fails (includes `.contractName`, `.methodName`) |\n| `CircularDependencyError` | Circular dependency in call graph                             |\n| `ExecutionLimitError`     | `maxRounds` or `maxCalls` exceeded                            |\n| `MulticallBatchError`     | Multicall3 batch execution fails                              |\n\n## Type Mapping\n\n| Solidity                    | CEL           | JavaScript   |\n| --------------------------- | ------------- | ------------ |\n| `uint8`–`uint256`           | `sol_int`     | `bigint`     |\n| `int8`–`int256`             | `sol_int`     | `bigint`     |\n| `bool`                      | `bool`        | `boolean`    |\n| `address`                   | `sol_address` | `string`     |\n| `string`                    | `string`      | `string`     |\n| `bytes`, `bytes1`–`bytes32` | `bytes`       | `Uint8Array` |\n| `T[]`, `T[N]`               | `list\u003cT\u003e`     | `Array`      |\n| `tuple`                     | `map`         | `Object`     |\n\n### Why `sol_int` and `sol_address`?\n\nSEL registers custom CEL types instead of using the built-in `int` and `string` types:\n\n- **`sol_int`** — Wraps all Solidity integer types (`uint8`–`uint256`, `int8`–`int256`) as a single CEL type backed by native `BigInt`. This bypasses cel-js's built-in `int` type which enforces 64-bit overflow checks — necessary because Solidity integers go up to 256 bits. Cast literals with `solInt(0)` or `solInt(\"1000000000000000000\")`.\n- **`sol_address`** — Wraps Solidity `address` as a dedicated CEL type with hex validation and lowercase normalization. This ensures address comparisons are case-insensitive (matching EVM semantics) rather than relying on plain string equality.\n\n## Credits\n\n- [@marcbachmann/cel-js](https://github.com/marcbachmann/cel-js) — CEL parser, type checker, and evaluator\n- [viem](https://viem.sh/) — Ethereum client, ABI encoding/decoding\n\n## License\n\nApache-2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fabinnovision%2Fseljs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fabinnovision%2Fseljs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fabinnovision%2Fseljs/lists"}