opencode-froggy 0.3.0 → 0.4.0

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
@@ -22,6 +22,7 @@ Plugin providing Claude Code–style hooks, specialized agents (doc-writer, code
22
22
  - [prompt-session](#prompt-session)
23
23
  - [list-child-sessions](#list-child-sessions)
24
24
  - [Blockchain](#blockchain)
25
+ - [Configuration](#configuration)
25
26
  - [eth-transaction](#eth-transaction)
26
27
  - [eth-address-balance](#eth-address-balance)
27
28
  - [eth-address-txs](#eth-address-txs)
@@ -140,38 +141,26 @@ gitingest({
140
141
 
141
142
  ### diff-summary
142
143
 
143
- Generate a structured summary of git diffs. Use for reviewing branch comparisons or working tree changes. Returns stats, commits, files changed, and full diff in a structured markdown format.
144
+ **Command** that displays a structured summary of git working tree changes (staged, unstaged, and untracked files). Injects git diff output directly into the prompt using bash commands.
144
145
 
145
- #### Parameters
146
+ #### Usage
146
147
 
147
- | Parameter | Type | Required | Default | Description |
148
- |-----------|------|----------|---------|-------------|
149
- | `source` | `string` | No | - | Source branch to compare (e.g., `feature-branch`). If omitted, analyzes working tree changes. |
150
- | `target` | `string` | No | `main` | Target branch to compare against |
151
- | `remote` | `string` | No | `origin` | Git remote name |
148
+ ```bash
149
+ /diff-summary
150
+ ```
152
151
 
153
- #### Usage Examples
152
+ #### What it shows
154
153
 
155
- ```typescript
156
- // Analyze working tree changes (staged, unstaged, and untracked files)
157
- diffSummary({})
154
+ - Git status (porcelain format)
155
+ - Staged changes (stats and full diff)
156
+ - Unstaged changes (stats and full diff)
157
+ - Untracked files content (first 50 lines of each file)
158
158
 
159
- // Compare feature branch against main
160
- diffSummary({ source: "feature-branch" })
159
+ #### Implementation
161
160
 
162
- // Compare feature branch against develop
163
- diffSummary({
164
- source: "feature-branch",
165
- target: "develop"
166
- })
161
+ This command uses OpenCode's `!`\`...\`` syntax to inject bash command output directly into the prompt, avoiding the 2000-line truncation limit that affects tools.
167
162
 
168
- // Compare branches on a different remote
169
- diffSummary({
170
- source: "feature-branch",
171
- target: "main",
172
- remote: "upstream"
173
- })
174
- ```
163
+ See `command/diff-summary.md` for the full implementation.
175
164
 
176
165
  #### Output Structure
177
166
 
@@ -279,9 +268,35 @@ All blockchain tools support multiple chains via the `chainId` parameter:
279
268
  | `250` | Fantom |
280
269
  | `324` | zkSync |
281
270
 
271
+ #### Configuration
272
+
273
+ The blockchain tools use Etherscan-compatible APIs. An API key is optional but recommended.
274
+
275
+ **Environment Variable:**
276
+
277
+ | Variable | Required | Description |
278
+ |----------|----------|-------------|
279
+ | `ETHERSCAN_API_KEY` | No | API key for Etherscan and compatible explorers |
280
+
281
+ **Without an API key:** Requests are rate-limited (typically 1 request per 5 seconds).
282
+
283
+ **With an API key:** Higher rate limits and more reliable access.
284
+
285
+ **Getting an API key:**
286
+
287
+ 1. Create a free account at [etherscan.io](https://etherscan.io/register)
288
+ 2. Navigate to API Keys in your account settings
289
+ 3. Generate a new API key
290
+
291
+ **Setting the environment variable:**
292
+
293
+ ```bash
294
+ export ETHERSCAN_API_KEY="your-api-key-here"
295
+ ```
296
+
282
297
  #### eth-transaction
283
298
 
284
- Get Ethereum transaction details by transaction hash. Returns status, block, addresses, gas costs, and log count.
299
+ Get Ethereum transaction details by transaction hash. Returns status, block, addresses, gas costs in JSON format. Use optional parameters to include internal transactions, token transfers, and decoded event logs.
285
300
 
286
301
  ##### Parameters
287
302
 
@@ -289,11 +304,14 @@ Get Ethereum transaction details by transaction hash. Returns status, block, add
289
304
  |-----------|------|----------|---------|-------------|
290
305
  | `hash` | `string` | Yes | - | Transaction hash (0x...) |
291
306
  | `chainId` | `string` | No | `"1"` | Chain ID (see table above) |
307
+ | `includeInternalTxs` | `boolean` | No | `false` | Include internal transactions (ETH transfers between contracts) |
308
+ | `includeTokenTransfers` | `boolean` | No | `false` | Include ERC-20 token transfers |
309
+ | `decodeLogs` | `boolean` | No | `false` | Decode event logs (Transfer, Approval, Deposit, Withdrawal) |
292
310
 
293
311
  ##### Usage Examples
294
312
 
295
313
  ```typescript
296
- // Get transaction on Ethereum mainnet
314
+ // Get basic transaction details on Ethereum mainnet
297
315
  ethTransaction({ hash: "0x123abc..." })
298
316
 
299
317
  // Get transaction on Polygon
@@ -301,8 +319,90 @@ ethTransaction({
301
319
  hash: "0x123abc...",
302
320
  chainId: "137"
303
321
  })
322
+
323
+ // Get transaction with internal transactions and token transfers
324
+ ethTransaction({
325
+ hash: "0x123abc...",
326
+ includeInternalTxs: true,
327
+ includeTokenTransfers: true
328
+ })
329
+
330
+ // Get full transaction details with decoded event logs
331
+ ethTransaction({
332
+ hash: "0x123abc...",
333
+ includeInternalTxs: true,
334
+ includeTokenTransfers: true,
335
+ decodeLogs: true
336
+ })
337
+ ```
338
+
339
+ ##### Output Structure
340
+
341
+ The tool returns JSON with labeled addresses (contract names resolved via Etherscan):
342
+
343
+ ```json
344
+ {
345
+ "hash": "0x123...",
346
+ "status": "success",
347
+ "block": 12345678,
348
+ "from": { "address": "0xabc...", "label": "Uniswap V3: Router" },
349
+ "to": { "address": "0xdef...", "label": "WETH" },
350
+ "value": "0",
351
+ "gas": { "used": 150000, "price": "20000000000", "cost": "0.003" }
352
+ }
353
+ ```
354
+
355
+ With `includeInternalTxs: true`:
356
+ ```json
357
+ {
358
+ "internalTransactions": [
359
+ {
360
+ "from": { "address": "0x...", "label": "Uniswap V3: Router" },
361
+ "to": { "address": "0x...", "label": null },
362
+ "value": "1.5",
363
+ "type": "call"
364
+ }
365
+ ]
366
+ }
304
367
  ```
305
368
 
369
+ With `includeTokenTransfers: true`:
370
+ ```json
371
+ {
372
+ "tokenTransfers": [
373
+ {
374
+ "token": { "address": "0x...", "name": "Wrapped Ether", "symbol": "WETH", "decimals": 18 },
375
+ "from": { "address": "0x...", "label": null },
376
+ "to": { "address": "0x...", "label": "Uniswap V3: Router" },
377
+ "value": "1.5"
378
+ }
379
+ ]
380
+ }
381
+ ```
382
+
383
+ With `decodeLogs: true`:
384
+ ```json
385
+ {
386
+ "decodedEvents": [
387
+ {
388
+ "name": "Transfer",
389
+ "address": { "address": "0x...", "label": "WETH" },
390
+ "params": { "from": "0x...", "to": "0x...", "value": "1500000000000000000" }
391
+ }
392
+ ],
393
+ "undecodedEventsCount": 2
394
+ }
395
+ ```
396
+
397
+ ##### Supported Decoded Events
398
+
399
+ | Event | Description |
400
+ |-------|-------------|
401
+ | `Transfer` | ERC-20 token transfer |
402
+ | `Approval` | ERC-20 approval for spending |
403
+ | `Deposit` | WETH deposit (ETH → WETH) |
404
+ | `Withdrawal` | WETH withdrawal (WETH → ETH) |
405
+
306
406
  #### eth-address-balance
307
407
 
308
408
  Get the ETH balance of an Ethereum address. Returns balance in both ETH and Wei.
@@ -9,7 +9,7 @@ agent: build
9
9
 
10
10
  ## Your task
11
11
 
12
- 1. Use the `diff-summary` tool (without parameters) to analyze all working tree changes
12
+ 1. Run `/diff-summary` to analyze all working tree changes
13
13
  2. Present a summary to the user:
14
14
  - Files modified/added/deleted with stats
15
15
  - Proposed commit message based on the changes
@@ -0,0 +1,19 @@
1
+ ---
2
+ description: Show working tree changes (staged, unstaged, untracked)
3
+ ---
4
+
5
+ # Diff Summary: Working Tree → HEAD
6
+
7
+ ## Status
8
+ !`git status --porcelain`
9
+
10
+ ## Staged Changes
11
+ !`git diff --cached --stat`
12
+ !`git diff --cached`
13
+
14
+ ## Unstaged Changes
15
+ !`git diff --stat`
16
+ !`git diff`
17
+
18
+ ## Untracked Files Content
19
+ !`bash -c 'git ls-files --others --exclude-standard | while read f; do [ -f "$f" ] && echo "=== $f ===" && sed -n "1,50p" "$f" && sed -n "51p" "$f" | grep -q . && echo "... (truncated)"; done'`
@@ -5,7 +5,7 @@ agent: doc-writer
5
5
 
6
6
  ## Analysis Phase
7
7
 
8
- Use the `diff-summary` tool (without parameters) to get the working tree changes, then:
8
+ Run `/diff-summary` to get the working tree changes, then:
9
9
 
10
10
  1. **Identify new features** in the changes:
11
11
  - New public APIs, functions, or methods
@@ -5,4 +5,18 @@ agent: code-reviewer
5
5
 
6
6
  # Review: Working Tree → HEAD
7
7
 
8
- Use the `diff-summary` tool (without parameters) to get the working tree changes, then review them.
8
+ ## Status
9
+ !`git status --porcelain`
10
+
11
+ ## Staged Changes
12
+ !`git diff --cached --stat`
13
+ !`git diff --cached`
14
+
15
+ ## Unstaged Changes
16
+ !`git diff --stat`
17
+ !`git diff`
18
+
19
+ ## Untracked Files Content
20
+ !`bash -c 'git ls-files --others --exclude-standard | while read f; do [ -f "$f" ] && echo "=== $f ===" && sed -n "1,50p" "$f" && sed -n "51p" "$f" | grep -q . && echo "... (truncated)"; done'`
21
+
22
+ Review the above changes for quality, correctness, and adherence to project guidelines.
@@ -3,6 +3,21 @@ description: Review changes from source branch into target branch
3
3
  agent: code-reviewer
4
4
  ---
5
5
 
6
- # Review: origin/$1 → origin/$2
6
+ # Review: $1 → $2
7
7
 
8
- Use the `diff-summary` tool with `source: "$1"` and `target: "$2"` to get the diff, then review the changes.
8
+ ## Fetch latest
9
+ !`git fetch --all --prune 2>/dev/null || true`
10
+
11
+ ## Stats Overview
12
+ !`git diff --stat $2...$1`
13
+
14
+ ## Commits to Review
15
+ !`git log --oneline --no-merges $2..$1`
16
+
17
+ ## Files Changed
18
+ !`git diff --name-only $2...$1`
19
+
20
+ ## Full Diff
21
+ !`git diff -U5 --function-context $2...$1`
22
+
23
+ Review the above changes for quality, correctness, and adherence to project guidelines.
@@ -5,4 +5,4 @@ agent: code-simplifier
5
5
 
6
6
  # Simplify: Working Tree → HEAD
7
7
 
8
- Use the `diff-summary` tool (without parameters) to get the working tree changes, then simplify them.
8
+ Run `/diff-summary` to get the working tree changes, then simplify them.
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { getGlobalHookDir, getProjectHookDir } from "./config-paths";
5
5
  import { hasCodeExtension } from "./code-files";
6
6
  import { log } from "./logger";
7
7
  import { executeBashAction, DEFAULT_BASH_TIMEOUT, } from "./bash-executor";
8
- import { gitingestTool, createDiffSummaryTool, createPromptSessionTool, createListChildSessionsTool, ethTransactionTool, ethAddressTxsTool, ethAddressBalanceTool, ethTokenTransfersTool, } from "./tools";
8
+ import { gitingestTool, createPromptSessionTool, createListChildSessionsTool, ethTransactionTool, ethAddressTxsTool, ethAddressBalanceTool, ethTokenTransfersTool, } from "./tools";
9
9
  export { parseFrontmatter, loadAgents, loadCommands } from "./loaders";
10
10
  // ============================================================================
11
11
  // CONSTANTS
@@ -32,7 +32,6 @@ const SmartfrogPlugin = async (ctx) => {
32
32
  hooks: Array.from(hooks.keys()),
33
33
  tools: [
34
34
  "gitingest",
35
- "diff-summary",
36
35
  "eth-transaction",
37
36
  "eth-address-txs",
38
37
  "eth-address-balance",
@@ -183,7 +182,6 @@ const SmartfrogPlugin = async (ctx) => {
183
182
  },
184
183
  tool: {
185
184
  gitingest: gitingestTool,
186
- "diff-summary": createDiffSummaryTool(ctx.directory),
187
185
  "prompt-session": createPromptSessionTool(ctx.client),
188
186
  "list-child-sessions": createListChildSessionsTool(ctx.client),
189
187
  "eth-transaction": ethTransactionTool,
@@ -2,19 +2,33 @@
2
2
  * Tool to get Ethereum transaction details by hash
3
3
  */
4
4
  import { type ToolContext } from "@opencode-ai/plugin";
5
+ import { type TransactionDetails } from "./types";
5
6
  export interface EthTransactionArgs {
6
7
  hash: string;
7
8
  chainId?: string;
9
+ includeInternalTxs?: boolean;
10
+ includeTokenTransfers?: boolean;
11
+ decodeLogs?: boolean;
8
12
  }
9
- export declare function getTransactionDetails(hash: string, chainId?: string): Promise<string>;
13
+ export declare function getTransactionDetails(hash: string, chainId?: string, options?: {
14
+ includeInternalTxs?: boolean;
15
+ includeTokenTransfers?: boolean;
16
+ decodeLogs?: boolean;
17
+ }): Promise<TransactionDetails>;
10
18
  export declare const ethTransactionTool: {
11
19
  description: string;
12
20
  args: {
13
21
  hash: import("zod").ZodString;
14
22
  chainId: import("zod").ZodOptional<import("zod").ZodString>;
23
+ includeInternalTxs: import("zod").ZodOptional<import("zod").ZodBoolean>;
24
+ includeTokenTransfers: import("zod").ZodOptional<import("zod").ZodBoolean>;
25
+ decodeLogs: import("zod").ZodOptional<import("zod").ZodBoolean>;
15
26
  };
16
27
  execute(args: {
17
28
  hash: string;
18
29
  chainId?: string | undefined;
30
+ includeInternalTxs?: boolean | undefined;
31
+ includeTokenTransfers?: boolean | undefined;
32
+ decodeLogs?: boolean | undefined;
19
33
  }, context: ToolContext): Promise<string>;
20
34
  };
@@ -2,37 +2,200 @@
2
2
  * Tool to get Ethereum transaction details by hash
3
3
  */
4
4
  import { tool } from "@opencode-ai/plugin";
5
- import { EtherscanClient, EtherscanClientError, validateTxHash } from "./etherscan-client";
6
- import { formatTransactionReceipt } from "./formatters";
7
- import { CHAIN_ID_DESCRIPTION } from "./types";
8
- export async function getTransactionDetails(hash, chainId) {
5
+ import { EtherscanClient, EtherscanClientError, validateTxHash, weiToEth } from "./etherscan-client";
6
+ import { getTransactionReceipt, getBlock, getTokenMetadata } from "./viem-client";
7
+ import { CHAIN_ID_DESCRIPTION, DEFAULT_CHAIN_ID, } from "./types";
8
+ import { decodeEvents } from "./event-decoder";
9
+ const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
10
+ function decodeAddress(topic) {
11
+ if (!topic || topic.length < 66)
12
+ return "0x0000000000000000000000000000000000000000";
13
+ return "0x" + topic.slice(26).toLowerCase();
14
+ }
15
+ function decodeUint256(data) {
16
+ if (!data)
17
+ return "0";
18
+ const hex = data.startsWith("0x") ? data.slice(2) : data;
19
+ if (hex === "" || !/^[0-9a-fA-F]+$/.test(hex))
20
+ return "0";
21
+ return BigInt("0x" + hex).toString();
22
+ }
23
+ function extractTransfersFromLogs(logs) {
24
+ return logs
25
+ .filter((log) => log.topics[0] === TRANSFER_TOPIC && log.topics.length === 3)
26
+ .map((log) => ({
27
+ contractAddress: log.address.toLowerCase(),
28
+ from: decodeAddress(log.topics[1]),
29
+ to: decodeAddress(log.topics[2]),
30
+ value: decodeUint256(log.data),
31
+ }));
32
+ }
33
+ function mapViemLogsToTransactionLogs(logs) {
34
+ return logs.map((log) => ({
35
+ address: log.address,
36
+ topics: log.topics,
37
+ data: log.data,
38
+ blockNumber: log.blockNumber?.toString() ?? "",
39
+ transactionHash: log.transactionHash ?? "",
40
+ transactionIndex: log.transactionIndex?.toString() ?? "",
41
+ blockHash: log.blockHash ?? "",
42
+ logIndex: log.logIndex?.toString() ?? "",
43
+ removed: log.removed ?? false,
44
+ }));
45
+ }
46
+ function formatTokenValue(value, decimals) {
47
+ const valueBigInt = BigInt(value);
48
+ const divisor = BigInt(10) ** BigInt(decimals);
49
+ const wholePart = valueBigInt / divisor;
50
+ const fractionPart = valueBigInt % divisor;
51
+ const fractionStr = fractionPart.toString().padStart(decimals, "0");
52
+ const trimmedFraction = fractionStr.replace(/0+$/, "").slice(0, 8);
53
+ if (trimmedFraction === "") {
54
+ return wholePart.toString();
55
+ }
56
+ return `${wholePart}.${trimmedFraction}`;
57
+ }
58
+ class ContractLabelResolver {
59
+ addressCache = new Map();
60
+ tokenCache = new Map();
61
+ chainId;
62
+ constructor(chainId) {
63
+ this.chainId = chainId;
64
+ }
65
+ async resolve(address) {
66
+ const lowerAddress = address.toLowerCase();
67
+ const cached = this.addressCache.get(lowerAddress);
68
+ if (cached) {
69
+ return cached;
70
+ }
71
+ const result = { address, label: null };
72
+ this.addressCache.set(lowerAddress, result);
73
+ return result;
74
+ }
75
+ async resolveToken(contractAddress) {
76
+ const lowerAddress = contractAddress.toLowerCase();
77
+ const cached = this.tokenCache.get(lowerAddress);
78
+ if (cached) {
79
+ return cached;
80
+ }
81
+ const metadata = await getTokenMetadata(contractAddress, this.chainId);
82
+ this.tokenCache.set(lowerAddress, metadata);
83
+ return metadata;
84
+ }
85
+ }
86
+ export async function getTransactionDetails(hash, chainId, options = {}) {
9
87
  validateTxHash(hash);
10
- const client = new EtherscanClient(undefined, chainId);
11
- const receipt = await client.getTransactionReceipt(hash);
88
+ const resolvedChainId = chainId ?? DEFAULT_CHAIN_ID;
89
+ const resolver = new ContractLabelResolver(resolvedChainId);
90
+ const receipt = await getTransactionReceipt(hash, resolvedChainId);
12
91
  if (!receipt) {
13
- return `Transaction not found: ${hash}`;
92
+ throw new EtherscanClientError(`Transaction not found: ${hash}`);
93
+ }
94
+ const status = receipt.status === "success" ? "success" : "failed";
95
+ const gasUsed = Number(receipt.gasUsed);
96
+ const effectiveGasPrice = receipt.effectiveGasPrice?.toString() ?? "0";
97
+ const gasCostWei = (receipt.gasUsed * (receipt.effectiveGasPrice ?? 0n)).toString();
98
+ const blockNumber = Number(receipt.blockNumber);
99
+ let timestamp = null;
100
+ const block = await getBlock(receipt.blockNumber, resolvedChainId);
101
+ if (block?.timestamp) {
102
+ timestamp = new Date(Number(block.timestamp) * 1000).toISOString();
103
+ }
104
+ const fromAddress = await resolver.resolve(receipt.from);
105
+ const toAddress = receipt.to ? await resolver.resolve(receipt.to) : null;
106
+ const result = {
107
+ hash,
108
+ status,
109
+ block: blockNumber,
110
+ timestamp,
111
+ from: fromAddress,
112
+ to: toAddress,
113
+ value: "0",
114
+ gas: {
115
+ used: gasUsed,
116
+ price: effectiveGasPrice,
117
+ cost: weiToEth(gasCostWei),
118
+ },
119
+ };
120
+ if (options.includeInternalTxs) {
121
+ const client = new EtherscanClient(undefined, resolvedChainId);
122
+ const internalTxs = await client.getInternalTransactionsByHash(hash);
123
+ const internalDetails = [];
124
+ for (const tx of internalTxs) {
125
+ const from = await resolver.resolve(tx.from);
126
+ const to = await resolver.resolve(tx.to);
127
+ internalDetails.push({
128
+ from,
129
+ to,
130
+ value: weiToEth(tx.value),
131
+ type: tx.type || "call",
132
+ });
133
+ }
134
+ result.internalTransactions = internalDetails;
14
135
  }
15
- return formatTransactionReceipt(hash, receipt);
136
+ const mappedLogs = Array.isArray(receipt.logs)
137
+ ? mapViemLogsToTransactionLogs(receipt.logs)
138
+ : [];
139
+ if (options.includeTokenTransfers && mappedLogs.length > 0) {
140
+ const rawTransfers = extractTransfersFromLogs(mappedLogs);
141
+ const tokenDetails = [];
142
+ for (const transfer of rawTransfers) {
143
+ const from = await resolver.resolve(transfer.from);
144
+ const to = await resolver.resolve(transfer.to);
145
+ const tokenMetadata = await resolver.resolveToken(transfer.contractAddress);
146
+ tokenDetails.push({
147
+ token: {
148
+ address: transfer.contractAddress,
149
+ name: tokenMetadata.name,
150
+ symbol: tokenMetadata.symbol,
151
+ decimals: tokenMetadata.decimals,
152
+ },
153
+ from,
154
+ to,
155
+ value: formatTokenValue(transfer.value, tokenMetadata.decimals),
156
+ });
157
+ }
158
+ result.tokenTransfers = tokenDetails;
159
+ }
160
+ if (options.decodeLogs && mappedLogs.length > 0) {
161
+ const { decoded, undecodedCount } = await decodeEvents(mappedLogs, resolver);
162
+ result.decodedEvents = decoded;
163
+ result.undecodedEventsCount = undecodedCount;
164
+ }
165
+ return result;
16
166
  }
17
167
  export const ethTransactionTool = tool({
18
168
  description: "Get Ethereum transaction details by transaction hash. " +
19
- "Returns status, block, addresses, gas costs, and log count.",
169
+ "Returns status, block, addresses, gas costs in JSON format. " +
170
+ "Use optional parameters to include internal transactions, token transfers, and decoded event logs.",
20
171
  args: {
21
- hash: tool.schema
22
- .string()
23
- .describe("Transaction hash (0x...)"),
24
- chainId: tool.schema
25
- .string()
172
+ hash: tool.schema.string().describe("Transaction hash (0x...)"),
173
+ chainId: tool.schema.string().optional().describe(CHAIN_ID_DESCRIPTION),
174
+ includeInternalTxs: tool.schema
175
+ .boolean()
176
+ .optional()
177
+ .describe("Include internal transactions (ETH transfers between contracts)"),
178
+ includeTokenTransfers: tool.schema
179
+ .boolean()
180
+ .optional()
181
+ .describe("Include ERC-20 token transfers"),
182
+ decodeLogs: tool.schema
183
+ .boolean()
26
184
  .optional()
27
- .describe(CHAIN_ID_DESCRIPTION),
185
+ .describe("Decode event logs (Transfer, Approval, Deposit, Withdrawal)"),
28
186
  },
29
187
  async execute(args, _context) {
30
188
  try {
31
- return await getTransactionDetails(args.hash, args.chainId);
189
+ const result = await getTransactionDetails(args.hash, args.chainId, {
190
+ includeInternalTxs: args.includeInternalTxs,
191
+ includeTokenTransfers: args.includeTokenTransfers,
192
+ decodeLogs: args.decodeLogs,
193
+ });
194
+ return JSON.stringify(result, null, 2);
32
195
  }
33
196
  catch (error) {
34
197
  if (error instanceof EtherscanClientError) {
35
- return `Error: ${error.message}`;
198
+ return JSON.stringify({ error: error.message });
36
199
  }
37
200
  throw error;
38
201
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Etherscan API client for Ethereum blockchain queries
3
3
  */
4
- import { type EthTransaction, type EthTokenTransfer, type EthInternalTransaction } from "./types";
4
+ import { type EthTransaction, type EthTokenTransfer, type EthInternalTransaction, type ContractInfo } from "./types";
5
5
  export declare class EtherscanClientError extends Error {
6
6
  constructor(message: string);
7
7
  }
@@ -14,9 +14,10 @@ export declare class EtherscanClient {
14
14
  getBalance(address: string): Promise<string>;
15
15
  getTransactions(address: string, limit?: number): Promise<EthTransaction[]>;
16
16
  getInternalTransactions(address: string, limit?: number): Promise<EthInternalTransaction[]>;
17
+ getInternalTransactionsByHash(txhash: string): Promise<EthInternalTransaction[]>;
17
18
  getTokenTransfers(address: string, limit?: number): Promise<EthTokenTransfer[]>;
18
19
  getTransactionByHash(hash: string): Promise<EthTransaction | null>;
19
- getTransactionReceipt(hash: string): Promise<Record<string, unknown> | null>;
20
+ getContractInfo(address: string): Promise<ContractInfo | null>;
20
21
  }
21
22
  export declare function weiToEth(wei: string): string;
22
23
  export declare function formatTimestamp(timestamp: string): string;
@@ -80,6 +80,17 @@ export class EtherscanClient {
80
80
  }
81
81
  return result;
82
82
  }
83
+ async getInternalTransactionsByHash(txhash) {
84
+ const result = await this.request({
85
+ module: "account",
86
+ action: "txlistinternal",
87
+ txhash,
88
+ });
89
+ if (typeof result === "string") {
90
+ return [];
91
+ }
92
+ return result;
93
+ }
83
94
  async getTokenTransfers(address, limit = DEFAULT_TRANSACTION_LIMIT) {
84
95
  const result = await this.request({
85
96
  module: "account",
@@ -115,13 +126,20 @@ export class EtherscanClient {
115
126
  }
116
127
  return result[0];
117
128
  }
118
- async getTransactionReceipt(hash) {
129
+ async getContractInfo(address) {
119
130
  const result = await this.request({
120
- module: "proxy",
121
- action: "eth_getTransactionReceipt",
122
- txhash: hash,
131
+ module: "contract",
132
+ action: "getsourcecode",
133
+ address,
123
134
  });
124
- return result;
135
+ if (typeof result === "string" || result.length === 0) {
136
+ return null;
137
+ }
138
+ const info = result[0];
139
+ if (!info.ContractName || info.ContractName === "") {
140
+ return null;
141
+ }
142
+ return info;
125
143
  }
126
144
  }
127
145
  export function weiToEth(wei) {
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Event decoder for common ERC-20 and WETH events
3
+ */
4
+ import { type TransactionLog, type DecodedEvent, type LabeledAddress } from "./types";
5
+ export interface AddressResolver {
6
+ resolve(address: string): Promise<LabeledAddress>;
7
+ }
8
+ export declare function decodeEvent(log: TransactionLog, resolver: AddressResolver): Promise<DecodedEvent | null>;
9
+ export declare function decodeEvents(logs: TransactionLog[], resolver: AddressResolver): Promise<{
10
+ decoded: DecodedEvent[];
11
+ undecodedCount: number;
12
+ }>;
13
+ export declare function isKnownEvent(topic0: string): boolean;
14
+ export declare function getEventName(topic0: string): string | null;