opencode-froggy 0.3.0 → 0.5.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 +179 -59
- package/command/agent-promote.md +5 -0
- package/command/commit-push.md +8 -3
- package/command/diff-summary.md +51 -0
- package/command/doc-changes.md +1 -1
- package/command/gh-create-pr.md +18 -0
- package/command/review-changes.md +15 -1
- package/command/review-pr.md +4 -2
- package/command/simplify-changes.md +1 -1
- package/dist/index.js +11 -5
- package/dist/tools/agent-promote-core.d.ts +6 -0
- package/dist/tools/agent-promote-core.js +14 -0
- package/dist/tools/agent-promote.d.ts +19 -0
- package/dist/tools/agent-promote.js +39 -0
- package/dist/tools/agent-promote.test.d.ts +1 -0
- package/dist/tools/agent-promote.test.js +71 -0
- 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 +1 -1
- package/dist/tools/index.js +1 -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
|
@@ -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;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event decoder for common ERC-20 and WETH events
|
|
3
|
+
*/
|
|
4
|
+
const EVENT_SIGNATURES = {
|
|
5
|
+
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef": "Transfer",
|
|
6
|
+
"0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925": "Approval",
|
|
7
|
+
"0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c": "Deposit",
|
|
8
|
+
"0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65": "Withdrawal",
|
|
9
|
+
};
|
|
10
|
+
function decodeAddress(topic) {
|
|
11
|
+
return "0x" + topic.slice(26).toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
function decodeUint256(data) {
|
|
14
|
+
const hex = data.startsWith("0x") ? data.slice(2) : data;
|
|
15
|
+
if (hex === "")
|
|
16
|
+
return "0";
|
|
17
|
+
return BigInt("0x" + hex).toString();
|
|
18
|
+
}
|
|
19
|
+
export async function decodeEvent(log, resolver) {
|
|
20
|
+
const topic0 = log.topics[0];
|
|
21
|
+
const signature = EVENT_SIGNATURES[topic0];
|
|
22
|
+
if (!signature) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const contractAddress = await resolver.resolve(log.address);
|
|
26
|
+
switch (signature) {
|
|
27
|
+
case "Transfer": {
|
|
28
|
+
if (log.topics.length < 3)
|
|
29
|
+
return null;
|
|
30
|
+
const from = decodeAddress(log.topics[1]);
|
|
31
|
+
const to = decodeAddress(log.topics[2]);
|
|
32
|
+
const value = decodeUint256(log.data);
|
|
33
|
+
return {
|
|
34
|
+
name: "Transfer",
|
|
35
|
+
address: contractAddress,
|
|
36
|
+
params: { from, to, value },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
case "Approval": {
|
|
40
|
+
if (log.topics.length < 3)
|
|
41
|
+
return null;
|
|
42
|
+
const owner = decodeAddress(log.topics[1]);
|
|
43
|
+
const spender = decodeAddress(log.topics[2]);
|
|
44
|
+
const value = decodeUint256(log.data);
|
|
45
|
+
return {
|
|
46
|
+
name: "Approval",
|
|
47
|
+
address: contractAddress,
|
|
48
|
+
params: { owner, spender, value },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
case "Deposit": {
|
|
52
|
+
if (log.topics.length < 2)
|
|
53
|
+
return null;
|
|
54
|
+
const dst = decodeAddress(log.topics[1]);
|
|
55
|
+
const wad = decodeUint256(log.data);
|
|
56
|
+
return {
|
|
57
|
+
name: "Deposit",
|
|
58
|
+
address: contractAddress,
|
|
59
|
+
params: { dst, wad },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
case "Withdrawal": {
|
|
63
|
+
if (log.topics.length < 2)
|
|
64
|
+
return null;
|
|
65
|
+
const src = decodeAddress(log.topics[1]);
|
|
66
|
+
const wad = decodeUint256(log.data);
|
|
67
|
+
return {
|
|
68
|
+
name: "Withdrawal",
|
|
69
|
+
address: contractAddress,
|
|
70
|
+
params: { src, wad },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
default:
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export async function decodeEvents(logs, resolver) {
|
|
78
|
+
const decoded = [];
|
|
79
|
+
let undecodedCount = 0;
|
|
80
|
+
for (const log of logs) {
|
|
81
|
+
const event = await decodeEvent(log, resolver);
|
|
82
|
+
if (event) {
|
|
83
|
+
decoded.push(event);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
undecodedCount++;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { decoded, undecodedCount };
|
|
90
|
+
}
|
|
91
|
+
export function isKnownEvent(topic0) {
|
|
92
|
+
return topic0 in EVENT_SIGNATURES;
|
|
93
|
+
}
|
|
94
|
+
export function getEventName(topic0) {
|
|
95
|
+
return EVENT_SIGNATURES[topic0] ?? null;
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { decodeEvent, decodeEvents, isKnownEvent, getEventName, } from "./event-decoder";
|
|
3
|
+
const mockResolver = {
|
|
4
|
+
async resolve(address) {
|
|
5
|
+
return { address, label: null };
|
|
6
|
+
},
|
|
7
|
+
};
|
|
8
|
+
describe("Event Decoder", () => {
|
|
9
|
+
describe("isKnownEvent", () => {
|
|
10
|
+
it("should recognize Transfer event", () => {
|
|
11
|
+
const topic0 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
12
|
+
expect(isKnownEvent(topic0)).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
it("should recognize Approval event", () => {
|
|
15
|
+
const topic0 = "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925";
|
|
16
|
+
expect(isKnownEvent(topic0)).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
it("should recognize Deposit event", () => {
|
|
19
|
+
const topic0 = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c";
|
|
20
|
+
expect(isKnownEvent(topic0)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
it("should recognize Withdrawal event", () => {
|
|
23
|
+
const topic0 = "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65";
|
|
24
|
+
expect(isKnownEvent(topic0)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
it("should return false for unknown event", () => {
|
|
27
|
+
const topic0 = "0x0000000000000000000000000000000000000000000000000000000000000000";
|
|
28
|
+
expect(isKnownEvent(topic0)).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe("getEventName", () => {
|
|
32
|
+
it("should return Transfer for Transfer topic", () => {
|
|
33
|
+
const topic0 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
34
|
+
expect(getEventName(topic0)).toBe("Transfer");
|
|
35
|
+
});
|
|
36
|
+
it("should return null for unknown topic", () => {
|
|
37
|
+
const topic0 = "0x0000000000000000000000000000000000000000000000000000000000000000";
|
|
38
|
+
expect(getEventName(topic0)).toBe(null);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe("decodeEvent", () => {
|
|
42
|
+
it("should decode Transfer event", async () => {
|
|
43
|
+
const log = {
|
|
44
|
+
address: "0x6b175474e89094c44da98b954eedeac495271d0f",
|
|
45
|
+
topics: [
|
|
46
|
+
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
|
47
|
+
"0x000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c8",
|
|
48
|
+
"0x000000000000000000000000000000002b0184b12e908bf97941f2f5385c9820",
|
|
49
|
+
],
|
|
50
|
+
data: "0x000000000000000000000000000000000000000000000278fd3c000000000000",
|
|
51
|
+
blockNumber: "0x1234",
|
|
52
|
+
transactionHash: "0xabc",
|
|
53
|
+
transactionIndex: "0x0",
|
|
54
|
+
blockHash: "0xdef",
|
|
55
|
+
logIndex: "0x0",
|
|
56
|
+
removed: false,
|
|
57
|
+
};
|
|
58
|
+
const result = await decodeEvent(log, mockResolver);
|
|
59
|
+
expect(result).not.toBeNull();
|
|
60
|
+
expect(result.name).toBe("Transfer");
|
|
61
|
+
expect(result.params.from).toBe("0xba12222222228d8ba445958a75a0704d566bf2c8");
|
|
62
|
+
expect(result.params.to).toBe("0x000000002b0184b12e908bf97941f2f5385c9820");
|
|
63
|
+
expect(result.params.value).toBe("11676589714374635028480");
|
|
64
|
+
});
|
|
65
|
+
it("should decode Approval event", async () => {
|
|
66
|
+
const log = {
|
|
67
|
+
address: "0x6b175474e89094c44da98b954eedeac495271d0f",
|
|
68
|
+
topics: [
|
|
69
|
+
"0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
|
|
70
|
+
"0x0000000000000000000000001111111111111111111111111111111111111111",
|
|
71
|
+
"0x0000000000000000000000002222222222222222222222222222222222222222",
|
|
72
|
+
],
|
|
73
|
+
data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
|
|
74
|
+
blockNumber: "0x1234",
|
|
75
|
+
transactionHash: "0xabc",
|
|
76
|
+
transactionIndex: "0x0",
|
|
77
|
+
blockHash: "0xdef",
|
|
78
|
+
logIndex: "0x0",
|
|
79
|
+
removed: false,
|
|
80
|
+
};
|
|
81
|
+
const result = await decodeEvent(log, mockResolver);
|
|
82
|
+
expect(result).not.toBeNull();
|
|
83
|
+
expect(result.name).toBe("Approval");
|
|
84
|
+
expect(result.params.owner).toBe("0x1111111111111111111111111111111111111111");
|
|
85
|
+
expect(result.params.spender).toBe("0x2222222222222222222222222222222222222222");
|
|
86
|
+
expect(result.params.value).toBe("1000000000000000000");
|
|
87
|
+
});
|
|
88
|
+
it("should decode Deposit event (WETH)", async () => {
|
|
89
|
+
const log = {
|
|
90
|
+
address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
|
|
91
|
+
topics: [
|
|
92
|
+
"0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c",
|
|
93
|
+
"0x0000000000000000000000001111111111111111111111111111111111111111",
|
|
94
|
+
],
|
|
95
|
+
data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
|
|
96
|
+
blockNumber: "0x1234",
|
|
97
|
+
transactionHash: "0xabc",
|
|
98
|
+
transactionIndex: "0x0",
|
|
99
|
+
blockHash: "0xdef",
|
|
100
|
+
logIndex: "0x0",
|
|
101
|
+
removed: false,
|
|
102
|
+
};
|
|
103
|
+
const result = await decodeEvent(log, mockResolver);
|
|
104
|
+
expect(result).not.toBeNull();
|
|
105
|
+
expect(result.name).toBe("Deposit");
|
|
106
|
+
expect(result.params.dst).toBe("0x1111111111111111111111111111111111111111");
|
|
107
|
+
expect(result.params.wad).toBe("1000000000000000000");
|
|
108
|
+
});
|
|
109
|
+
it("should decode Withdrawal event (WETH)", async () => {
|
|
110
|
+
const log = {
|
|
111
|
+
address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
|
|
112
|
+
topics: [
|
|
113
|
+
"0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65",
|
|
114
|
+
"0x0000000000000000000000001111111111111111111111111111111111111111",
|
|
115
|
+
],
|
|
116
|
+
data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
|
|
117
|
+
blockNumber: "0x1234",
|
|
118
|
+
transactionHash: "0xabc",
|
|
119
|
+
transactionIndex: "0x0",
|
|
120
|
+
blockHash: "0xdef",
|
|
121
|
+
logIndex: "0x0",
|
|
122
|
+
removed: false,
|
|
123
|
+
};
|
|
124
|
+
const result = await decodeEvent(log, mockResolver);
|
|
125
|
+
expect(result).not.toBeNull();
|
|
126
|
+
expect(result.name).toBe("Withdrawal");
|
|
127
|
+
expect(result.params.src).toBe("0x1111111111111111111111111111111111111111");
|
|
128
|
+
expect(result.params.wad).toBe("1000000000000000000");
|
|
129
|
+
});
|
|
130
|
+
it("should return null for unknown event", async () => {
|
|
131
|
+
const log = {
|
|
132
|
+
address: "0x1234567890123456789012345678901234567890",
|
|
133
|
+
topics: ["0x0000000000000000000000000000000000000000000000000000000000000000"],
|
|
134
|
+
data: "0x",
|
|
135
|
+
blockNumber: "0x1234",
|
|
136
|
+
transactionHash: "0xabc",
|
|
137
|
+
transactionIndex: "0x0",
|
|
138
|
+
blockHash: "0xdef",
|
|
139
|
+
logIndex: "0x0",
|
|
140
|
+
removed: false,
|
|
141
|
+
};
|
|
142
|
+
const result = await decodeEvent(log, mockResolver);
|
|
143
|
+
expect(result).toBeNull();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
describe("decodeEvents", () => {
|
|
147
|
+
it("should decode multiple events and count undecoded", async () => {
|
|
148
|
+
const logs = [
|
|
149
|
+
{
|
|
150
|
+
address: "0x6b175474e89094c44da98b954eedeac495271d0f",
|
|
151
|
+
topics: [
|
|
152
|
+
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
|
153
|
+
"0x0000000000000000000000001111111111111111111111111111111111111111",
|
|
154
|
+
"0x0000000000000000000000002222222222222222222222222222222222222222",
|
|
155
|
+
],
|
|
156
|
+
data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
|
|
157
|
+
blockNumber: "0x1234",
|
|
158
|
+
transactionHash: "0xabc",
|
|
159
|
+
transactionIndex: "0x0",
|
|
160
|
+
blockHash: "0xdef",
|
|
161
|
+
logIndex: "0x0",
|
|
162
|
+
removed: false,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
address: "0x1234567890123456789012345678901234567890",
|
|
166
|
+
topics: ["0x0000000000000000000000000000000000000000000000000000000000000000"],
|
|
167
|
+
data: "0x",
|
|
168
|
+
blockNumber: "0x1234",
|
|
169
|
+
transactionHash: "0xabc",
|
|
170
|
+
transactionIndex: "0x0",
|
|
171
|
+
blockHash: "0xdef",
|
|
172
|
+
logIndex: "0x1",
|
|
173
|
+
removed: false,
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
|
|
177
|
+
topics: [
|
|
178
|
+
"0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c",
|
|
179
|
+
"0x0000000000000000000000001111111111111111111111111111111111111111",
|
|
180
|
+
],
|
|
181
|
+
data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
|
|
182
|
+
blockNumber: "0x1234",
|
|
183
|
+
transactionHash: "0xabc",
|
|
184
|
+
transactionIndex: "0x0",
|
|
185
|
+
blockHash: "0xdef",
|
|
186
|
+
logIndex: "0x2",
|
|
187
|
+
removed: false,
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
const result = await decodeEvents(logs, mockResolver);
|
|
191
|
+
expect(result.decoded.length).toBe(2);
|
|
192
|
+
expect(result.undecodedCount).toBe(1);
|
|
193
|
+
expect(result.decoded[0].name).toBe("Transfer");
|
|
194
|
+
expect(result.decoded[1].name).toBe("Deposit");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|