Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ See [ARCHITECTURE.md](ARCHITECTURE.md) for the full specification.

| Package | Description |
|---------|-------------|
| `@openscan/utils` | Zero-dep utilities: hex, units, address validation, ABI, events, signatures |
| `@openscan/utils` | Zero-dep utilities: hex, units, address validation, ABI, events, signatures, tx-analysis (call trees, prestate diffs, contract enrichment) |
| `@openscan/algorithms` | On-chain algorithms: tx history, token balance, gas price |
| `@openscan/cli` | CLI tool (`openscan`) wrapping algorithms and utils |
| `@openscan/skills` | Markdown-based procedural knowledge for AI agents (skills.sh format, in `skills/` not `packages/`) |
Expand All @@ -36,14 +36,19 @@ See [ARCHITECTURE.md](ARCHITECTURE.md) for the full specification.
- **Zero production deps** in `@openscan/utils` (matching network-connectors philosophy)
- **ES Modules** throughout (`"type": "module"`)

## Before Committing
## Workflow Rules

```bash
pnpm format:fix
pnpm lint:fix
pnpm typecheck
pnpm test
```
- **TDD always**: Write tests first, then implementation. Before writing any tests, ask the user which tests to add.
- **Never co-author commits**: Do not add `Co-Authored-By: Claude` or any Claude/Anthropic attribution to commits.
- **Conventional Commits**: All commit messages must follow the [Conventional Commits](https://www.conventionalcommits.org/) spec (e.g. `feat(cli): ...`, `fix(utils): ...`, `chore: ...`).
- **Pre-finish gate**: Before declaring a task finished, all of the following must pass:

```bash
pnpm format:fix
pnpm lint:fix
pnpm typecheck
pnpm test
```

## Key Patterns

Expand Down
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Modular, TypeScript-first system for on-chain blockchain analysis. Built on top

| Package | Description |
|---------|-------------|
| `@openscan/utils` | Zero-dep utilities: hex, units, address validation, ABI, events, signatures |
| `@openscan/utils` | Zero-dep utilities: hex, units, address validation, ABI, events, signatures, tx-analysis (call trees, prestate diffs, contract enrichment) |
| `@openscan/algorithms` | On-chain algorithms: tx history, token balance, gas price |
| `@openscan/cli` | CLI tool (`openscan`) wrapping algorithms and utils |
| `@openscan/skills` | Markdown-based procedural knowledge for AI agents (skills.sh format) |
Expand All @@ -20,6 +20,27 @@ pnpm install
pnpm build
```

## CLI Usage

After `pnpm build`, invoke the CLI via `node packages/cli/dist/bin.js <command>` (or link the `openscan` bin).

### `analyze-tx <txHash>`

High-level transaction debugger. Wraps `analyzeTx` from `@openscan/utils` to fetch the call tree, prestate diff, raw structLogs trace, and contract metadata in one pass. Requires an RPC that supports `debug_traceTransaction` (e.g., Alchemy) — falls back per-section with `{ data: null, error: "not supported" }` when a tracer isn't available.

```bash
# Defaults: call tree + contracts
openscan analyze-tx 0x<txHash> --chain 1 --alchemy-key "$ALCHEMY_API_KEY"

# Full: add prestate diff and raw trace, enrich contracts via Etherscan
openscan analyze-tx 0x<txHash> --chain 1 --alchemy-key "$ALCHEMY_API_KEY" \
--include-prestate --include-raw-trace --etherscan-key "$ETHERSCAN_API_KEY"
```

Flags: `--include-prestate`, `--include-raw-trace`, `--skip-call-tree`, `--skip-contracts`, `--etherscan-key` (or `ETHERSCAN_API_KEY` env var).

Result includes `verificationLinks` pointing at `https://openscan.eth.link/#/:chainId/tx/:txHash`.

## Testing

### Run all tests
Expand Down
23 changes: 8 additions & 15 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ All packages are published under the `@openscan` npm scope with public access.
1. A developer creates a changeset describing what changed
2. The changeset is committed and pushed as part of a PR to `main`
3. On merge, the GitHub Actions **Release** workflow runs
4. The [`changesets/action`](https://github.com/changesets/action) either:
- **Creates a "Version Packages" PR** — if there are pending changesets, it bumps versions, updates changelogs, and opens a PR titled `chore: version packages`
- **Publishes to npm** — if the "Version Packages" PR was just merged (no pending changesets, but versions were bumped), it publishes all updated packages with [npm provenance](https://docs.npmjs.com/generating-provenance-statements)
4. The workflow:
- Applies pending changesets (bumps versions, updates changelogs, removes consumed changeset files)
- Builds all packages
- Publishes updated packages to npm with [npm provenance](https://docs.npmjs.com/generating-provenance-statements)
- Commits and pushes the version changes back to `main`

### Dependency Order

Expand Down Expand Up @@ -60,18 +62,9 @@ This creates a markdown file in `.changeset/`. Commit it with your PR.

Push your branch and open a PR to `main`. Once merged, the Release workflow picks up the changeset.

### Step 3: Merge the Version PR
### Step 3: Automatic Version and Publish

The workflow creates a PR titled **"chore: version packages"** that:
- Consumes all pending changesets
- Bumps `version` in each `package.json`
- Updates `CHANGELOG.md` files

Review and merge this PR.

### Step 4: Automatic Publish

Once the version PR merges, the Release workflow runs again and publishes all updated packages to npm.
Once merged, the Release workflow automatically versions, builds, publishes to npm, and pushes the version changes back to `main`. No additional steps required.

## Pre-release (Alpha) Versions

Expand Down Expand Up @@ -129,7 +122,7 @@ pnpm --filter @openscan/utils exec npm publish --access public
The release pipeline is defined in `.github/workflows/release.yml` and requires:

- **`NPM_TOKEN`** — a granular access token from npmjs.com scoped to the `@openscan` org with read/write permissions. Added as a GitHub Actions secret.
- **`GITHUB_TOKEN`** — automatically provided by GitHub Actions. Used to create the version PR.
- **`GITHUB_TOKEN`** — automatically provided by GitHub Actions. Used to push version changes back to `main`.

### Creating an npm Token

Expand Down
2 changes: 2 additions & 0 deletions packages/adapters-langchain/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

### Patch Changes

- General fixes
- 4ec24b6: General release fixes
- Updated dependencies
- Updated dependencies [4ec24b6]
- @openscan/algorithms@0.0.1
- @openscan/utils@0.0.1
1 change: 1 addition & 0 deletions packages/adapters-langchain/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,5 +142,6 @@ The `scripts/` directory contains per-tool demo scripts that exercise each LangC
| `txHistory.ts` | `getTransactionHistory` | Recent transactions for an address |
| `addressType.ts` | `getAddressType` | Identify address type (EOA vs contract) |
| `tokenBalance.ts` | `getTokenBalanceHistory` | Token balance history for an address |
| `txAnalysis.ts` | `getTransactionAnalysis` | Analyze a transaction (call tree + contracts) |

When adding a new tool, add a corresponding demo script following the same pattern.
25 changes: 25 additions & 0 deletions packages/adapters-langchain/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ import {
getGasPriceHistory,
getAddressType,
getTokenBalanceHistory,
getTransactionAnalysis,
} from "@openscan/adapters-langchain";

const tools = [
getTransactionHistory,
getGasPriceHistory,
getAddressType,
getTokenBalanceHistory,
getTransactionAnalysis,
];

const llm = new ChatOpenAI({ model: "gpt-4o" });
Expand Down Expand Up @@ -64,6 +66,7 @@ console.log(result.output);
| `getGasPriceHistory` | `get_gas_price_history` | Get gas price history by sampling blocks exponentially |
| `getAddressType` | `detect_address_type` | Detect whether an address is an EOA, contract, or proxy |
| `getTokenBalanceHistory` | `get_token_balance_history` | Track ERC-20 token balance changes via Transfer event logs |
| `getTransactionAnalysis` | `get_transaction_analysis` | Analyze a single confirmed tx: call tree, contracts, optional state diff / raw trace |

## RPC Resolution

Expand Down Expand Up @@ -150,6 +153,22 @@ const result = await getTokenBalanceHistory.invoke({
console.log(result); // JSON string of balance changes over time
```

### Transaction Analysis

```typescript
import { getTransactionAnalysis } from "@openscan/adapters-langchain";

const result = await getTransactionAnalysis.invoke({
txHash: "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060",
chainId: 1,
alchemyKey: process.env.ALCHEMY_API_KEY, // recommended — trace RPC needed
});

console.log(result); // JSON string: call tree + addresses + contracts (+ optional prestate/raw trace)
```

Note: `analyzeTx` requires a trace-enabled RPC (`debug_traceTransaction`). Public RPCs frequently lack this; passing `alchemyKey` or explicit trace-enabled `rpcUrls` is strongly recommended.

### Using with Alchemy

Pass `alchemyKey` to any tool for premium RPC access:
Expand Down Expand Up @@ -191,6 +210,12 @@ const result = await getGasPriceHistory.invoke({
| `getAddressType` | `address` | `string` | Yes | Address to classify |
| `getTokenBalanceHistory` | `address` | `string` | Yes | Holder address to track |
| `getTokenBalanceHistory` | `tokenAddress` | `string` | Yes | ERC-20 token contract address |
| `getTransactionAnalysis` | `txHash` | `string` | Yes | 0x-prefixed transaction hash |
| `getTransactionAnalysis` | `includePrestate` | `boolean` | No | Include pre/post state storage diff (default: false) |
| `getTransactionAnalysis` | `includeRawTrace` | `boolean` | No | Include raw opcode-level trace (default: false) |
| `getTransactionAnalysis` | `skipCallTree` | `boolean` | No | Skip call tree fetch (default: false) |
| `getTransactionAnalysis` | `skipContracts` | `boolean` | No | Skip contract metadata enrichment (default: false) |
| `getTransactionAnalysis` | `etherscanKey` | `string` | No | Etherscan API key (defaults to ETHERSCAN_API_KEY env var) |

## Architecture

Expand Down
2 changes: 1 addition & 1 deletion packages/adapters-langchain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"url": "https://github.com/openscan-explorer/ia.git",
"directory": "packages/adapters-langchain"
},
"version": "0.0.1",
"version": "0.0.3",
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
Expand Down
43 changes: 43 additions & 0 deletions packages/adapters-langchain/scripts/txAnalysis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { createAgent } from "langchain";
import { ChatGroq } from "@langchain/groq";
import dotenvFlow from 'dotenv-flow';
import {
getTransactionHistory,
getGasPriceHistory,
getAddressType,
getTokenBalanceHistory,
getTransactionAnalysis,
} from "@openscan/adapters-langchain";

dotenvFlow.config();

const model = new ChatGroq({
model: "openai/gpt-oss-120b",
apiKey: process.env.API_KEY
});

const agent = createAgent({
model,
tools: [
getTransactionHistory,
getGasPriceHistory,
getAddressType,
getTokenBalanceHistory,
getTransactionAnalysis,
]
});


const r = await agent.invoke({
messages: [{ role: "user", content: "Analyze Ethereum transaction 0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060 — what did it do and which contracts did it touch?" }],
});

const toolsUsed = r.messages
.filter((m: any) => m.tool_calls?.length > 0)
.flatMap((m: any) => m.tool_calls.map((tc: any) => tc.name));
const response = r.messages.at(-1)?.content;

console.log("\n--- Tools used ---");
console.log(toolsUsed.join(", "));
console.log("\n--- Response ---");
console.log(response);
1 change: 1 addition & 0 deletions packages/adapters-langchain/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { getTransactionHistory } from "./tools/txHistory.js";
export { getGasPriceHistory } from "./tools/gasPrice.js";
export { getAddressType } from "./tools/addressType.js";
export { getTokenBalanceHistory } from "./tools/tokenBalance.js";
export { getTransactionAnalysis } from "./tools/txAnalysis.js";
6 changes: 5 additions & 1 deletion packages/adapters-langchain/src/tools/addressType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ export const getAddressType = tool(
name: "detect_address_type",
description: "Detect whether a blockchain address is an EOA, contract, or proxy",
schema: z.object({
address: z.string().describe("Blockchain address to check"),
address: z
.string()
.describe(
"42-char 0x-prefixed hex address to check (e.g. 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48)",
),
chainId: z.number().describe("EVM chain ID"),
rpcUrls: z
.array(z.string())
Expand Down
12 changes: 10 additions & 2 deletions packages/adapters-langchain/src/tools/tokenBalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,16 @@ export const getTokenBalanceHistory = tool(
name: "get_token_balance_history",
description: "Track ERC-20 token balance changes for an address via Transfer event logs",
schema: z.object({
address: z.string().describe("The holder address to track"),
tokenAddress: z.string().describe("The ERC-20 token contract address"),
address: z
.string()
.describe(
"42-char 0x-prefixed holder address to track (e.g. 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045)",
),
tokenAddress: z
.string()
.describe(
"42-char 0x-prefixed ERC-20 token contract address (e.g. 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 for USDC)",
),
chainId: z.number().describe("EVM chain ID"),
rpcUrls: z
.array(z.string())
Expand Down
103 changes: 103 additions & 0 deletions packages/adapters-langchain/src/tools/txAnalysis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { analyzeTx } from "@openscan/utils";
import { resolveRpcUrls } from "../rpc.js";
import { injectVerificationLinks } from "../verify.js";

export const getTransactionAnalysis = tool(
async ({
txHash,
chainId,
rpcUrls,
alchemyKey,
includePrestate,
includeRawTrace,
skipCallTree,
skipContracts,
etherscanKey,
}) => {
if (!txHash?.startsWith("0x")) {
return `Invalid txHash: must be a 0x-prefixed hex string`;
}

const resolvedRpcUrls = rpcUrls ?? resolveRpcUrls({ chainId, alchemyKey });
const analysis = await analyzeTx({
txHash,
chainId,
rpcUrls: resolvedRpcUrls,
include: {
callTree: !skipCallTree,
prestate: Boolean(includePrestate),
rawTrace: Boolean(includeRawTrace),
contracts: !skipContracts,
},
etherscanKey: etherscanKey ?? process.env.ETHERSCAN_API_KEY,
});

return JSON.stringify(injectVerificationLinks(analysis, { chainId, txHash }), null, 2);
},
{
name: "get_transaction_analysis",
description: [
"Analyze a single confirmed EVM transaction: returns its decoded call tree (nested internal calls with from/to/value/gasUsed/revertReason), a list of all addresses touched, per-address contract metadata (name + ABI from Sourcify/Etherscan), and optionally the prestate/poststate storage diff and raw opcode trace.",
"Use this when the user asks what a transaction did, why it reverted, which contracts it called, what state it changed, or to decode opaque calldata on a specific tx hash.",
"Do NOT use this to list a wallet's transactions (use get_transaction_history) or for pending/unconfirmed txs. Requires a trace-enabled RPC (debug_traceTransaction); public RPCs often lack this — pass alchemyKey or explicit rpcUrls for reliability.",
].join(" "),
schema: z.object({
txHash: z
.string()
.describe(
"The 66-char transaction hash, 0x-prefixed (e.g. 0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060). Must be a confirmed transaction.",
),
chainId: z
.number()
.describe(
"EVM chain ID (1=Ethereum mainnet, 137=Polygon, 8453=Base, 42161=Arbitrum, 10=Optimism)",
),
rpcUrls: z
.array(z.string())
.optional()
.describe(
"Trace-enabled RPC endpoints. Omit to auto-resolve from public RPCs (may lack debug_traceTransaction).",
),
alchemyKey: z
.string()
.optional()
.describe(
"Alchemy API key — strongly recommended since public RPCs often don't support debug_traceTransaction.",
),
includePrestate: z
.boolean()
.optional()
.default(false)
.describe(
"Include pre/post state storage diff. Expensive — only set true when the user asks about state changes.",
),
includeRawTrace: z
.boolean()
.optional()
.default(false)
.describe(
"Include raw opcode-level structLogs. Very large output — only set true when debugging opcode-level behavior.",
),
skipCallTree: z
.boolean()
.optional()
.default(false)
.describe("Skip the call tree. Leave false unless you only need contracts/addresses."),
skipContracts: z
.boolean()
.optional()
.default(false)
.describe(
"Skip contract metadata enrichment (names + ABIs). Leave false unless minimizing latency.",
),
etherscanKey: z
.string()
.optional()
.describe(
"Etherscan API key for contract verification fallback (defaults to ETHERSCAN_API_KEY env var).",
),
}),
},
);
Loading