opencode-froggy 0.1.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +508 -246
  3. package/agent/architect.md +91 -0
  4. package/agent/partner.md +143 -0
  5. package/agent/rubber-duck.md +129 -0
  6. package/command/commit-push.md +21 -0
  7. package/command/doc-changes.md +45 -0
  8. package/command/review-changes.md +1 -21
  9. package/command/review-pr.md +1 -22
  10. package/command/send-to.md +21 -0
  11. package/command/simplify-changes.md +2 -20
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +27 -52
  14. package/dist/index.test.js +29 -8
  15. package/dist/loaders.d.ts +9 -5
  16. package/dist/loaders.js +5 -1
  17. package/dist/tools/blockchain/eth-address-balance.d.ts +20 -0
  18. package/dist/tools/blockchain/eth-address-balance.js +37 -0
  19. package/dist/tools/blockchain/eth-address-txs.d.ts +23 -0
  20. package/dist/tools/blockchain/eth-address-txs.js +41 -0
  21. package/dist/tools/blockchain/eth-token-transfers.d.ts +23 -0
  22. package/dist/tools/blockchain/eth-token-transfers.js +41 -0
  23. package/dist/tools/blockchain/eth-transaction.d.ts +20 -0
  24. package/dist/tools/blockchain/eth-transaction.js +40 -0
  25. package/dist/tools/blockchain/etherscan-client.d.ts +25 -0
  26. package/dist/tools/blockchain/etherscan-client.js +156 -0
  27. package/dist/tools/blockchain/etherscan-client.test.d.ts +1 -0
  28. package/dist/tools/blockchain/etherscan-client.test.js +211 -0
  29. package/dist/tools/blockchain/formatters.d.ts +10 -0
  30. package/dist/tools/blockchain/formatters.js +147 -0
  31. package/dist/tools/blockchain/index.d.ts +10 -0
  32. package/dist/tools/blockchain/index.js +10 -0
  33. package/dist/tools/blockchain/tools.test.d.ts +1 -0
  34. package/dist/tools/blockchain/tools.test.js +208 -0
  35. package/dist/tools/blockchain/types.d.ts +90 -0
  36. package/dist/tools/blockchain/types.js +8 -0
  37. package/dist/tools/diff-summary.d.ts +20 -0
  38. package/dist/tools/diff-summary.js +111 -0
  39. package/dist/tools/gitingest.d.ts +26 -0
  40. package/dist/tools/gitingest.js +41 -0
  41. package/dist/tools/index.d.ts +5 -0
  42. package/dist/tools/index.js +5 -0
  43. package/dist/tools/list-child-sessions.d.ts +9 -0
  44. package/dist/tools/list-child-sessions.js +24 -0
  45. package/dist/tools/prompt-session.d.ts +19 -0
  46. package/dist/tools/prompt-session.js +39 -0
  47. package/dist/tools/reply-child.d.ts +19 -0
  48. package/dist/tools/reply-child.js +42 -0
  49. package/images/logo.png +0 -0
  50. package/package.json +4 -2
  51. package/command/commit.md +0 -18
@@ -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 {};
@@ -0,0 +1,211 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { EtherscanClient, EtherscanClientError, weiToEth, formatTimestamp, shortenAddress } from "./etherscan-client";
3
+ describe("EtherscanClient", () => {
4
+ const originalEnv = process.env.ETHERSCAN_API_KEY;
5
+ const mockFetch = vi.fn();
6
+ beforeEach(() => {
7
+ vi.resetAllMocks();
8
+ vi.stubGlobal("fetch", mockFetch);
9
+ });
10
+ afterEach(() => {
11
+ vi.unstubAllGlobals();
12
+ if (originalEnv) {
13
+ process.env.ETHERSCAN_API_KEY = originalEnv;
14
+ }
15
+ else {
16
+ delete process.env.ETHERSCAN_API_KEY;
17
+ }
18
+ });
19
+ describe("constructor", () => {
20
+ it("should throw error when API key is missing", () => {
21
+ delete process.env.ETHERSCAN_API_KEY;
22
+ expect(() => new EtherscanClient()).toThrow(EtherscanClientError);
23
+ expect(() => new EtherscanClient()).toThrow("ETHERSCAN_API_KEY environment variable is required");
24
+ });
25
+ it("should create client with API key from environment", () => {
26
+ process.env.ETHERSCAN_API_KEY = "test-api-key";
27
+ const client = new EtherscanClient();
28
+ expect(client).toBeInstanceOf(EtherscanClient);
29
+ });
30
+ it("should create client with provided API key", () => {
31
+ delete process.env.ETHERSCAN_API_KEY;
32
+ const client = new EtherscanClient("provided-api-key");
33
+ expect(client).toBeInstanceOf(EtherscanClient);
34
+ });
35
+ it("should create client with custom chainId", () => {
36
+ process.env.ETHERSCAN_API_KEY = "test-api-key";
37
+ const client = new EtherscanClient(undefined, "137");
38
+ expect(client).toBeInstanceOf(EtherscanClient);
39
+ });
40
+ });
41
+ describe("getBalance", () => {
42
+ it("should return balance for valid address", async () => {
43
+ process.env.ETHERSCAN_API_KEY = "test-api-key";
44
+ const mockResponse = {
45
+ status: "1",
46
+ message: "OK",
47
+ result: "1000000000000000000",
48
+ };
49
+ mockFetch.mockResolvedValueOnce({
50
+ ok: true,
51
+ json: async () => mockResponse,
52
+ });
53
+ const client = new EtherscanClient();
54
+ const balance = await client.getBalance("0x1234567890123456789012345678901234567890");
55
+ expect(balance).toBe("1000000000000000000");
56
+ expect(mockFetch).toHaveBeenCalledTimes(1);
57
+ const callUrl = mockFetch.mock.calls[0][0];
58
+ expect(callUrl).toContain("module=account");
59
+ expect(callUrl).toContain("action=balance");
60
+ expect(callUrl).toContain("chainid=1");
61
+ expect(callUrl).toContain("/v2/api");
62
+ });
63
+ it("should use custom chainId in requests", async () => {
64
+ process.env.ETHERSCAN_API_KEY = "test-api-key";
65
+ const mockResponse = {
66
+ status: "1",
67
+ message: "OK",
68
+ result: "500000000000000000",
69
+ };
70
+ mockFetch.mockResolvedValueOnce({
71
+ ok: true,
72
+ json: async () => mockResponse,
73
+ });
74
+ const client = new EtherscanClient(undefined, "137");
75
+ await client.getBalance("0x1234567890123456789012345678901234567890");
76
+ const callUrl = mockFetch.mock.calls[0][0];
77
+ expect(callUrl).toContain("chainid=137");
78
+ });
79
+ it("should throw error on API failure", async () => {
80
+ process.env.ETHERSCAN_API_KEY = "test-api-key";
81
+ const mockResponse = {
82
+ status: "0",
83
+ message: "NOTOK",
84
+ result: "Invalid address format",
85
+ };
86
+ mockFetch.mockResolvedValueOnce({
87
+ ok: true,
88
+ json: async () => mockResponse,
89
+ });
90
+ const client = new EtherscanClient();
91
+ await expect(client.getBalance("invalid-address")).rejects.toThrow(EtherscanClientError);
92
+ });
93
+ it("should throw error on HTTP failure", async () => {
94
+ process.env.ETHERSCAN_API_KEY = "test-api-key";
95
+ mockFetch.mockResolvedValueOnce({
96
+ ok: false,
97
+ status: 500,
98
+ statusText: "Internal Server Error",
99
+ });
100
+ const client = new EtherscanClient();
101
+ await expect(client.getBalance("0x1234567890123456789012345678901234567890")).rejects.toThrow("Etherscan API HTTP error: 500 Internal Server Error");
102
+ });
103
+ });
104
+ describe("getTransactions", () => {
105
+ it("should return transactions for valid address", async () => {
106
+ process.env.ETHERSCAN_API_KEY = "test-api-key";
107
+ const mockTransactions = [
108
+ {
109
+ hash: "0xabc123",
110
+ from: "0x1111111111111111111111111111111111111111",
111
+ to: "0x2222222222222222222222222222222222222222",
112
+ value: "1000000000000000000",
113
+ timeStamp: "1640000000",
114
+ isError: "0",
115
+ },
116
+ ];
117
+ const mockResponse = {
118
+ status: "1",
119
+ message: "OK",
120
+ result: mockTransactions,
121
+ };
122
+ mockFetch.mockResolvedValueOnce({
123
+ ok: true,
124
+ json: async () => mockResponse,
125
+ });
126
+ const client = new EtherscanClient();
127
+ const transactions = await client.getTransactions("0x1234567890123456789012345678901234567890", 10);
128
+ expect(transactions).toHaveLength(1);
129
+ expect(transactions[0].hash).toBe("0xabc123");
130
+ });
131
+ it("should return empty array when no transactions found", async () => {
132
+ process.env.ETHERSCAN_API_KEY = "test-api-key";
133
+ const mockResponse = {
134
+ status: "0",
135
+ message: "No transactions found",
136
+ result: "No transactions found",
137
+ };
138
+ mockFetch.mockResolvedValueOnce({
139
+ ok: true,
140
+ json: async () => mockResponse,
141
+ });
142
+ const client = new EtherscanClient();
143
+ const transactions = await client.getTransactions("0x1234567890123456789012345678901234567890");
144
+ expect(transactions).toEqual([]);
145
+ });
146
+ });
147
+ describe("getTokenTransfers", () => {
148
+ it("should return token transfers for valid address", async () => {
149
+ process.env.ETHERSCAN_API_KEY = "test-api-key";
150
+ const mockTransfers = [
151
+ {
152
+ hash: "0xdef456",
153
+ from: "0x1111111111111111111111111111111111111111",
154
+ to: "0x2222222222222222222222222222222222222222",
155
+ value: "1000000000000000000",
156
+ tokenName: "Test Token",
157
+ tokenSymbol: "TST",
158
+ tokenDecimal: "18",
159
+ contractAddress: "0x3333333333333333333333333333333333333333",
160
+ },
161
+ ];
162
+ const mockResponse = {
163
+ status: "1",
164
+ message: "OK",
165
+ result: mockTransfers,
166
+ };
167
+ mockFetch.mockResolvedValueOnce({
168
+ ok: true,
169
+ json: async () => mockResponse,
170
+ });
171
+ const client = new EtherscanClient();
172
+ const transfers = await client.getTokenTransfers("0x1234567890123456789012345678901234567890");
173
+ expect(transfers).toHaveLength(1);
174
+ expect(transfers[0].tokenSymbol).toBe("TST");
175
+ });
176
+ });
177
+ });
178
+ describe("weiToEth", () => {
179
+ it("should convert 1 ETH correctly", () => {
180
+ expect(weiToEth("1000000000000000000")).toBe("1");
181
+ });
182
+ it("should convert 0.5 ETH correctly", () => {
183
+ expect(weiToEth("500000000000000000")).toBe("0.5");
184
+ });
185
+ it("should convert 0 ETH correctly", () => {
186
+ expect(weiToEth("0")).toBe("0");
187
+ });
188
+ it("should handle large values", () => {
189
+ expect(weiToEth("123456789000000000000000000")).toBe("123456789");
190
+ });
191
+ it("should handle small fractional values", () => {
192
+ const result = weiToEth("1234567890123456789");
193
+ expect(result).toBe("1.234567890123456789");
194
+ });
195
+ });
196
+ describe("formatTimestamp", () => {
197
+ it("should format Unix timestamp to ISO string", () => {
198
+ const result = formatTimestamp("1640000000");
199
+ expect(result).toBe("2021-12-20T11:33:20.000Z");
200
+ });
201
+ });
202
+ describe("shortenAddress", () => {
203
+ it("should shorten long address", () => {
204
+ const address = "0x1234567890123456789012345678901234567890";
205
+ expect(shortenAddress(address)).toBe("0x1234...7890");
206
+ });
207
+ it("should return short address unchanged", () => {
208
+ const address = "0x12345";
209
+ expect(shortenAddress(address)).toBe("0x12345");
210
+ });
211
+ });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Formatting utilities for blockchain data
3
+ */
4
+ import { type EthTransaction, type EthTokenTransfer } from "./types";
5
+ export declare function formatTransactionReceipt(hash: string, receipt: Record<string, unknown>): string;
6
+ export declare function formatTransaction(tx: EthTransaction, address: string): string;
7
+ export declare function formatTransactionList(address: string, transactions: EthTransaction[]): string;
8
+ export declare function formatBalance(address: string, balanceWei: string): string;
9
+ export declare function formatTokenTransfer(transfer: EthTokenTransfer, address: string): string;
10
+ export declare function formatTokenTransferList(address: string, transfers: EthTokenTransfer[]): string;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Formatting utilities for blockchain data
3
+ */
4
+ import { weiToEth, formatTimestamp, shortenAddress } from "./etherscan-client";
5
+ export function formatTransactionReceipt(hash, receipt) {
6
+ const status = receipt.status === "0x1" ? "Success" : "Failed";
7
+ const gasUsed = receipt.gasUsed ? parseInt(String(receipt.gasUsed), 16).toString() : "N/A";
8
+ const effectiveGasPrice = receipt.effectiveGasPrice
9
+ ? parseInt(String(receipt.effectiveGasPrice), 16).toString()
10
+ : "N/A";
11
+ const gasCostWei = receipt.gasUsed && receipt.effectiveGasPrice
12
+ ? (BigInt(String(receipt.gasUsed)) * BigInt(String(receipt.effectiveGasPrice))).toString()
13
+ : "0";
14
+ const lines = [
15
+ `## Transaction Details`,
16
+ ``,
17
+ `**Hash:** ${hash}`,
18
+ `**Status:** ${status}`,
19
+ `**Block:** ${receipt.blockNumber ? parseInt(String(receipt.blockNumber), 16) : "Pending"}`,
20
+ ``,
21
+ `### Addresses`,
22
+ `**From:** ${receipt.from}`,
23
+ `**To:** ${receipt.to ?? "Contract Creation"}`,
24
+ receipt.contractAddress ? `**Contract Created:** ${receipt.contractAddress}` : null,
25
+ ``,
26
+ `### Gas`,
27
+ `**Gas Used:** ${gasUsed}`,
28
+ `**Effective Gas Price:** ${effectiveGasPrice} wei`,
29
+ `**Transaction Cost:** ${weiToEth(gasCostWei)} ETH`,
30
+ ``,
31
+ `### Logs`,
32
+ `**Log Count:** ${Array.isArray(receipt.logs) ? receipt.logs.length : 0}`,
33
+ ].filter(Boolean);
34
+ return lines.join("\n");
35
+ }
36
+ export function formatTransaction(tx, address) {
37
+ const isOutgoing = tx.from.toLowerCase() === address.toLowerCase();
38
+ const direction = isOutgoing ? "OUT" : "IN";
39
+ const counterparty = isOutgoing ? (tx.to ?? "Contract Creation") : tx.from;
40
+ const status = tx.isError === "0" ? "OK" : "FAIL";
41
+ const value = weiToEth(tx.value);
42
+ const date = formatTimestamp(tx.timeStamp);
43
+ return [
44
+ `[${direction}] ${date}`,
45
+ ` Hash: ${tx.hash}`,
46
+ ` ${isOutgoing ? "To" : "From"}: ${counterparty}`,
47
+ ` Value: ${value} ETH`,
48
+ ` Status: ${status}`,
49
+ tx.functionName ? ` Function: ${tx.functionName}` : null,
50
+ ].filter(Boolean).join("\n");
51
+ }
52
+ export function formatTransactionList(address, transactions) {
53
+ if (transactions.length === 0) {
54
+ return `No transactions found for address: ${address}`;
55
+ }
56
+ const addressLower = address.toLowerCase();
57
+ const inCount = transactions.filter(tx => tx.to?.toLowerCase() === addressLower).length;
58
+ const outCount = transactions.length - inCount;
59
+ const lines = [
60
+ `## Transactions for ${shortenAddress(address)}`,
61
+ ``,
62
+ `**Total:** ${transactions.length} (${inCount} incoming, ${outCount} outgoing)`,
63
+ `**Address:** ${address}`,
64
+ ``,
65
+ `### Recent Transactions`,
66
+ ``,
67
+ ...transactions.map(tx => formatTransaction(tx, address)),
68
+ ];
69
+ return lines.join("\n");
70
+ }
71
+ export function formatBalance(address, balanceWei) {
72
+ const balanceEth = weiToEth(balanceWei);
73
+ const lines = [
74
+ `## Balance for ${address}`,
75
+ ``,
76
+ `**ETH:** ${balanceEth}`,
77
+ `**Wei:** ${balanceWei}`,
78
+ ];
79
+ return lines.join("\n");
80
+ }
81
+ function formatTokenValue(value, decimals) {
82
+ const dec = parseInt(decimals, 10) || 18;
83
+ const valueBigInt = BigInt(value);
84
+ const divisor = BigInt(10 ** dec);
85
+ const wholePart = valueBigInt / divisor;
86
+ const fractionPart = valueBigInt % divisor;
87
+ const fractionStr = fractionPart.toString().padStart(dec, "0");
88
+ const trimmedFraction = fractionStr.replace(/0+$/, "").slice(0, 6);
89
+ if (trimmedFraction === "") {
90
+ return wholePart.toString();
91
+ }
92
+ return `${wholePart}.${trimmedFraction}`;
93
+ }
94
+ export function formatTokenTransfer(transfer, address) {
95
+ const isOutgoing = transfer.from.toLowerCase() === address.toLowerCase();
96
+ const direction = isOutgoing ? "OUT" : "IN";
97
+ const counterparty = isOutgoing ? (transfer.to ?? "Unknown") : transfer.from;
98
+ const value = formatTokenValue(transfer.value, transfer.tokenDecimal);
99
+ const date = formatTimestamp(transfer.timeStamp);
100
+ return [
101
+ `[${direction}] ${date}`,
102
+ ` Token: ${transfer.tokenName} (${transfer.tokenSymbol})`,
103
+ ` Hash: ${transfer.hash}`,
104
+ ` ${isOutgoing ? "To" : "From"}: ${counterparty}`,
105
+ ` Value: ${value} ${transfer.tokenSymbol}`,
106
+ ` Contract: ${transfer.contractAddress}`,
107
+ ].join("\n");
108
+ }
109
+ export function formatTokenTransferList(address, transfers) {
110
+ if (transfers.length === 0) {
111
+ return `No ERC-20 token transfers found for address: ${address}`;
112
+ }
113
+ const tokenSummary = new Map();
114
+ let inCount = 0;
115
+ let outCount = 0;
116
+ const addressLower = address.toLowerCase();
117
+ for (const transfer of transfers) {
118
+ const isOutgoing = transfer.from.toLowerCase() === addressLower;
119
+ if (!tokenSummary.has(transfer.contractAddress)) {
120
+ tokenSummary.set(transfer.contractAddress, { in: 0, out: 0, symbol: transfer.tokenSymbol });
121
+ }
122
+ const stats = tokenSummary.get(transfer.contractAddress);
123
+ if (isOutgoing) {
124
+ stats.out++;
125
+ outCount++;
126
+ }
127
+ else {
128
+ stats.in++;
129
+ inCount++;
130
+ }
131
+ }
132
+ const lines = [
133
+ `## ERC-20 Token Transfers for ${shortenAddress(address)}`,
134
+ ``,
135
+ `**Total:** ${transfers.length} transfers (${inCount} incoming, ${outCount} outgoing)`,
136
+ `**Address:** ${address}`,
137
+ `**Unique Tokens:** ${tokenSummary.size}`,
138
+ ``,
139
+ `### Token Summary`,
140
+ ...Array.from(tokenSummary.entries()).map(([contract, stats]) => `- ${stats.symbol}: ${stats.in} in, ${stats.out} out (${shortenAddress(contract)})`),
141
+ ``,
142
+ `### Recent Transfers`,
143
+ ``,
144
+ ...transfers.map(t => formatTokenTransfer(t, address)),
145
+ ];
146
+ return lines.join("\n");
147
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Blockchain tools for Ethereum transaction and address tracing
3
+ */
4
+ export { EtherscanClient, EtherscanClientError, weiToEth, formatTimestamp, shortenAddress, } from "./etherscan-client";
5
+ export { formatTransactionReceipt, formatTransactionList, formatBalance, formatTokenTransferList, } from "./formatters";
6
+ export { ethTransactionTool, getTransactionDetails, type EthTransactionArgs, } from "./eth-transaction";
7
+ export { ethAddressTxsTool, getAddressTransactions, type EthAddressTxsArgs, } from "./eth-address-txs";
8
+ export { ethAddressBalanceTool, getAddressBalance, type EthAddressBalanceArgs, } from "./eth-address-balance";
9
+ export { ethTokenTransfersTool, getTokenTransfers, type EthTokenTransfersArgs, } from "./eth-token-transfers";
10
+ export * from "./types";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Blockchain tools for Ethereum transaction and address tracing
3
+ */
4
+ export { EtherscanClient, EtherscanClientError, weiToEth, formatTimestamp, shortenAddress, } from "./etherscan-client";
5
+ export { formatTransactionReceipt, formatTransactionList, formatBalance, formatTokenTransferList, } from "./formatters";
6
+ export { ethTransactionTool, getTransactionDetails, } from "./eth-transaction";
7
+ export { ethAddressTxsTool, getAddressTransactions, } from "./eth-address-txs";
8
+ export { ethAddressBalanceTool, getAddressBalance, } from "./eth-address-balance";
9
+ export { ethTokenTransfersTool, getTokenTransfers, } from "./eth-token-transfers";
10
+ export * from "./types";
@@ -0,0 +1 @@
1
+ export {};