opencode-froggy 0.2.0 → 0.3.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
@@ -21,6 +21,11 @@ Plugin providing Claude Code–style hooks, specialized agents (doc-writer, code
21
21
  - [diff-summary](#diff-summary)
22
22
  - [prompt-session](#prompt-session)
23
23
  - [list-child-sessions](#list-child-sessions)
24
+ - [Blockchain](#blockchain)
25
+ - [eth-transaction](#eth-transaction)
26
+ - [eth-address-balance](#eth-address-balance)
27
+ - [eth-address-txs](#eth-address-txs)
28
+ - [eth-token-transfers](#eth-token-transfers)
24
29
  - [Hooks](#hooks)
25
30
  - [Configuration Locations](#configuration-locations)
26
31
  - [Configuration File Format](#configuration-file-format)
@@ -256,6 +261,126 @@ Child sessions (2):
256
261
 
257
262
  ---
258
263
 
264
+ ### Blockchain
265
+
266
+ Tools for querying Ethereum and EVM-compatible blockchains via Etherscan APIs.
267
+
268
+ All blockchain tools support multiple chains via the `chainId` parameter:
269
+
270
+ | Chain ID | Network |
271
+ |----------|---------|
272
+ | `1` | Ethereum (default) |
273
+ | `137` | Polygon |
274
+ | `56` | BSC |
275
+ | `42161` | Arbitrum |
276
+ | `10` | Optimism |
277
+ | `8453` | Base |
278
+ | `43114` | Avalanche |
279
+ | `250` | Fantom |
280
+ | `324` | zkSync |
281
+
282
+ #### eth-transaction
283
+
284
+ Get Ethereum transaction details by transaction hash. Returns status, block, addresses, gas costs, and log count.
285
+
286
+ ##### Parameters
287
+
288
+ | Parameter | Type | Required | Default | Description |
289
+ |-----------|------|----------|---------|-------------|
290
+ | `hash` | `string` | Yes | - | Transaction hash (0x...) |
291
+ | `chainId` | `string` | No | `"1"` | Chain ID (see table above) |
292
+
293
+ ##### Usage Examples
294
+
295
+ ```typescript
296
+ // Get transaction on Ethereum mainnet
297
+ ethTransaction({ hash: "0x123abc..." })
298
+
299
+ // Get transaction on Polygon
300
+ ethTransaction({
301
+ hash: "0x123abc...",
302
+ chainId: "137"
303
+ })
304
+ ```
305
+
306
+ #### eth-address-balance
307
+
308
+ Get the ETH balance of an Ethereum address. Returns balance in both ETH and Wei.
309
+
310
+ ##### Parameters
311
+
312
+ | Parameter | Type | Required | Default | Description |
313
+ |-----------|------|----------|---------|-------------|
314
+ | `address` | `string` | Yes | - | Ethereum address (0x...) |
315
+ | `chainId` | `string` | No | `"1"` | Chain ID (see table above) |
316
+
317
+ ##### Usage Examples
318
+
319
+ ```typescript
320
+ // Get balance on Ethereum mainnet
321
+ ethAddressBalance({ address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" })
322
+
323
+ // Get balance on Arbitrum
324
+ ethAddressBalance({
325
+ address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
326
+ chainId: "42161"
327
+ })
328
+ ```
329
+
330
+ #### eth-address-txs
331
+
332
+ List Ethereum transactions for an address. Shows incoming and outgoing transactions with values, timestamps, and status.
333
+
334
+ ##### Parameters
335
+
336
+ | Parameter | Type | Required | Default | Description |
337
+ |-----------|------|----------|---------|-------------|
338
+ | `address` | `string` | Yes | - | Ethereum address (0x...) |
339
+ | `limit` | `number` | No | `20` | Maximum number of transactions to return |
340
+ | `chainId` | `string` | No | `"1"` | Chain ID (see table above) |
341
+
342
+ ##### Usage Examples
343
+
344
+ ```typescript
345
+ // List recent transactions on Ethereum mainnet
346
+ ethAddressTxs({ address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" })
347
+
348
+ // List last 50 transactions on Base
349
+ ethAddressTxs({
350
+ address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
351
+ limit: 50,
352
+ chainId: "8453"
353
+ })
354
+ ```
355
+
356
+ #### eth-token-transfers
357
+
358
+ List ERC-20 token transfers for an Ethereum address. Shows token names, symbols, values, and transaction details.
359
+
360
+ ##### Parameters
361
+
362
+ | Parameter | Type | Required | Default | Description |
363
+ |-----------|------|----------|---------|-------------|
364
+ | `address` | `string` | Yes | - | Ethereum address (0x...) |
365
+ | `limit` | `number` | No | `20` | Maximum number of transfers to return |
366
+ | `chainId` | `string` | No | `"1"` | Chain ID (see table above) |
367
+
368
+ ##### Usage Examples
369
+
370
+ ```typescript
371
+ // List recent token transfers on Ethereum mainnet
372
+ ethTokenTransfers({ address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" })
373
+
374
+ // List last 100 token transfers on Optimism
375
+ ethTokenTransfers({
376
+ address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
377
+ limit: 100,
378
+ chainId: "10"
379
+ })
380
+ ```
381
+
382
+ ---
383
+
259
384
  ## Hooks
260
385
 
261
386
  Hooks run actions on session events. Configuration is loaded from standard OpenCode configuration directories.
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, } from "./tools";
8
+ import { gitingestTool, createDiffSummaryTool, createPromptSessionTool, createListChildSessionsTool, ethTransactionTool, ethAddressTxsTool, ethAddressBalanceTool, ethTokenTransfersTool, } from "./tools";
9
9
  export { parseFrontmatter, loadAgents, loadCommands } from "./loaders";
10
10
  // ============================================================================
11
11
  // CONSTANTS
@@ -30,7 +30,14 @@ const SmartfrogPlugin = async (ctx) => {
30
30
  agents: Object.keys(agents),
31
31
  commands: Object.keys(commands),
32
32
  hooks: Array.from(hooks.keys()),
33
- tools: ["gitingest", "diff-summary"],
33
+ tools: [
34
+ "gitingest",
35
+ "diff-summary",
36
+ "eth-transaction",
37
+ "eth-address-txs",
38
+ "eth-address-balance",
39
+ "eth-token-transfers",
40
+ ],
34
41
  });
35
42
  async function executeHookActions(hook, sessionID, extraLog, options) {
36
43
  const prefix = `[hook:${hook.event}]`;
@@ -179,6 +186,10 @@ const SmartfrogPlugin = async (ctx) => {
179
186
  "diff-summary": createDiffSummaryTool(ctx.directory),
180
187
  "prompt-session": createPromptSessionTool(ctx.client),
181
188
  "list-child-sessions": createListChildSessionsTool(ctx.client),
189
+ "eth-transaction": ethTransactionTool,
190
+ "eth-address-txs": ethAddressTxsTool,
191
+ "eth-address-balance": ethAddressBalanceTool,
192
+ "eth-token-transfers": ethTokenTransfersTool,
182
193
  },
183
194
  "tool.execute.before": async (input, output) => {
184
195
  const sessionID = input.sessionID;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Tool to get Ethereum address balance
3
+ */
4
+ import { type ToolContext } from "@opencode-ai/plugin";
5
+ export interface EthAddressBalanceArgs {
6
+ address: string;
7
+ chainId?: string;
8
+ }
9
+ export declare function getAddressBalance(address: string, chainId?: string): Promise<string>;
10
+ export declare const ethAddressBalanceTool: {
11
+ description: string;
12
+ args: {
13
+ address: import("zod").ZodString;
14
+ chainId: import("zod").ZodOptional<import("zod").ZodString>;
15
+ };
16
+ execute(args: {
17
+ address: string;
18
+ chainId?: string | undefined;
19
+ }, context: ToolContext): Promise<string>;
20
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Tool to get Ethereum address balance
3
+ */
4
+ import { tool } from "@opencode-ai/plugin";
5
+ import { EtherscanClient, EtherscanClientError, validateAddress } from "./etherscan-client";
6
+ import { formatBalance } from "./formatters";
7
+ import { CHAIN_ID_DESCRIPTION } from "./types";
8
+ export async function getAddressBalance(address, chainId) {
9
+ validateAddress(address);
10
+ const client = new EtherscanClient(undefined, chainId);
11
+ const balanceWei = await client.getBalance(address);
12
+ return formatBalance(address, balanceWei);
13
+ }
14
+ export const ethAddressBalanceTool = tool({
15
+ description: "Get the ETH balance of an Ethereum address. " +
16
+ "Returns balance in both ETH and Wei.",
17
+ args: {
18
+ address: tool.schema
19
+ .string()
20
+ .describe("Ethereum address (0x...)"),
21
+ chainId: tool.schema
22
+ .string()
23
+ .optional()
24
+ .describe(CHAIN_ID_DESCRIPTION),
25
+ },
26
+ async execute(args, _context) {
27
+ try {
28
+ return await getAddressBalance(args.address, args.chainId);
29
+ }
30
+ catch (error) {
31
+ if (error instanceof EtherscanClientError) {
32
+ return `Error: ${error.message}`;
33
+ }
34
+ throw error;
35
+ }
36
+ },
37
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Tool to list Ethereum transactions for an address
3
+ */
4
+ import { type ToolContext } from "@opencode-ai/plugin";
5
+ export interface EthAddressTxsArgs {
6
+ address: string;
7
+ limit?: number;
8
+ chainId?: string;
9
+ }
10
+ export declare function getAddressTransactions(address: string, limit?: number, chainId?: string): Promise<string>;
11
+ export declare const ethAddressTxsTool: {
12
+ description: string;
13
+ args: {
14
+ address: import("zod").ZodString;
15
+ limit: import("zod").ZodOptional<import("zod").ZodNumber>;
16
+ chainId: import("zod").ZodOptional<import("zod").ZodString>;
17
+ };
18
+ execute(args: {
19
+ address: string;
20
+ limit?: number | undefined;
21
+ chainId?: string | undefined;
22
+ }, context: ToolContext): Promise<string>;
23
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Tool to list Ethereum transactions for an address
3
+ */
4
+ import { tool } from "@opencode-ai/plugin";
5
+ import { EtherscanClient, EtherscanClientError, validateAddress } from "./etherscan-client";
6
+ import { formatTransactionList } from "./formatters";
7
+ import { DEFAULT_TRANSACTION_LIMIT, CHAIN_ID_DESCRIPTION } from "./types";
8
+ export async function getAddressTransactions(address, limit = DEFAULT_TRANSACTION_LIMIT, chainId) {
9
+ validateAddress(address);
10
+ const client = new EtherscanClient(undefined, chainId);
11
+ const transactions = await client.getTransactions(address, limit);
12
+ return formatTransactionList(address, transactions);
13
+ }
14
+ export const ethAddressTxsTool = tool({
15
+ description: "List Ethereum transactions for an address. " +
16
+ "Shows incoming and outgoing transactions with values, timestamps, and status.",
17
+ args: {
18
+ address: tool.schema
19
+ .string()
20
+ .describe("Ethereum address (0x...)"),
21
+ limit: tool.schema
22
+ .number()
23
+ .optional()
24
+ .describe(`Maximum number of transactions to return (default: ${DEFAULT_TRANSACTION_LIMIT})`),
25
+ chainId: tool.schema
26
+ .string()
27
+ .optional()
28
+ .describe(CHAIN_ID_DESCRIPTION),
29
+ },
30
+ async execute(args, _context) {
31
+ try {
32
+ return await getAddressTransactions(args.address, args.limit, args.chainId);
33
+ }
34
+ catch (error) {
35
+ if (error instanceof EtherscanClientError) {
36
+ return `Error: ${error.message}`;
37
+ }
38
+ throw error;
39
+ }
40
+ },
41
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Tool to list ERC-20 token transfers for an address
3
+ */
4
+ import { type ToolContext } from "@opencode-ai/plugin";
5
+ export interface EthTokenTransfersArgs {
6
+ address: string;
7
+ limit?: number;
8
+ chainId?: string;
9
+ }
10
+ export declare function getTokenTransfers(address: string, limit?: number, chainId?: string): Promise<string>;
11
+ export declare const ethTokenTransfersTool: {
12
+ description: string;
13
+ args: {
14
+ address: import("zod").ZodString;
15
+ limit: import("zod").ZodOptional<import("zod").ZodNumber>;
16
+ chainId: import("zod").ZodOptional<import("zod").ZodString>;
17
+ };
18
+ execute(args: {
19
+ address: string;
20
+ limit?: number | undefined;
21
+ chainId?: string | undefined;
22
+ }, context: ToolContext): Promise<string>;
23
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Tool to list ERC-20 token transfers for an address
3
+ */
4
+ import { tool } from "@opencode-ai/plugin";
5
+ import { EtherscanClient, EtherscanClientError, validateAddress } from "./etherscan-client";
6
+ import { formatTokenTransferList } from "./formatters";
7
+ import { DEFAULT_TRANSACTION_LIMIT, CHAIN_ID_DESCRIPTION } from "./types";
8
+ export async function getTokenTransfers(address, limit = DEFAULT_TRANSACTION_LIMIT, chainId) {
9
+ validateAddress(address);
10
+ const client = new EtherscanClient(undefined, chainId);
11
+ const transfers = await client.getTokenTransfers(address, limit);
12
+ return formatTokenTransferList(address, transfers);
13
+ }
14
+ export const ethTokenTransfersTool = tool({
15
+ description: "List ERC-20 token transfers for an Ethereum address. " +
16
+ "Shows token names, symbols, values, and transaction details.",
17
+ args: {
18
+ address: tool.schema
19
+ .string()
20
+ .describe("Ethereum address (0x...)"),
21
+ limit: tool.schema
22
+ .number()
23
+ .optional()
24
+ .describe(`Maximum number of transfers to return (default: ${DEFAULT_TRANSACTION_LIMIT})`),
25
+ chainId: tool.schema
26
+ .string()
27
+ .optional()
28
+ .describe(CHAIN_ID_DESCRIPTION),
29
+ },
30
+ async execute(args, _context) {
31
+ try {
32
+ return await getTokenTransfers(args.address, args.limit, args.chainId);
33
+ }
34
+ catch (error) {
35
+ if (error instanceof EtherscanClientError) {
36
+ return `Error: ${error.message}`;
37
+ }
38
+ throw error;
39
+ }
40
+ },
41
+ });
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Tool to get Ethereum transaction details by hash
3
+ */
4
+ import { type ToolContext } from "@opencode-ai/plugin";
5
+ export interface EthTransactionArgs {
6
+ hash: string;
7
+ chainId?: string;
8
+ }
9
+ export declare function getTransactionDetails(hash: string, chainId?: string): Promise<string>;
10
+ export declare const ethTransactionTool: {
11
+ description: string;
12
+ args: {
13
+ hash: import("zod").ZodString;
14
+ chainId: import("zod").ZodOptional<import("zod").ZodString>;
15
+ };
16
+ execute(args: {
17
+ hash: string;
18
+ chainId?: string | undefined;
19
+ }, context: ToolContext): Promise<string>;
20
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Tool to get Ethereum transaction details by hash
3
+ */
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) {
9
+ validateTxHash(hash);
10
+ const client = new EtherscanClient(undefined, chainId);
11
+ const receipt = await client.getTransactionReceipt(hash);
12
+ if (!receipt) {
13
+ return `Transaction not found: ${hash}`;
14
+ }
15
+ return formatTransactionReceipt(hash, receipt);
16
+ }
17
+ export const ethTransactionTool = tool({
18
+ description: "Get Ethereum transaction details by transaction hash. " +
19
+ "Returns status, block, addresses, gas costs, and log count.",
20
+ args: {
21
+ hash: tool.schema
22
+ .string()
23
+ .describe("Transaction hash (0x...)"),
24
+ chainId: tool.schema
25
+ .string()
26
+ .optional()
27
+ .describe(CHAIN_ID_DESCRIPTION),
28
+ },
29
+ async execute(args, _context) {
30
+ try {
31
+ return await getTransactionDetails(args.hash, args.chainId);
32
+ }
33
+ catch (error) {
34
+ if (error instanceof EtherscanClientError) {
35
+ return `Error: ${error.message}`;
36
+ }
37
+ throw error;
38
+ }
39
+ },
40
+ });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Etherscan API client for Ethereum blockchain queries
3
+ */
4
+ import { type EthTransaction, type EthTokenTransfer, type EthInternalTransaction } from "./types";
5
+ export declare class EtherscanClientError extends Error {
6
+ constructor(message: string);
7
+ }
8
+ export declare class EtherscanClient {
9
+ private readonly apiKey;
10
+ private readonly chainId;
11
+ private readonly baseUrl;
12
+ constructor(apiKey?: string, chainId?: string, baseUrl?: string);
13
+ private request;
14
+ getBalance(address: string): Promise<string>;
15
+ getTransactions(address: string, limit?: number): Promise<EthTransaction[]>;
16
+ getInternalTransactions(address: string, limit?: number): Promise<EthInternalTransaction[]>;
17
+ getTokenTransfers(address: string, limit?: number): Promise<EthTokenTransfer[]>;
18
+ getTransactionByHash(hash: string): Promise<EthTransaction | null>;
19
+ getTransactionReceipt(hash: string): Promise<Record<string, unknown> | null>;
20
+ }
21
+ export declare function weiToEth(wei: string): string;
22
+ export declare function formatTimestamp(timestamp: string): string;
23
+ export declare function shortenAddress(address: string): string;
24
+ export declare function validateAddress(address: string): void;
25
+ export declare function validateTxHash(hash: string): void;
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Etherscan API client for Ethereum blockchain queries
3
+ */
4
+ import { DEFAULT_TRANSACTION_LIMIT, DEFAULT_CHAIN_ID, } from "./types";
5
+ const ETHERSCAN_BASE_URL = "https://api.etherscan.io/v2/api";
6
+ export class EtherscanClientError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = "EtherscanClientError";
10
+ }
11
+ }
12
+ export class EtherscanClient {
13
+ apiKey;
14
+ chainId;
15
+ baseUrl;
16
+ constructor(apiKey, chainId, baseUrl) {
17
+ const key = apiKey ?? process.env.ETHERSCAN_API_KEY;
18
+ if (!key) {
19
+ throw new EtherscanClientError("ETHERSCAN_API_KEY environment variable is required. " +
20
+ "Get a free API key at https://etherscan.io/apis");
21
+ }
22
+ this.apiKey = key;
23
+ this.chainId = chainId ?? DEFAULT_CHAIN_ID;
24
+ this.baseUrl = baseUrl ?? ETHERSCAN_BASE_URL;
25
+ }
26
+ async request(params) {
27
+ const url = new URL(this.baseUrl);
28
+ url.searchParams.set("apikey", this.apiKey);
29
+ url.searchParams.set("chainid", this.chainId);
30
+ for (const [key, value] of Object.entries(params)) {
31
+ url.searchParams.set(key, value);
32
+ }
33
+ const response = await fetch(url.toString());
34
+ if (!response.ok) {
35
+ throw new EtherscanClientError(`Etherscan API HTTP error: ${response.status} ${response.statusText}`);
36
+ }
37
+ const data = await response.json();
38
+ if (data.status === "0" && data.message !== "No transactions found") {
39
+ throw new EtherscanClientError(`Etherscan API error: ${data.message} - ${data.result}`);
40
+ }
41
+ return data.result;
42
+ }
43
+ async getBalance(address) {
44
+ return this.request({
45
+ module: "account",
46
+ action: "balance",
47
+ address,
48
+ tag: "latest",
49
+ });
50
+ }
51
+ async getTransactions(address, limit = DEFAULT_TRANSACTION_LIMIT) {
52
+ const result = await this.request({
53
+ module: "account",
54
+ action: "txlist",
55
+ address,
56
+ startblock: "0",
57
+ endblock: "99999999",
58
+ page: "1",
59
+ offset: String(limit),
60
+ sort: "desc",
61
+ });
62
+ if (typeof result === "string") {
63
+ return [];
64
+ }
65
+ return result;
66
+ }
67
+ async getInternalTransactions(address, limit = DEFAULT_TRANSACTION_LIMIT) {
68
+ const result = await this.request({
69
+ module: "account",
70
+ action: "txlistinternal",
71
+ address,
72
+ startblock: "0",
73
+ endblock: "99999999",
74
+ page: "1",
75
+ offset: String(limit),
76
+ sort: "desc",
77
+ });
78
+ if (typeof result === "string") {
79
+ return [];
80
+ }
81
+ return result;
82
+ }
83
+ async getTokenTransfers(address, limit = DEFAULT_TRANSACTION_LIMIT) {
84
+ const result = await this.request({
85
+ module: "account",
86
+ action: "tokentx",
87
+ address,
88
+ startblock: "0",
89
+ endblock: "99999999",
90
+ page: "1",
91
+ offset: String(limit),
92
+ sort: "desc",
93
+ });
94
+ if (typeof result === "string") {
95
+ return [];
96
+ }
97
+ return result;
98
+ }
99
+ async getTransactionByHash(hash) {
100
+ const result = await this.request({
101
+ module: "account",
102
+ action: "txlistinternal",
103
+ txhash: hash,
104
+ });
105
+ if (typeof result === "string" || result.length === 0) {
106
+ const txList = await this.request({
107
+ module: "proxy",
108
+ action: "eth_getTransactionByHash",
109
+ txhash: hash,
110
+ });
111
+ if (!txList || typeof txList === "string") {
112
+ return null;
113
+ }
114
+ return txList;
115
+ }
116
+ return result[0];
117
+ }
118
+ async getTransactionReceipt(hash) {
119
+ const result = await this.request({
120
+ module: "proxy",
121
+ action: "eth_getTransactionReceipt",
122
+ txhash: hash,
123
+ });
124
+ return result;
125
+ }
126
+ }
127
+ export function weiToEth(wei) {
128
+ const weiBigInt = BigInt(wei);
129
+ const ethWhole = weiBigInt / BigInt(10 ** 18);
130
+ const ethFraction = weiBigInt % BigInt(10 ** 18);
131
+ const fractionStr = ethFraction.toString().padStart(18, "0");
132
+ const trimmedFraction = fractionStr.replace(/0+$/, "");
133
+ if (trimmedFraction === "") {
134
+ return ethWhole.toString();
135
+ }
136
+ return `${ethWhole}.${trimmedFraction}`;
137
+ }
138
+ export function formatTimestamp(timestamp) {
139
+ const date = new Date(parseInt(timestamp, 10) * 1000);
140
+ return date.toISOString();
141
+ }
142
+ export function shortenAddress(address) {
143
+ if (address.length < 12)
144
+ return address;
145
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
146
+ }
147
+ export function validateAddress(address) {
148
+ if (!address?.startsWith("0x") || address.length !== 42) {
149
+ throw new EtherscanClientError("Invalid Ethereum address format");
150
+ }
151
+ }
152
+ export function validateTxHash(hash) {
153
+ if (!hash?.startsWith("0x") || hash.length !== 66) {
154
+ throw new EtherscanClientError("Invalid transaction hash format");
155
+ }
156
+ }
@@ -0,0 +1 @@
1
+ export {};