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 +125 -0
- package/dist/index.js +13 -2
- package/dist/tools/blockchain/eth-address-balance.d.ts +20 -0
- package/dist/tools/blockchain/eth-address-balance.js +37 -0
- package/dist/tools/blockchain/eth-address-txs.d.ts +23 -0
- package/dist/tools/blockchain/eth-address-txs.js +41 -0
- package/dist/tools/blockchain/eth-token-transfers.d.ts +23 -0
- package/dist/tools/blockchain/eth-token-transfers.js +41 -0
- package/dist/tools/blockchain/eth-transaction.d.ts +20 -0
- package/dist/tools/blockchain/eth-transaction.js +40 -0
- package/dist/tools/blockchain/etherscan-client.d.ts +25 -0
- package/dist/tools/blockchain/etherscan-client.js +156 -0
- package/dist/tools/blockchain/etherscan-client.test.d.ts +1 -0
- package/dist/tools/blockchain/etherscan-client.test.js +211 -0
- package/dist/tools/blockchain/formatters.d.ts +10 -0
- package/dist/tools/blockchain/formatters.js +147 -0
- package/dist/tools/blockchain/index.d.ts +10 -0
- package/dist/tools/blockchain/index.js +10 -0
- package/dist/tools/blockchain/tools.test.d.ts +1 -0
- package/dist/tools/blockchain/tools.test.js +208 -0
- package/dist/tools/blockchain/types.d.ts +90 -0
- package/dist/tools/blockchain/types.js +8 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +1 -0
- package/package.json +1 -1
|
@@ -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 {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { formatTransactionReceipt, formatTransactionList, formatBalance, formatTokenTransferList, } from "./formatters";
|
|
3
|
+
describe("Blockchain Formatters", () => {
|
|
4
|
+
describe("formatTransactionReceipt", () => {
|
|
5
|
+
it("should format successful transaction receipt", () => {
|
|
6
|
+
const receipt = {
|
|
7
|
+
status: "0x1",
|
|
8
|
+
blockNumber: "0xf4240",
|
|
9
|
+
from: "0x1111111111111111111111111111111111111111",
|
|
10
|
+
to: "0x2222222222222222222222222222222222222222",
|
|
11
|
+
gasUsed: "0x5208",
|
|
12
|
+
effectiveGasPrice: "0x3b9aca00",
|
|
13
|
+
logs: [],
|
|
14
|
+
};
|
|
15
|
+
const result = formatTransactionReceipt("0xabc123", receipt);
|
|
16
|
+
expect(result).toContain("## Transaction Details");
|
|
17
|
+
expect(result).toContain("**Hash:** 0xabc123");
|
|
18
|
+
expect(result).toContain("**Status:** Success");
|
|
19
|
+
expect(result).toContain("**Block:** 1000000");
|
|
20
|
+
expect(result).toContain("**From:** 0x1111111111111111111111111111111111111111");
|
|
21
|
+
expect(result).toContain("**To:** 0x2222222222222222222222222222222222222222");
|
|
22
|
+
expect(result).toContain("**Gas Used:** 21000");
|
|
23
|
+
expect(result).toContain("**Log Count:** 0");
|
|
24
|
+
});
|
|
25
|
+
it("should format failed transaction receipt", () => {
|
|
26
|
+
const receipt = {
|
|
27
|
+
status: "0x0",
|
|
28
|
+
blockNumber: "0xf4240",
|
|
29
|
+
from: "0x1111111111111111111111111111111111111111",
|
|
30
|
+
to: "0x2222222222222222222222222222222222222222",
|
|
31
|
+
gasUsed: "0x5208",
|
|
32
|
+
effectiveGasPrice: "0x3b9aca00",
|
|
33
|
+
logs: [],
|
|
34
|
+
};
|
|
35
|
+
const result = formatTransactionReceipt("0xabc123", receipt);
|
|
36
|
+
expect(result).toContain("**Status:** Failed");
|
|
37
|
+
});
|
|
38
|
+
it("should handle contract creation", () => {
|
|
39
|
+
const receipt = {
|
|
40
|
+
status: "0x1",
|
|
41
|
+
blockNumber: "0xf4240",
|
|
42
|
+
from: "0x1111111111111111111111111111111111111111",
|
|
43
|
+
to: null,
|
|
44
|
+
contractAddress: "0x3333333333333333333333333333333333333333",
|
|
45
|
+
gasUsed: "0x5208",
|
|
46
|
+
effectiveGasPrice: "0x3b9aca00",
|
|
47
|
+
logs: [],
|
|
48
|
+
};
|
|
49
|
+
const result = formatTransactionReceipt("0xabc123", receipt);
|
|
50
|
+
expect(result).toContain("**To:** Contract Creation");
|
|
51
|
+
expect(result).toContain("**Contract Created:** 0x3333333333333333333333333333333333333333");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe("formatTransactionList", () => {
|
|
55
|
+
it("should format transaction list with in and out", () => {
|
|
56
|
+
const address = "0x1234567890123456789012345678901234567890";
|
|
57
|
+
const transactions = [
|
|
58
|
+
{
|
|
59
|
+
hash: "0xabc123",
|
|
60
|
+
blockNumber: "1000000",
|
|
61
|
+
blockHash: "0x...",
|
|
62
|
+
timeStamp: "1640000000",
|
|
63
|
+
from: address,
|
|
64
|
+
to: "0x2222222222222222222222222222222222222222",
|
|
65
|
+
value: "1000000000000000000",
|
|
66
|
+
gas: "21000",
|
|
67
|
+
gasPrice: "1000000000",
|
|
68
|
+
gasUsed: "21000",
|
|
69
|
+
nonce: "1",
|
|
70
|
+
transactionIndex: "0",
|
|
71
|
+
input: "0x",
|
|
72
|
+
isError: "0",
|
|
73
|
+
txreceipt_status: "1",
|
|
74
|
+
contractAddress: "",
|
|
75
|
+
cumulativeGasUsed: "21000",
|
|
76
|
+
confirmations: "100",
|
|
77
|
+
methodId: "0x",
|
|
78
|
+
functionName: "transfer(address,uint256)",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
hash: "0xdef456",
|
|
82
|
+
blockNumber: "999999",
|
|
83
|
+
blockHash: "0x...",
|
|
84
|
+
timeStamp: "1639999000",
|
|
85
|
+
from: "0x3333333333333333333333333333333333333333",
|
|
86
|
+
to: address,
|
|
87
|
+
value: "500000000000000000",
|
|
88
|
+
gas: "21000",
|
|
89
|
+
gasPrice: "1000000000",
|
|
90
|
+
gasUsed: "21000",
|
|
91
|
+
nonce: "1",
|
|
92
|
+
transactionIndex: "0",
|
|
93
|
+
input: "0x",
|
|
94
|
+
isError: "0",
|
|
95
|
+
txreceipt_status: "1",
|
|
96
|
+
contractAddress: "",
|
|
97
|
+
cumulativeGasUsed: "21000",
|
|
98
|
+
confirmations: "100",
|
|
99
|
+
methodId: "0x",
|
|
100
|
+
functionName: "",
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
const result = formatTransactionList(address, transactions);
|
|
104
|
+
expect(result).toContain("## Transactions for 0x1234...7890");
|
|
105
|
+
expect(result).toContain("**Total:** 2 (1 incoming, 1 outgoing)");
|
|
106
|
+
expect(result).toContain("[OUT]");
|
|
107
|
+
expect(result).toContain("[IN]");
|
|
108
|
+
expect(result).toContain("Value: 1 ETH");
|
|
109
|
+
expect(result).toContain("Value: 0.5 ETH");
|
|
110
|
+
expect(result).toContain("Function: transfer(address,uint256)");
|
|
111
|
+
});
|
|
112
|
+
it("should return no transactions message for empty list", () => {
|
|
113
|
+
const result = formatTransactionList("0x1234567890123456789012345678901234567890", []);
|
|
114
|
+
expect(result).toContain("No transactions found");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe("formatBalance", () => {
|
|
118
|
+
it("should format balance in ETH and Wei", () => {
|
|
119
|
+
const result = formatBalance("0x1234567890123456789012345678901234567890", "1500000000000000000");
|
|
120
|
+
expect(result).toContain("## Balance for 0x1234567890123456789012345678901234567890");
|
|
121
|
+
expect(result).toContain("**ETH:** 1.5");
|
|
122
|
+
expect(result).toContain("**Wei:** 1500000000000000000");
|
|
123
|
+
});
|
|
124
|
+
it("should handle zero balance", () => {
|
|
125
|
+
const result = formatBalance("0x1234567890123456789012345678901234567890", "0");
|
|
126
|
+
expect(result).toContain("**ETH:** 0");
|
|
127
|
+
expect(result).toContain("**Wei:** 0");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe("formatTokenTransferList", () => {
|
|
131
|
+
it("should format token transfer list", () => {
|
|
132
|
+
const address = "0x1234567890123456789012345678901234567890";
|
|
133
|
+
const transfers = [
|
|
134
|
+
{
|
|
135
|
+
hash: "0xtoken123",
|
|
136
|
+
blockNumber: "1000000",
|
|
137
|
+
timeStamp: "1640000000",
|
|
138
|
+
from: address,
|
|
139
|
+
to: "0x2222222222222222222222222222222222222222",
|
|
140
|
+
value: "1000000000000000000",
|
|
141
|
+
contractAddress: "0x4444444444444444444444444444444444444444",
|
|
142
|
+
tokenName: "Test Token",
|
|
143
|
+
tokenSymbol: "TST",
|
|
144
|
+
tokenDecimal: "18",
|
|
145
|
+
gas: "60000",
|
|
146
|
+
gasPrice: "1000000000",
|
|
147
|
+
gasUsed: "55000",
|
|
148
|
+
nonce: "1",
|
|
149
|
+
transactionIndex: "0",
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
const result = formatTokenTransferList(address, transfers);
|
|
153
|
+
expect(result).toContain("## ERC-20 Token Transfers for 0x1234...7890");
|
|
154
|
+
expect(result).toContain("**Total:** 1 transfers");
|
|
155
|
+
expect(result).toContain("**Unique Tokens:** 1");
|
|
156
|
+
expect(result).toContain("Token: Test Token (TST)");
|
|
157
|
+
expect(result).toContain("[OUT]");
|
|
158
|
+
expect(result).toContain("Value: 1 TST");
|
|
159
|
+
});
|
|
160
|
+
it("should return no transfers message for empty list", () => {
|
|
161
|
+
const result = formatTokenTransferList("0x1234567890123456789012345678901234567890", []);
|
|
162
|
+
expect(result).toContain("No ERC-20 token transfers found");
|
|
163
|
+
});
|
|
164
|
+
it("should show token summary with multiple tokens", () => {
|
|
165
|
+
const address = "0x1234567890123456789012345678901234567890";
|
|
166
|
+
const transfers = [
|
|
167
|
+
{
|
|
168
|
+
hash: "0xtoken1",
|
|
169
|
+
blockNumber: "1000000",
|
|
170
|
+
timeStamp: "1640000000",
|
|
171
|
+
from: address,
|
|
172
|
+
to: "0x2222222222222222222222222222222222222222",
|
|
173
|
+
value: "1000000000000000000",
|
|
174
|
+
contractAddress: "0x4444444444444444444444444444444444444444",
|
|
175
|
+
tokenName: "Token A",
|
|
176
|
+
tokenSymbol: "TKA",
|
|
177
|
+
tokenDecimal: "18",
|
|
178
|
+
gas: "60000",
|
|
179
|
+
gasPrice: "1000000000",
|
|
180
|
+
gasUsed: "55000",
|
|
181
|
+
nonce: "1",
|
|
182
|
+
transactionIndex: "0",
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
hash: "0xtoken2",
|
|
186
|
+
blockNumber: "1000001",
|
|
187
|
+
timeStamp: "1640001000",
|
|
188
|
+
from: "0x3333333333333333333333333333333333333333",
|
|
189
|
+
to: address,
|
|
190
|
+
value: "2000000000000000000",
|
|
191
|
+
contractAddress: "0x5555555555555555555555555555555555555555",
|
|
192
|
+
tokenName: "Token B",
|
|
193
|
+
tokenSymbol: "TKB",
|
|
194
|
+
tokenDecimal: "18",
|
|
195
|
+
gas: "60000",
|
|
196
|
+
gasPrice: "1000000000",
|
|
197
|
+
gasUsed: "55000",
|
|
198
|
+
nonce: "1",
|
|
199
|
+
transactionIndex: "0",
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
const result = formatTokenTransferList(address, transfers);
|
|
203
|
+
expect(result).toContain("**Unique Tokens:** 2");
|
|
204
|
+
expect(result).toContain("TKA: 0 in, 1 out");
|
|
205
|
+
expect(result).toContain("TKB: 1 in, 0 out");
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|