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 +127 -27
- package/command/commit-push.md +1 -1
- package/command/diff-summary.md +19 -0
- package/command/doc-changes.md +1 -1
- package/command/review-changes.md +15 -1
- package/command/review-pr.md +17 -2
- package/command/simplify-changes.md +1 -1
- package/dist/index.js +1 -3
- package/dist/tools/blockchain/eth-transaction.d.ts +15 -1
- package/dist/tools/blockchain/eth-transaction.js +180 -17
- package/dist/tools/blockchain/etherscan-client.d.ts +3 -2
- package/dist/tools/blockchain/etherscan-client.js +23 -5
- package/dist/tools/blockchain/event-decoder.d.ts +14 -0
- package/dist/tools/blockchain/event-decoder.js +96 -0
- package/dist/tools/blockchain/event-decoder.test.d.ts +1 -0
- package/dist/tools/blockchain/event-decoder.test.js +197 -0
- package/dist/tools/blockchain/types.d.ts +64 -0
- package/dist/tools/blockchain/viem-client.d.ts +9 -0
- package/dist/tools/blockchain/viem-client.js +98 -0
- package/dist/tools/index.d.ts +0 -1
- package/dist/tools/index.js +0 -1
- package/package.json +3 -2
- package/skill/code-simplify/SKILL.md +6 -0
- package/dist/tools/diff-summary.d.ts +0 -20
- package/dist/tools/diff-summary.js +0 -111
- package/dist/tools/reply-child.d.ts +0 -19
- package/dist/tools/reply-child.js +0 -42
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
|
-
|
|
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
|
-
####
|
|
146
|
+
#### Usage
|
|
146
147
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
####
|
|
152
|
+
#### What it shows
|
|
154
153
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
160
|
-
diffSummary({ source: "feature-branch" })
|
|
159
|
+
#### Implementation
|
|
161
160
|
|
|
162
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
package/command/commit-push.md
CHANGED
|
@@ -9,7 +9,7 @@ agent: build
|
|
|
9
9
|
|
|
10
10
|
## Your task
|
|
11
11
|
|
|
12
|
-
1.
|
|
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'`
|
package/command/doc-changes.md
CHANGED
|
@@ -5,7 +5,7 @@ agent: doc-writer
|
|
|
5
5
|
|
|
6
6
|
## Analysis Phase
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
|
|
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.
|
package/command/review-pr.md
CHANGED
|
@@ -3,6 +3,21 @@ description: Review changes from source branch into target branch
|
|
|
3
3
|
agent: code-reviewer
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Review:
|
|
6
|
+
# Review: $1 → $2
|
|
7
7
|
|
|
8
|
-
|
|
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.
|
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,
|
|
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
|
|
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 {
|
|
7
|
-
import { CHAIN_ID_DESCRIPTION } from "./types";
|
|
8
|
-
|
|
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
|
|
11
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.
|
|
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(
|
|
185
|
+
.describe("Decode event logs (Transfer, Approval, Deposit, Withdrawal)"),
|
|
28
186
|
},
|
|
29
187
|
async execute(args, _context) {
|
|
30
188
|
try {
|
|
31
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
129
|
+
async getContractInfo(address) {
|
|
119
130
|
const result = await this.request({
|
|
120
|
-
module: "
|
|
121
|
-
action: "
|
|
122
|
-
|
|
131
|
+
module: "contract",
|
|
132
|
+
action: "getsourcecode",
|
|
133
|
+
address,
|
|
123
134
|
});
|
|
124
|
-
|
|
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;
|