Skip to content

Commit

Permalink
Etherscan Plugin (#206)
Browse files Browse the repository at this point in the history
* feat: Add Etherscan plugin with all endpoints

Co-Authored-By: [email protected] <[email protected]>

* chore: update pnpm lockfile

Co-Authored-By: [email protected] <[email protected]>

* fix: update tsconfig extends path

Co-Authored-By: [email protected] <[email protected]>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: [email protected] <[email protected]>
  • Loading branch information
devin-ai-integration[bot] and joycieland authored Jan 14, 2025
1 parent 25dc4a7 commit f344c8b
Show file tree
Hide file tree
Showing 10 changed files with 576 additions and 0 deletions.
67 changes: 67 additions & 0 deletions typescript/packages/plugins/etherscan/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# GOAT Etherscan Plugin

This plugin enables your agent to interact with the Ethereum blockchain data from Etherscan.

## Installation

```bash
pnpm add @goat-sdk/plugin-etherscan
```

## Configuration

To use the Etherscan plugin, you'll need an API key from [Etherscan](https://etherscan.io/apis).

```typescript
import { etherscan } from "@goat-sdk/plugin-etherscan";

const tools = getOnChainTools({
plugins: [
etherscan({
apiKey: "YOUR_ETHERSCAN_API_KEY",
}),
],
});
```

## Features

- Account balance and transaction history
- Contract ABI and source code retrieval
- Transaction status and receipt information
- Block data
- Token balances and transfers
- Gas price tracking
- Event logs

## Network Support

The plugin supports both mainnet and testnet networks:
- Ethereum Mainnet
- Goerli Testnet
- Sepolia Testnet

## Example Usage

```typescript
// Get account balance
const balance = await tools.get_account_balance({
address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
network: "mainnet",
});

// Get contract ABI
const abi = await tools.get_contract_abi({
address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
network: "mainnet",
});

// Get gas price
const gasPrice = await tools.get_gas_price({
network: "mainnet",
});
```

## API Reference

For detailed API documentation, please refer to the TypeScript types and JSDoc comments in the source code.
32 changes: 32 additions & 0 deletions typescript/packages/plugins/etherscan/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@goat-sdk/plugin-etherscan",
"version": "0.1.0",
"files": ["dist/**/*", "README.md", "package.json"],
"scripts": {
"build": "tsup",
"clean": "rm -rf dist",
"test": "vitest run --passWithNoTests"
},
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"dependencies": {
"@goat-sdk/core": "workspace:*",
"reflect-metadata": "^0.1.13",
"zod": "catalog:"
},
"peerDependencies": {
"@goat-sdk/core": "workspace:*"
},
"homepage": "https://ohmygoat.dev",
"repository": {
"type": "git",
"url": "git+https://github.com/goat-sdk/goat.git"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/goat-sdk/goat/issues"
},
"keywords": ["ai", "agents", "web3", "blockchain", "etherscan"]
}
20 changes: 20 additions & 0 deletions typescript/packages/plugins/etherscan/src/etherscan.plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Chain, PluginBase, WalletClientBase } from "@goat-sdk/core";
import { EtherscanService } from "./etherscan.service";

interface EtherscanPluginOptions {
apiKey: string;
}

export class EtherscanPlugin extends PluginBase<WalletClientBase> {
constructor({ apiKey }: EtherscanPluginOptions) {
super("etherscan", [new EtherscanService(apiKey)]);
}

supportsChain(chain: Chain): boolean {
return chain.type === "evm";
}
}

export function etherscan(options: EtherscanPluginOptions) {
return new EtherscanPlugin(options);
}
257 changes: 257 additions & 0 deletions typescript/packages/plugins/etherscan/src/etherscan.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import "reflect-metadata";
import { Tool, WalletClientBase } from "@goat-sdk/core";
import {
AccountBalanceParameters,
AccountTransactionsParameters,
BlockByNumberParameters,
ContractABIParameters,
ContractSourceCodeParameters,
EventLogsParameters,
GasPriceParameters,
TokenBalanceParameters,
TransactionReceiptParameters,
TransactionStatusParameters,
} from "./parameters";
import { buildUrl, etherscanRequest } from "./request";

// Common response types
export interface AccountBalance {
status: string;
message: string;
result: string;
}

export interface TransactionList {
status: string;
message: string;
result: Array<{
blockNumber: string;
timeStamp: string;
hash: string;
from: string;
to: string;
value: string;
gas: string;
gasPrice: string;
}>;
}

// Types for better type safety
export type EtherscanResponse<T> = Promise<T>;

export class EtherscanService {
constructor(private readonly apiKey: string) {}

@Tool({
name: "get_account_balance",
description: "Get account balance from Etherscan",
})
async getAccountBalance(
_walletClient: WalletClientBase,
parameters: AccountBalanceParameters,
): EtherscanResponse<AccountBalance> {
const { address, tag, network } = parameters;
return etherscanRequest(
buildUrl(network, "account", "balance", {
address,
tag,
}),
this.apiKey,
);
}

@Tool({
name: "get_account_transactions",
description: "Get account transactions from Etherscan",
})
async getAccountTransactions(
_walletClient: WalletClientBase,
parameters: AccountTransactionsParameters,
): EtherscanResponse<TransactionList> {
const { address, startBlock, endBlock, page, offset, sort, network } = parameters;
return etherscanRequest(
buildUrl(network, "account", "txlist", {
address,
startblock: startBlock,
endblock: endBlock,
page,
offset,
sort,
}),
this.apiKey,
);
}

@Tool({
name: "get_contract_abi",
description: "Get contract ABI from Etherscan",
})
async getContractABI(
_walletClient: WalletClientBase,
parameters: ContractABIParameters,
): EtherscanResponse<string> {
const { address, network } = parameters;
return etherscanRequest(
buildUrl(network, "contract", "getabi", {
address,
}),
this.apiKey,
);
}

@Tool({
name: "get_contract_source_code",
description: "Get contract source code from Etherscan",
})
async getContractSourceCode(
_walletClient: WalletClientBase,
parameters: ContractSourceCodeParameters,
): EtherscanResponse<string> {
const { address, network } = parameters;
return etherscanRequest(
buildUrl(network, "contract", "getsourcecode", {
address,
}),
this.apiKey,
);
}

@Tool({
name: "get_transaction_status",
description: "Get transaction status from Etherscan",
})
async getTransactionStatus(
_walletClient: WalletClientBase,
parameters: TransactionStatusParameters,
): EtherscanResponse<{ status: string; message: string; result: { status: string } }> {
const { txhash, network } = parameters;
return etherscanRequest(
buildUrl(network, "transaction", "getstatus", {
txhash,
}),
this.apiKey,
);
}

@Tool({
name: "get_transaction_receipt",
description: "Get transaction receipt from Etherscan",
})
async getTransactionReceipt(
_walletClient: WalletClientBase,
parameters: TransactionReceiptParameters,
): EtherscanResponse<{ status: string; message: string; result: { status: string } }> {
const { txhash, network } = parameters;
return etherscanRequest(
buildUrl(network, "transaction", "gettxreceiptstatus", {
txhash,
}),
this.apiKey,
);
}

@Tool({
name: "get_block_by_number",
description: "Get block by number from Etherscan",
})
async getBlockByNumber(
_walletClient: WalletClientBase,
parameters: BlockByNumberParameters,
): EtherscanResponse<{
jsonrpc: string;
id: number;
result: {
number: string;
hash: string;
parentHash: string;
transactions: string[];
timestamp: string;
nonce: string;
difficulty: string;
gasLimit: string;
gasUsed: string;
miner: string;
extraData: string;
size: string;
};
}> {
const { blockNumber, network } = parameters;
return etherscanRequest(
buildUrl(network, "proxy", "eth_getBlockByNumber", {
tag: typeof blockNumber === "number" ? `0x${blockNumber.toString(16)}` : blockNumber,
boolean: true,
}),
this.apiKey,
);
}

@Tool({
name: "get_token_balance",
description: "Get token balance from Etherscan",
})
async getTokenBalance(
_walletClient: WalletClientBase,
parameters: TokenBalanceParameters,
): EtherscanResponse<{ status: string; message: string; result: string }> {
const { address, contractAddress, tag, network } = parameters;
return etherscanRequest(
buildUrl(network, "account", "tokenbalance", {
address,
contractaddress: contractAddress,
tag,
}),
this.apiKey,
);
}

@Tool({
name: "get_gas_price",
description: "Get current gas price from Etherscan",
})
async getGasPrice(
_walletClient: WalletClientBase,
parameters: GasPriceParameters,
): EtherscanResponse<{ jsonrpc: string; id: number; result: string }> {
const { network } = parameters;
return etherscanRequest(buildUrl(network, "proxy", "eth_gasPrice", {}), this.apiKey);
}

@Tool({
name: "get_event_logs",
description: "Get event logs from Etherscan",
})
async getEventLogs(
_walletClient: WalletClientBase,
parameters: EventLogsParameters,
): EtherscanResponse<{
status: string;
message: string;
result: Array<{
address: string;
topics: string[];
data: string;
blockNumber: string;
timeStamp: string;
gasPrice: string;
gasUsed: string;
logIndex: string;
transactionHash: string;
transactionIndex: string;
blockHash: string;
}>;
}> {
const { address, fromBlock, toBlock, topic0, topic1, topic2, topic3, network } = parameters;
const params: Record<string, string | number> = {
address,
fromBlock: typeof fromBlock === "number" ? `0x${fromBlock.toString(16)}` : fromBlock,
toBlock: typeof toBlock === "number" ? `0x${toBlock.toString(16)}` : toBlock,
};

if (topic0) params.topic0 = topic0;
if (topic1) params.topic1 = topic1;
if (topic2) params.topic2 = topic2;
if (topic3) params.topic3 = topic3;

return etherscanRequest(buildUrl(network, "logs", "getLogs", params), this.apiKey);
}
}
4 changes: 4 additions & 0 deletions typescript/packages/plugins/etherscan/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./etherscan.plugin";
export * from "./etherscan.service";
export * from "./parameters";
export * from "./request";
Loading

0 comments on commit f344c8b

Please sign in to comment.