diff --git a/.env.example b/.env.example index 5784fb517e..45a49175de 100644 --- a/.env.example +++ b/.env.example @@ -601,6 +601,12 @@ CRONOSZKEVM_PRIVATE_KEY= # Fuel Ecosystem (FuelVM) FUEL_WALLET_PRIVATE_KEY= +# Taiko Plugin Configuration +TAIKO_PRIVATE_KEY= # private key of the account to use +GOLDRUSH_API_KEY= # goldrush api key for contract analytics +TAIKO_PROVIDER_URL= # optional rpc url + + # Tokenizer Settings TOKENIZER_MODEL= # Specify the tokenizer model to be used. TOKENIZER_TYPE= # Options: tiktoken (for OpenAI models) or auto (AutoTokenizer from Hugging Face for non-OpenAI models). Default: tiktoken. diff --git a/agent/package.json b/agent/package.json index ca2910ea08..181605c497 100644 --- a/agent/package.json +++ b/agent/package.json @@ -152,6 +152,7 @@ "@elizaos/plugin-near": "workspace:*", "@elizaos/plugin-stargaze": "workspace:*", "@elizaos/plugin-zksync-era": "workspace:*", + "@elizaos/plugin-taiko": "workspace:*", "readline": "1.3.0", "ws": "8.18.0", "yargs": "17.7.2" diff --git a/packages/plugin-taiko/README.md b/packages/plugin-taiko/README.md new file mode 100644 index 0000000000..d3194e3751 --- /dev/null +++ b/packages/plugin-taiko/README.md @@ -0,0 +1,108 @@ +# `@elizaos/plugin-taiko` + +## Description + +The Taiko Plugin provides integration with the Taiko Layer 2 network and Taiko Hekla Testnet, offering essential tools for onchain interactions. + +## Features + +- Send native tokens and ERC20 tokens +- Onchain name resolution +- Monitor wallet balances in real-time +- Track on-chain analytics for any address +- Configure custom RPC endpoints + +## Installation + +```bash +pnpm install @elizaos/plugin-taiko +``` + +## Configuration + +### Required Environment Variables + +```env +# Required +TAIKO_PRIVATE_KEY= # Your Private Key +GOLDRUSH_API_KEY= # Request an API key from https://goldrush.dev/platform/ + +# Optional - Custom RPC URLs +TAIKO_PROVIDER_URL=https://your-custom-mainnet-rpc-url +``` + +### Chains Supported + +By default, both **Taiko Alethia Mainnet** and **Taiko Hekla Testnet** are enabled. + +## Actions + +### 1. Transfer + +Transfer native tokens and ERC20 tokens on Taiko L2: + +> **Note:** This action supports domain name resolution for most name systems, including ENS and Unstoppable Domains. Check out the [Web3 Name SDK](https://goldrush.dev/platform/) for more information about name resolution. + +```typescript +Transfer 1 ETH to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e +``` + +```typescript +Send 1 TAIKO to vitalik.eth on Taiko Hekla +``` + +### 2. Balance Retrieval + +Fetch balances of ETH and ERC20 tokens: + +> **Note:** This action supports domain name resolution for most name systems, including ENS and Unstoppable Domains. Check out the [Web3 Name SDK](https://goldrush.dev/platform/) for more information about name resolution. + +```typescript +Can you tell me how much USDC does vitalik.eth have on Taiko? +``` + +```typescript +How much ETH does 0x123abc have on Taiko Hekla? +``` + +### 3. Onchain Analytics + +Track detailed on-chain metrics for any address on Taiko L2, including: + +- Total gas spent +- Transaction count +- Top interacted addresses +- Unique address interactions + +Available time frames: 1 day, 7 days, and 30 + +> **Tip:** This action is useful to understand the performance of a given smart contract on Taiko + +```typescript +Show me the onchain analytics for 0x1231223 on Taiko Hekla. +``` + +## Development + +1. Clone the repository +2. Install dependencies: + +```bash +pnpm install +``` + +3. Build the plugin: + +```bash +pnpm run build +``` + +4. Run tests: + +```bash +pnpm test +``` + +## License + +This plugin is part of the Eliza project. See the main project repository for license information. diff --git a/packages/plugin-taiko/package.json b/packages/plugin-taiko/package.json new file mode 100644 index 0000000000..9cee7f338f --- /dev/null +++ b/packages/plugin-taiko/package.json @@ -0,0 +1,40 @@ +{ + "name": "@elizaos/plugin-taiko", + "version": "0.1.9", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "files": [ + "dist" + ], + "dependencies": { + "@elizaos/core": "workspace:*", + "@lifi/sdk": "3.4.1", + "@web3-name-sdk/core": "^0.3.2", + "tsup": "8.3.5", + "viem": "2.21.58", + "zod": "^3.22.4" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "lint": "eslint --fix --cache .", + "test": "vitest run" + }, + "peerDependencies": { + "whatwg-url": "7.1.0" + }, + "devDependencies": { + "@types/node": "^20.0.0" + } +} diff --git a/packages/plugin-taiko/src/actions/balance.ts b/packages/plugin-taiko/src/actions/balance.ts new file mode 100644 index 0000000000..4849d3b7ca --- /dev/null +++ b/packages/plugin-taiko/src/actions/balance.ts @@ -0,0 +1,128 @@ +import { + composeContext, + elizaLogger, + generateObjectDeprecated, + type HandlerCallback, + ModelClass, + type IAgentRuntime, + type Memory, + type State, +} from "@elizaos/core"; + +import { taikoWalletProvider, initWalletProvider } from "../providers/wallet"; +import { getBalanceTemplate, transferTemplate } from "../templates"; +import type { GetBalanceParams } from "../types"; + +import { BalanceAction } from "../services/balance"; + +export const balanceAction = { + name: "balance", + description: "retrieve balance of a token for a given address.", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: Record, + callback?: HandlerCallback + ) => { + elizaLogger.log("Starting transfer action..."); + + // Initialize or update state + let currentState = state; + if (!currentState) { + currentState = (await runtime.composeState(message)) as State; + } else { + currentState = await runtime.updateRecentMessageState(currentState); + } + state.walletInfo = await taikoWalletProvider.get( + runtime, + message, + currentState + ); + + const balanceContext = composeContext({ + state: currentState, + template: getBalanceTemplate, + }); + const content = await generateObjectDeprecated({ + runtime, + context: balanceContext, + modelClass: ModelClass.LARGE, + }); + + const walletProvider = initWalletProvider(runtime); + const action = new BalanceAction(walletProvider); + const paramOptions: GetBalanceParams = { + chain: content.chain, + token: content.token, + address: content.address, + }; + + try { + const resp = await action.balance(paramOptions); + callback?.({ + text: `${resp.address} has ${resp.balance.amount} ${ + resp.balance.token + } in ${resp.chain === "taikoHekla" ? "Taiko Hekla" : "Taiko"}.`, + content: { ...resp }, + }); + + return true; + } catch (error) { + elizaLogger.error("Error during fetching balance:", error.message); + callback?.({ + text: `Fetching failed: ${error.message}`, + content: { error: error.message }, + }); + return false; + } + }, + template: transferTemplate, + validate: async (runtime: IAgentRuntime) => { + const privateKey = runtime.getSetting("TAIKO_PRIVATE_KEY"); + return typeof privateKey === "string" && privateKey.startsWith("0x"); + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "How much USDC does siddesh.eth have in Taiko?", + }, + }, + { + user: "{{agent}}", + content: { + text: "I'll find how much USDC does siddesh.eth have in Taiko", + action: "GET_BALANCE", + content: { + chain: "taiko", + token: "USDC", + address: "siddesh.eth", + }, + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Tell how many ETH does 0x742d35Cc6634C0532925a3b844Bc454e4438f44e have in Taiko Hekla.", + }, + }, + { + user: "{{agent}}", + content: { + text: "Sure, Let me find the balance of ETH for 0x742d35Cc6634C0532925a3b844Bc454e4438f44e on Taiko Hekla", + action: "GET_BALANCE", + content: { + chain: "taikoHekla", + token: "ETH", + address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + }, + }, + }, + ], + ], + similes: ["GET_BALANCE", "BALANCE"], +}; diff --git a/packages/plugin-taiko/src/actions/onchainAnalytics.ts b/packages/plugin-taiko/src/actions/onchainAnalytics.ts new file mode 100644 index 0000000000..e2ab1d36f3 --- /dev/null +++ b/packages/plugin-taiko/src/actions/onchainAnalytics.ts @@ -0,0 +1,119 @@ +import { + composeContext, + elizaLogger, + generateObjectDeprecated, + type HandlerCallback, + ModelClass, + type IAgentRuntime, + type Memory, + type State, +} from "@elizaos/core"; + +import { onchainAnalyticsTemplate } from "../templates"; +import { OnchainAnalyticsAction } from "../services/onchainAnalytics"; +import { validateTaikoConfig } from "../environment"; +import { formatAnalysisResults } from "../utils"; + +export const onchainAnalyticsAction = { + name: "onchainAnalytics", + description: + "Gives an overview of a given address in terms onchain activity.", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: Record, + callback?: HandlerCallback + ) => { + elizaLogger.log("Starting a onchain analytics research action"); + + const context = composeContext({ + state: state, + template: onchainAnalyticsTemplate, + }); + const content = await generateObjectDeprecated({ + runtime, + context, + modelClass: ModelClass.LARGE, + }); + const config = await validateTaikoConfig(runtime); + const action = new OnchainAnalyticsAction(config.GOLDRUSH_API_KEY); + const paramOptions: any = { + chainName: content.chain, + contractAddress: content.contractAddress, + }; + + try { + const resp = await action.getOnchainAnalytics(paramOptions); + callback?.({ + text: `Here you go,\n ${formatAnalysisResults(resp)}`, + content: { ...resp }, + }); + + return true; + } catch (error) { + elizaLogger.error( + "Error during fetching analytics:", + error.message + ); + callback?.({ + text: `Analytics Process failed: ${error.message}`, + content: { error: error.message }, + }); + return false; + } + }, + template: onchainAnalyticsTemplate, + validate: async (runtime: IAgentRuntime) => { + return true; + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Show some contract metrics for 0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + }, + }, + { + user: "{{agent}}", + content: { + text: "I'll find out and show the contract metrics for 0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + action: "ONCHAIN_ANALYTICS", + content: { + chain: "taiko", + contractAddress: + "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + }, + }, + }, + ], + + [ + { + user: "{{user1}}", + content: { + text: "Tell me about this contract 0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + }, + }, + { + user: "{{agent}}", + content: { + text: "I'll find the metrics for 0x742d35Cc6634C0532925a3b844Bc454e4438f44e on Taiko", + action: "ONCHAIN_ANALYTICS", + content: { + chain: "taiko", + contractAddress: + "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + }, + }, + }, + ], + ], + similes: [ + "ONCHAIN_ANALYTICS", + "GET_CONTRACT_ANALYTICS", + "GAS_SPENT", + "TOTAL_TRANSACTIONS", + ], +}; diff --git a/packages/plugin-taiko/src/actions/transfer.ts b/packages/plugin-taiko/src/actions/transfer.ts new file mode 100644 index 0000000000..83c616f930 --- /dev/null +++ b/packages/plugin-taiko/src/actions/transfer.ts @@ -0,0 +1,142 @@ +import { + composeContext, + elizaLogger, + generateObjectDeprecated, + type HandlerCallback, + ModelClass, + type IAgentRuntime, + type Memory, + type State, +} from "@elizaos/core"; + +import { taikoWalletProvider, initWalletProvider } from "../providers/wallet"; +import { transferTemplate } from "../templates"; +import type { TransferParams } from "../types"; +import { TransferAction } from "../services/transfer"; + +export const transferAction = { + name: "transfer", + description: + "Transfer Native tokens and ERC20 tokens between addresses on Taiko", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: Record, + callback?: HandlerCallback + ) => { + elizaLogger.log("Starting transfer action..."); + + // Validate transfer + if (!(message.content.source === "direct")) { + callback?.({ + text: "I can't do that for you.", + content: { error: "Transfer not allowed" }, + }); + return false; + } + + // Initialize or update state + let currentState = state; + if (!currentState) { + currentState = (await runtime.composeState(message)) as State; + } else { + currentState = await runtime.updateRecentMessageState(currentState); + } + state.walletInfo = await taikoWalletProvider.get( + runtime, + message, + currentState + ); + + // Compose transfer context + const transferContext = composeContext({ + state: currentState, + template: transferTemplate, + }); + const content = await generateObjectDeprecated({ + runtime, + context: transferContext, + modelClass: ModelClass.LARGE, + }); + + const walletProvider = initWalletProvider(runtime); + const action = new TransferAction(walletProvider); + const paramOptions: TransferParams = { + chain: content.chain, + token: content.token, + amount: content.amount, + toAddress: content.toAddress, + data: content.data, + }; + + try { + const transferResp = await action.transfer(paramOptions); + const explorerUrl = + walletProvider.getCurrentChain().blockExplorers.default.url; + callback?.({ + text: `Successfully transferred ${transferResp.amount} ${transferResp.token} to ${transferResp.recipient}\n\nLink to explorer: ${explorerUrl}/tx/${transferResp.txHash}`, + content: { ...transferResp }, + }); + + return true; + } catch (error) { + elizaLogger.error("Error during transfer:", error.message); + callback?.({ + text: `Transfer failed: ${error.message}`, + content: { error: error.message }, + }); + return false; + } + }, + template: transferTemplate, + validate: async (runtime: IAgentRuntime) => { + const privateKey = runtime.getSetting("TAIKO_PRIVATE_KEY"); + return typeof privateKey === "string" && privateKey.startsWith("0x"); + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Transfer 1 ETH to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + }, + }, + { + user: "{{agent}}", + content: { + text: "I'll help you transfer 1 ETH to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e on ETH", + action: "TRANSFER", + content: { + chain: "taiko", + token: "ETH", + amount: "1", + toAddress: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + }, + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Transfer 1 token of 0x1234 to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + }, + }, + { + user: "{{agent}}", + content: { + text: "I'll help you transfer 1 token of 0x1234 to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e on TAIKO", + action: "TRANSFER", + content: { + chain: "taiko", + token: "0x1234", + amount: "1", + toAddress: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + }, + }, + }, + ], + ], + similes: ["TRANSFER", "SEND_TOKENS", "TOKEN_TRANSFER", "MOVE_TOKENS"], +}; diff --git a/packages/plugin-taiko/src/environment.ts b/packages/plugin-taiko/src/environment.ts new file mode 100644 index 0000000000..cdcb9d6952 --- /dev/null +++ b/packages/plugin-taiko/src/environment.ts @@ -0,0 +1,32 @@ +import { IAgentRuntime } from "@elizaos/core"; +import { z } from "zod"; + +export const taikoEnvSchema = z.object({ + GOLDRUSH_API_KEY: z.string().min(1, "Goldrush API key is required"), + TAIKO_PRIVATE_KEY: z.string().min(1, "TAIKO private key is required"), +}); + +export type taikoConfig = z.infer; + +export async function validateTaikoConfig( + runtime: IAgentRuntime +): Promise { + try { + const config = { + GOLDRUSH_API_KEY: runtime.getSetting("GOLDRUSH_API_KEY"), + TAIKO_PRIVATE_KEY: runtime.getSetting("TAIKO_PRIVATE_KEY"), + }; + + return taikoEnvSchema.parse(config); + } catch (error) { + if (error instanceof z.ZodError) { + const errorMessages = error.errors + .map((err) => `${err.path.join(".")}: ${err.message}`) + .join("\n"); + throw new Error( + `Taiko configuration validation failed:\n${errorMessages}` + ); + } + throw error; + } +} diff --git a/packages/plugin-taiko/src/index.ts b/packages/plugin-taiko/src/index.ts new file mode 100644 index 0000000000..f2f00bce48 --- /dev/null +++ b/packages/plugin-taiko/src/index.ts @@ -0,0 +1,14 @@ +import { Plugin } from "@elizaos/core"; +import { taikoWalletProvider } from "./providers/wallet"; +import { transferAction } from "./actions/transfer"; +import { balanceAction } from "./actions/balance"; +import { onchainAnalyticsAction } from "./actions/onchainAnalytics"; + +export const taikoPlugin: Plugin = { + name: "taiko", + description: "Taiko plugin for Eliza", + actions: [transferAction, onchainAnalyticsAction, balanceAction], + evaluators: [], + providers: [taikoWalletProvider], +}; +export default taikoPlugin; diff --git a/packages/plugin-taiko/src/providers/wallet.ts b/packages/plugin-taiko/src/providers/wallet.ts new file mode 100644 index 0000000000..4e72909304 --- /dev/null +++ b/packages/plugin-taiko/src/providers/wallet.ts @@ -0,0 +1,299 @@ +import type { IAgentRuntime, Provider, Memory, State } from "@elizaos/core"; +import type { + Address, + WalletClient, + PublicClient, + Chain, + HttpTransport, + Account, + PrivateKeyAccount, + Hex, + ByteArray, +} from "viem"; +import { + createPublicClient, + createWalletClient, + erc20Abi, + formatUnits, + http, +} from "viem"; +import { getToken } from "@lifi/sdk"; +import { createWeb3Name } from "@web3-name-sdk/core"; +import { privateKeyToAccount } from "viem/accounts"; +import * as viemChains from "viem/chains"; + +import type { SupportedChain } from "../types"; + +export class WalletProvider { + private currentChain: SupportedChain = "taiko"; + chains: Record = { + taiko: viemChains.taiko, + taikoHekla: viemChains.taikoHekla, + }; + account: PrivateKeyAccount; + + constructor(privateKey: `0x${string}`, chains?: Record) { + this.setAccount(privateKey); + this.setChains(chains); + + if (chains && Object.keys(chains).length > 0) { + this.setCurrentChain(Object.keys(chains)[0] as SupportedChain); + } + } + + getAccount(): PrivateKeyAccount { + return this.account; + } + + getAddress(): Address { + return this.account.address; + } + + getCurrentChain(): Chain { + return this.chains[this.currentChain]; + } + + getPublicClient( + chainName: SupportedChain + ): PublicClient { + const transport = this.createHttpTransport(chainName); + + const publicClient = createPublicClient({ + chain: this.chains[chainName], + transport, + }); + return publicClient; + } + + getWalletClient(chainName: SupportedChain): WalletClient { + const transport = this.createHttpTransport(chainName); + + const walletClient = createWalletClient({ + chain: this.chains[chainName], + transport, + account: this.account, + }); + + return walletClient; + } + + getChainConfigs(chainName: SupportedChain): Chain { + const chain = viemChains[chainName]; + + if (!chain?.id) { + throw new Error("Invalid chain name"); + } + + return chain; + } + + async getBalance(): Promise { + const client = this.getPublicClient(this.currentChain); + const balance = await client.getBalance({ + address: this.account.address, + }); + return formatUnits(balance, 18); + } + + addChain(chain: Record) { + this.setChains(chain); + } + + switchChain(chainName: SupportedChain, customRpcUrl?: string) { + if (!this.chains[chainName]) { + const chain = WalletProvider.genChainFromName( + chainName, + customRpcUrl + ); + this.addChain({ [chainName]: chain }); + } + this.setCurrentChain(chainName); + } + + async formatAddress(address: string): Promise
{ + if (!address || address.length === 0) { + throw new Error("Empty address"); + } + + if (address.startsWith("0x") && address.length === 42) { + return address as Address; + } + + const resolvedAddress = await this.resolveWeb3Name(address); + if (resolvedAddress) { + return resolvedAddress as Address; + } + throw new Error("Invalid address"); + } + + async resolveWeb3Name(name: string): Promise { + const nameService = createWeb3Name(); + return await nameService.getAddress(name); + } + + async getTokenAddress( + chainName: SupportedChain, + tokenSymbol: string + ): Promise { + const token = await getToken( + this.getChainConfigs(chainName).id, + tokenSymbol + ); + return token.address; + } + + async transfer( + chain: SupportedChain, + toAddress: Address, + amount: bigint, + options?: { + gas?: bigint; + gasPrice?: bigint; + data?: Hex; + } + ): Promise { + const walletClient = this.getWalletClient(chain); + return await walletClient.sendTransaction({ + account: this.account, + to: toAddress, + value: amount, + chain: this.getChainConfigs(chain), + kzg: { + blobToKzgCommitment: (_: ByteArray): ByteArray => { + throw new Error("Function not implemented."); + }, + computeBlobKzgProof: ( + _blob: ByteArray, + _commitment: ByteArray + ): ByteArray => { + throw new Error("Function not implemented."); + }, + }, + ...options, + }); + } + + async transferERC20( + chain: SupportedChain, + tokenAddress: Address, + toAddress: Address, + amount: bigint, + options?: { + gas?: bigint; + gasPrice?: bigint; + } + ): Promise { + const publicClient = this.getPublicClient(chain); + const walletClient = this.getWalletClient(chain); + const { request } = await publicClient.simulateContract({ + account: this.account, + address: tokenAddress as `0x${string}`, + abi: erc20Abi, + functionName: "transfer", + args: [toAddress as `0x${string}`, amount], + ...options, + }); + + return await walletClient.writeContract(request); + } + + private setAccount = (pk: `0x${string}`) => { + this.account = privateKeyToAccount(pk); + }; + + private setChains = (chains?: Record) => { + if (!chains) { + return; + } + for (const chain of Object.keys(chains)) { + this.chains[chain] = chains[chain]; + } + }; + + private setCurrentChain = (chain: SupportedChain) => { + this.currentChain = chain; + }; + + private createHttpTransport = (chainName: SupportedChain) => { + const chain = this.chains[chainName]; + + if (chain.rpcUrls.custom) { + return http(chain.rpcUrls.custom.http[0]); + } + return http(chain.rpcUrls.default.http[0]); + }; + + static genChainFromName( + chainName: string, + customRpcUrl?: string | null + ): Chain { + const baseChain = viemChains[chainName]; + + if (!baseChain?.id) { + throw new Error("Invalid chain name"); + } + + const viemChain: Chain = customRpcUrl + ? { + ...baseChain, + rpcUrls: { + ...baseChain.rpcUrls, + custom: { + http: [customRpcUrl], + }, + }, + } + : baseChain; + + return viemChain; + } +} + +const genChainsFromRuntime = ( + runtime: IAgentRuntime +): Record => { + const chainNames = ["taiko", "taikoHekla"]; + const chains = {}; + + for (const chainName of chainNames) { + const chain = WalletProvider.genChainFromName(chainName); + chains[chainName] = chain; + } + const mainnet_rpcurl = runtime.getSetting("TAIKO_PROVIDER_URL"); + if (mainnet_rpcurl) { + const chain = WalletProvider.genChainFromName("taiko", mainnet_rpcurl); + chains["taiko"] = chain; + } + + return chains; +}; + +export const initWalletProvider = (runtime: IAgentRuntime) => { + const privateKey = runtime.getSetting("TAIKO_PRIVATE_KEY"); + if (!privateKey) { + throw new Error("TAIKO_PRIVATE_KEY is missing"); + } + + const chains = genChainsFromRuntime(runtime); + + return new WalletProvider(privateKey as `0x${string}`, chains); +}; + +export const taikoWalletProvider: Provider = { + async get( + runtime: IAgentRuntime, + _message: Memory, + _state?: State + ): Promise { + try { + const walletProvider = initWalletProvider(runtime); + const address = walletProvider.getAddress(); + const balance = await walletProvider.getBalance(); + const chain = walletProvider.getCurrentChain(); + return `Taiko chain Wallet Address: ${address}\nBalance: ${balance} ${chain.nativeCurrency.symbol}\nChain ID: ${chain.id}, Name: ${chain.name}`; + } catch (error) { + console.error("Error in Taiko chain wallet provider:", error); + return null; + } + }, +}; diff --git a/packages/plugin-taiko/src/services/balance.ts b/packages/plugin-taiko/src/services/balance.ts new file mode 100644 index 0000000000..6f5e8c7061 --- /dev/null +++ b/packages/plugin-taiko/src/services/balance.ts @@ -0,0 +1,83 @@ +import { erc20Abi, formatEther, formatUnits } from "viem"; +import { WalletProvider } from "../providers/wallet"; +import { GetBalanceParams, GetBalanceResponse } from "../types"; + +export class BalanceAction { + constructor(private walletProvider: WalletProvider) {} + + async balance(params: GetBalanceParams): Promise { + try { + if (!params.address) { + throw new Error("No address provided."); + } + + const { chain, token } = params; + const targetAddress = await this.walletProvider.formatAddress( + params.address + ); + const nativeToken = + this.walletProvider.chains[chain].nativeCurrency.symbol; + + this.walletProvider.switchChain(chain); + + const publicClient = this.walletProvider.getPublicClient(chain); + + const response: GetBalanceResponse = { + chain, + address: targetAddress, + balance: await this.fetchBalance({ + publicClient, + token, + nativeToken, + targetAddress, + chain, + }), + }; + + return response; + } catch (error) { + throw new Error(`Failed to fetch balance: ${error.message}`); + } + } + + private async fetchBalance({ + publicClient, + token, + nativeToken, + targetAddress, + chain, + }) { + if (!token || token === "null" || token === nativeToken) { + const nativeBalanceWei = await publicClient.getBalance({ + address: targetAddress, + }); + return { + token: nativeToken, + amount: formatEther(nativeBalanceWei), + }; + } + + const tokenAddress = token.startsWith("0x") + ? token + : await this.walletProvider.getTokenAddress(chain, token); + + const [balance, decimals] = await Promise.all([ + publicClient.readContract({ + address: tokenAddress as `0x${string}`, + abi: erc20Abi, + functionName: "balanceOf", + args: [targetAddress], + }), + publicClient.readContract({ + address: tokenAddress as `0x${string}`, + abi: erc20Abi, + functionName: "decimals", + }), + ]); + + return { + token, + amount: formatUnits(balance, decimals), + }; + } +} diff --git a/packages/plugin-taiko/src/services/onchainAnalytics.ts b/packages/plugin-taiko/src/services/onchainAnalytics.ts new file mode 100644 index 0000000000..cfc01a81b7 --- /dev/null +++ b/packages/plugin-taiko/src/services/onchainAnalytics.ts @@ -0,0 +1,204 @@ +import type { + ContractTransactionsParams, + ContractTransactionsResponse, + TransactionAnalysis, +} from "../types"; + +export class OnchainAnalyticsAction { + constructor(private goldrushAPIKey: string) { + if (!goldrushAPIKey) { + throw new Error("Goldrush API key is required"); + } + } + async getOnchainAnalytics(params: ContractTransactionsParams) { + if (!params.contractAddress?.startsWith("0x")) { + throw new Error("Contract address must start with '0x'"); + } + + const transactions = await this.fetchTaikoTransactions(params); + + if (!transactions) { + throw new Error("Failed to fetch transaction data"); + } + + return this.analyzeTransactions(transactions); + } + + async fetchTaikoTransactions( + params: ContractTransactionsParams + ): Promise { + const chainId = + params.chainName === "taikoHekla" + ? "taiko-hekla-testnet" + : "taiko-mainnet"; + + const GOLDRUSH_API = `https://api.covalenthq.com/v1/${chainId}/address/${params.contractAddress}/transactions_v2/?page-size=1000`; + + try { + const response = await fetch(GOLDRUSH_API, { + headers: { + Authorization: `Bearer ${this.goldrushAPIKey}`, + }, + }); + + if (!response.ok) { + throw new Error( + `API request failed: ${response.status} ${response.statusText}` + ); + } + + const data = await response.json(); + return data as ContractTransactionsResponse; + } catch (error) { + console.error("Error fetching Taiko transactions:", error); + return null; + } + } + + analyzeTransactions( + data: ContractTransactionsResponse + ): TransactionAnalysis { + if (!data?.data?.items?.length) { + return this.getEmptyAnalysis(); + } + + const now = new Date(); + const oneDayAgo = new Date(); + oneDayAgo.setDate(now.getDate() - 1); + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(now.getDate() - 7); + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(now.getDate() - 30); + + let totalGasSpent1d = 0; + let totalGasSpent7d = 0; + let totalGasSpent30d = 0; + + let txCount1d = 0; + let txCount7d = 0; + let txCount30d = 0; + + const uniqueAddresses1d = new Set(); + const uniqueAddresses7d = new Set(); + const uniqueAddresses30d = new Set(); + + const addressInteractions1d = new Map(); + const addressInteractions7d = new Map(); + const addressInteractions30d = new Map(); + + for (const tx of data.data.items) { + const txDate = new Date(tx.block_signed_at); + + // Track 1 day data + if (txDate >= oneDayAgo) { + totalGasSpent1d += tx.gas_spent; + uniqueAddresses1d.add(tx.from_address); + uniqueAddresses1d.add(tx.to_address); + addressInteractions1d.set( + tx.from_address, + (addressInteractions1d.get(tx.from_address) || 0) + 1 + ); + addressInteractions1d.set( + tx.to_address, + (addressInteractions1d.get(tx.to_address) || 0) + 1 + ); + txCount1d++; + } + + // Track 7 days data + if (txDate >= sevenDaysAgo) { + totalGasSpent7d += tx.gas_spent; + uniqueAddresses7d.add(tx.from_address); + uniqueAddresses7d.add(tx.to_address); + addressInteractions7d.set( + tx.from_address, + (addressInteractions7d.get(tx.from_address) || 0) + 1 + ); + addressInteractions7d.set( + tx.to_address, + (addressInteractions7d.get(tx.to_address) || 0) + 1 + ); + txCount7d++; + } + + // Track 30 days data + if (txDate >= thirtyDaysAgo) { + totalGasSpent30d += tx.gas_spent; + uniqueAddresses30d.add(tx.from_address); + uniqueAddresses30d.add(tx.to_address); + addressInteractions30d.set( + tx.from_address, + (addressInteractions30d.get(tx.from_address) || 0) + 1 + ); + addressInteractions30d.set( + tx.to_address, + (addressInteractions30d.get(tx.to_address) || 0) + 1 + ); + txCount30d++; + } + } + + // Function to get top addresses by interaction count + function getTopAddresses( + addressInteractions: Map, + topN: number + ): string[] { + return Array.from(addressInteractions.entries()) + .sort((a, b) => b[1] - a[1]) // Sort by interaction count (descending) + .slice(0, topN) + .map(([address]) => address); + } + + const topAddresses1d = getTopAddresses(addressInteractions1d, 3); // Top 3 addresses for 1 day + const topAddresses7d = getTopAddresses(addressInteractions7d, 3); // Top 3 addresses for 7 days + const topAddresses30d = getTopAddresses(addressInteractions30d, 3); // Top 3 addresses for 30 days + + return { + gasSpent: { + "1d": `${totalGasSpent1d} gwei`, + "7d": `${totalGasSpent7d} gwei`, + "30d": `${totalGasSpent30d} gwei`, + }, + txCount: { + "1d": txCount1d, + "7d": txCount7d, + "30d": txCount30d, + }, + uniqueAddresses: { + "1d": Array.from(uniqueAddresses1d).length, + "7d": Array.from(uniqueAddresses7d).length, + "30d": Array.from(uniqueAddresses30d).length, + }, + topAddresses: { + "1d": topAddresses1d, + "7d": topAddresses7d, + "30d": topAddresses30d, + }, + }; + } + + private getEmptyAnalysis(): TransactionAnalysis { + return { + gasSpent: { + "1d": "0 gwei", + "7d": "0 gwei", + "30d": "0 gwei", + }, + txCount: { + "1d": 0, + "7d": 0, + "30d": 0, + }, + uniqueAddresses: { + "1d": 0, + "7d": 0, + "30d": 0, + }, + topAddresses: { + "1d": [], + "7d": [], + "30d": [], + }, + }; + } +} diff --git a/packages/plugin-taiko/src/services/transfer.ts b/packages/plugin-taiko/src/services/transfer.ts new file mode 100644 index 0000000000..e227b16e85 --- /dev/null +++ b/packages/plugin-taiko/src/services/transfer.ts @@ -0,0 +1,144 @@ +import { WalletProvider } from "../providers/wallet"; + +import type { TransferParams, TransferResponse } from "../types"; +import { + erc20Abi, + formatEther, + formatUnits, + parseEther, + parseUnits, +} from "viem"; + +export class TransferAction { + constructor(private walletProvider: WalletProvider) {} + + async transfer(params: TransferParams): Promise { + console.log("Initiating a transfer transaction in Taiko:", params); + + // Validate required parameters + if (!params.toAddress) { + throw new Error("Recipient address is missing"); + } + if (!params.chain) { + throw new Error("Chain parameter is missing"); + } + if (params.amount && isNaN(Number(params.amount))) { + throw new Error("Invalid amount provided"); + } + + const toAddress = await this.walletProvider.formatAddress( + params.toAddress + ); + const fromAddress = this.walletProvider.getAddress(); + + this.walletProvider.switchChain(params.chain); + + const nativeToken = + this.walletProvider.chains[params.chain].nativeCurrency.symbol; + + const resp: TransferResponse = { + chain: params.chain, + txHash: "0x", + recipient: toAddress, + amount: "", + token: params.token === "null" ? null : params.token ?? nativeToken, + }; + + try { + if ( + !params.token || + params.token === "null" || + params.token === nativeToken + ) { + await this.handleNativeTransfer(params, toAddress, resp); + } else { + await this.handleERC20Transfer( + params, + fromAddress, + toAddress, + resp + ); + } + + if (!resp.txHash || resp.txHash === "0x") { + throw new Error("Transaction hash is invalid"); + } + + return resp; + } catch (error) { + throw new Error(`Transfer failed: ${error.message}`); + } + } + + private async handleNativeTransfer( + params: TransferParams, + toAddress: string, + resp: TransferResponse + ): Promise { + if (!params.amount) { + throw new Error("Amount is required for native token transfer"); + } + const value = parseEther(params.amount); + resp.amount = formatEther(value); + resp.txHash = await this.walletProvider.transfer( + params.chain, + toAddress as `0x${string}`, + value + ); + } + + private async handleERC20Transfer( + params: TransferParams, + fromAddress: string, + toAddress: string, + resp: TransferResponse + ): Promise { + const tokenAddress = params.token.startsWith("0x") + ? params.token + : await this.walletProvider.getTokenAddress( + params.chain, + params.token + ); + + const publicClient = this.walletProvider.getPublicClient(params.chain); + const decimals = await publicClient.readContract({ + address: tokenAddress as `0x${string}`, + abi: erc20Abi, + functionName: "decimals", + }); + + const value = await this.getERC20TransferAmount( + publicClient, + tokenAddress, + fromAddress, + params.amount, + decimals + ); + + resp.amount = formatUnits(value, decimals); + resp.txHash = await this.walletProvider.transferERC20( + params.chain, + tokenAddress as `0x${string}`, + toAddress as `0x${string}`, + value + ); + } + + private async getERC20TransferAmount( + publicClient: any, + tokenAddress: string, + fromAddress: string, + amount: string | undefined, + decimals: number + ): Promise { + if (!amount) { + return await publicClient.readContract({ + address: tokenAddress as `0x${string}`, + abi: erc20Abi, + functionName: "balanceOf", + args: [fromAddress], + }); + } + return parseUnits(amount, decimals); + } +} diff --git a/packages/plugin-taiko/src/templates/index.ts b/packages/plugin-taiko/src/templates/index.ts new file mode 100644 index 0000000000..a024b72105 --- /dev/null +++ b/packages/plugin-taiko/src/templates/index.ts @@ -0,0 +1,65 @@ +export const transferTemplate = `Given the recent messages and wallet information below: + +{{recentMessages}} + +{{walletInfo}} + +Extract the following information about the requested transfer: +- chain: Must be one of ["taiko", "taikoHekla"]. Default: "taiko" +- token: Token symbol (e.g., "ETH") or contract address (0x-prefixed). Default: "ETH" +- amount: Positive number as string in ether units (e.g., "0.1"). Required +- toAddress: Valid Ethereum address (0x-prefixed) or web3 domain name. Required +- data: (Optional) Transaction data as hex string (0x-prefixed) + +Respond with a JSON markdown block containing only the extracted values: + +\`\`\`json +{ + "chain": "taiko" | "taikoHekla", + "token": string, + "amount": string, + "toAddress": string, + "data": string | null +} +\`\`\` +`; + +export const onchainAnalyticsTemplate = `Given the recent messages below: + +{{recentMessages}} + +Extract the following information about the smart contract to analyze: +- chain: Must be one of ["taiko", "taikoHekla"]. Default: "taiko" +- contractAddress: Valid Ethereum address (0x-prefixed). Required + +Respond with a JSON markdown block containing only the extracted values: + +\`\`\`json +{ + "chain": "taiko" | "taikoHekla", + "contractAddress": string +} +\`\`\` +`; + +export const getBalanceTemplate = `Given the recent messages and wallet information below: + +{{recentMessages}} + +{{walletInfo}} + +Extract the following information about the requested balance check: +- chain: Must be one of ["taiko", "taikoHekla"]. Default: "taiko" +- address: Valid Ethereum address (0x-prefixed) or web3 domain name. Default: Current wallet address +- token: Token symbol (e.g., "ETH") or contract address (0x-prefixed). Default: "ETH" + +Respond with a JSON markdown block containing only the extracted values: + +\`\`\`json +{ + "chain": "taiko" | "taikoHekla", + "address": string, + "token": string +} +\`\`\` +`; diff --git a/packages/plugin-taiko/src/tests/balance.test.ts b/packages/plugin-taiko/src/tests/balance.test.ts new file mode 100644 index 0000000000..49533c6635 --- /dev/null +++ b/packages/plugin-taiko/src/tests/balance.test.ts @@ -0,0 +1,58 @@ +import { describe, it, beforeEach, expect } from "vitest"; +import { + generatePrivateKey, + Account, + privateKeyToAccount, +} from "viem/accounts"; + +import { WalletProvider } from "../providers/wallet"; +import { GetBalanceParams } from "../types"; +import { BalanceAction } from "../services/balance"; + +describe("GetBalance Action", () => { + let account: Account; + let wp: WalletProvider; + let ga: BalanceAction; + + beforeEach(async () => { + const pk = generatePrivateKey(); + account = privateKeyToAccount(pk); + wp = new WalletProvider(pk); + ga = new BalanceAction(wp); + }); + + describe("Get Balance", () => { + it("get ETH balance", async () => { + const input: GetBalanceParams = { + chain: "taiko", + address: account.address, + token: "ETH", + }; + const resp = await ga.balance(input); + expect(resp.balance).toBeDefined(); + expect(typeof resp.balance).toBe("object"); + }); + + it("get TAIKO balance", async () => { + const input: GetBalanceParams = { + chain: "taiko", + address: account.address, + token: "TAIKO", + }; + const resp = await ga.balance(input); + expect(resp.balance).toBeDefined(); + expect(typeof resp.balance).toBe("object"); + }); + + it("get balance by token contract address", async () => { + const input: GetBalanceParams = { + chain: "taiko", + address: account.address, + token: "0xA9d23408b9bA935c230493c40C73824Df71A0975", + }; + const resp = await ga.balance(input); + expect(resp.balance).toBeDefined(); + expect(typeof resp.balance).toBe("object"); + }); + }); +}); diff --git a/packages/plugin-taiko/src/tests/onchainAnalytics.test.ts b/packages/plugin-taiko/src/tests/onchainAnalytics.test.ts new file mode 100644 index 0000000000..12effe0660 --- /dev/null +++ b/packages/plugin-taiko/src/tests/onchainAnalytics.test.ts @@ -0,0 +1,113 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { OnchainAnalyticsAction } from "../services/onchainAnalytics"; + +describe("OnchainAnalyticsAction", () => { + let oa: OnchainAnalyticsAction; + const GOLDRUSH_API_KEY = "goldrush-key"; + const VALID_CONTRACT_ADDRESS = "0x1234567890123456789012345678901234567890"; + + beforeEach(() => { + vi.resetAllMocks(); + oa = new OnchainAnalyticsAction(GOLDRUSH_API_KEY); + global.fetch = vi.fn(); + }); + + describe("getOnchainAnalytics", () => { + it("throws error if contract address doesn't start with 0x", async () => { + await expect( + oa.getOnchainAnalytics({ + chainName: "taikoHekla", + contractAddress: "invalid-address", + }) + ).rejects.toThrow("Contract address must start with '0x'"); + }); + + it("throws error if transaction fetch fails", async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: "Not Found", + }); + + await expect( + oa.getOnchainAnalytics({ + chainName: "taikoHekla", + contractAddress: VALID_CONTRACT_ADDRESS, + }) + ).rejects.toThrow("Failed to fetch transaction data"); + }); + + it("returns empty analysis when no transactions found", async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ data: { items: [] } }), + }); + + const result = await oa.getOnchainAnalytics({ + chainName: "taikoHekla", + contractAddress: VALID_CONTRACT_ADDRESS, + }); + + expect(result).toEqual({ + gasSpent: { "1d": "0 gwei", "7d": "0 gwei", "30d": "0 gwei" }, + txCount: { "1d": 0, "7d": 0, "30d": 0 }, + uniqueAddresses: { "1d": 0, "7d": 0, "30d": 0 }, + topAddresses: { "1d": [], "7d": [], "30d": [] }, + }); + }); + + it("correctly analyzes transaction data", async () => { + const mockDate = new Date("2025-01-15T12:00:00Z"); + vi.setSystemTime(mockDate); + + const mockTransactions = { + data: { + items: [ + { + block_signed_at: "2025-01-15T10:00:00Z", // Within 1 day + gas_spent: 1000, + from_address: "0xaddr1", + to_address: "0xaddr2", + }, + { + block_signed_at: "2025-01-10T10:00:00Z", // Within 7 days + gas_spent: 2000, + from_address: "0xaddr3", + to_address: "0xaddr4", + }, + { + block_signed_at: "2025-01-01T10:00:00Z", // Within 30 days + gas_spent: 3000, + from_address: "0xaddr5", + to_address: "0xaddr6", + }, + ], + }, + }; + + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockTransactions), + }); + + const result = await oa.getOnchainAnalytics({ + chainName: "taikoHekla", + contractAddress: VALID_CONTRACT_ADDRESS, + }); + + expect(result.gasSpent["1d"]).toBe("1000 gwei"); + expect(result.gasSpent["7d"]).toBe("3000 gwei"); + expect(result.gasSpent["30d"]).toBe("6000 gwei"); + + expect(result.txCount["1d"]).toBe(1); + expect(result.txCount["7d"]).toBe(2); + expect(result.txCount["30d"]).toBe(3); + + expect(result.uniqueAddresses["1d"]).toBe(2); + expect(result.uniqueAddresses["7d"]).toBe(4); + expect(result.uniqueAddresses["30d"]).toBe(6); + + vi.useRealTimers(); + }); + }); +}); diff --git a/packages/plugin-taiko/src/tests/transfer.test.ts b/packages/plugin-taiko/src/tests/transfer.test.ts new file mode 100644 index 0000000000..dff950741a --- /dev/null +++ b/packages/plugin-taiko/src/tests/transfer.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import type { Account } from "viem"; + +import { TransferAction } from "../services/transfer"; +import { WalletProvider } from "../providers/wallet"; + +describe("Transfer Action", () => { + let account: Account; + let wp: WalletProvider; + let tp: TransferAction; + + beforeEach(async () => { + const pk = generatePrivateKey(); + account = privateKeyToAccount(pk); + wp = new WalletProvider(pk); + tp = new TransferAction(wp); + }); + + describe("Constructor", () => { + it("should initialize with wallet provider", () => { + const ta = new TransferAction(wp); + + expect(ta).toBeDefined(); + }); + }); + describe("Transfer", () => { + let ta: TransferAction; + let receiver: Account; + + beforeEach(() => { + ta = new TransferAction(wp); + receiver = privateKeyToAccount(generatePrivateKey()); + }); + + it("throws if not enough gas", async () => { + await expect( + ta.transfer({ + chain: "taiko", + toAddress: receiver.address, + amount: "1", + }) + ).rejects.toThrow( + "Transfer failed: The total cost (gas * gas fee + value) of executing this transaction exceeds the balance of the account." + ); + }); + }); +}); diff --git a/packages/plugin-taiko/src/tests/wallet.test.ts b/packages/plugin-taiko/src/tests/wallet.test.ts new file mode 100644 index 0000000000..c624140a34 --- /dev/null +++ b/packages/plugin-taiko/src/tests/wallet.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { + Account, + generatePrivateKey, + privateKeyToAccount, +} from "viem/accounts"; +import { taiko, taikoHekla } from "viem/chains"; + +import { WalletProvider } from "../providers/wallet"; + +describe("Wallet provider", () => { + let pk: `0x${string}`; + let account: Account; + let walletProvider: WalletProvider; + + beforeAll(() => { + pk = generatePrivateKey(); + account = privateKeyToAccount(pk); + walletProvider = new WalletProvider(pk); + }); + + describe("Constructor", () => { + it("get address", () => { + const expectedAddress = account.address; + + expect(walletProvider.getAddress()).toEqual(expectedAddress); + }); + it("get current chain", () => { + expect(walletProvider.getCurrentChain().id).toEqual(taiko.id); + }); + it("get chain configs", () => { + expect(walletProvider.getChainConfigs("taiko").id).toEqual( + taiko.id + ); + expect(walletProvider.getChainConfigs("taikoHekla").id).toEqual( + taikoHekla.id + ); + }); + }); + describe("Clients", () => { + it("generates public client", () => { + const client = walletProvider.getPublicClient("taiko"); + expect(client.chain.id).toEqual(taiko.id); + expect(client.transport.url).toEqual(taiko.rpcUrls.default.http[0]); + }); + + it("generates wallet client", () => { + const expectedAddress = account.address; + + const client = walletProvider.getWalletClient("taiko"); + + expect(client.account?.address).toEqual(expectedAddress); + expect(client.transport.url).toEqual(taiko.rpcUrls.default.http[0]); + }); + }); +}); diff --git a/packages/plugin-taiko/src/types/index.ts b/packages/plugin-taiko/src/types/index.ts new file mode 100644 index 0000000000..faa782c52b --- /dev/null +++ b/packages/plugin-taiko/src/types/index.ts @@ -0,0 +1,146 @@ +import type { Address, Hash } from "viem"; + +export type SupportedChain = "taiko" | "taikoHekla"; + +// Action parameters +export interface GetBalanceParams { + chain: SupportedChain; + address?: Address; + token: string; +} + +export interface TransferParams { + chain: SupportedChain; + token?: string; + amount?: string; + toAddress: Address; + data?: `0x${string}`; +} + +// Action return types +export interface GetBalanceResponse { + chain: SupportedChain; + address: Address; + balance?: { token: string; amount: string }; +} + +export interface TransferResponse { + chain: SupportedChain; + txHash: Hash; + recipient: Address; + amount: string; + token: string; + data?: `0x${string}`; +} + +// Contract Analytics types +interface GasMetadata { + contract_decimals: number; + contract_name: string; + contract_ticker_symbol: string; + contract_address: string; + supports_erc: string[]; + logo_url: string; +} + +interface LogEvent { + block_signed_at: string; + block_height: number; + tx_offset: number; + log_offset: number; + tx_hash: string; + raw_log_topics: string[]; + sender_contract_decimals: number | null; + sender_name: string | null; + sender_contract_ticker_symbol: string | null; + sender_address: string; + sender_address_label: string | null; + sender_logo_url: string | null; + supports_erc: string[] | null; + sender_factory_address: string | null; + raw_log_data: string; + decoded: { + name: string; + signature: string; + params: { + name: string; + type: string; + indexed: boolean; + decoded: boolean; + value: string; + }[]; + } | null; +} + +interface TransactionItem { + block_signed_at: string; + block_height: number; + block_hash: string; + tx_hash: string; + tx_offset: number; + successful: boolean; + miner_address: string; + from_address: string; + from_address_label: string | null; + to_address: string; + to_address_label: string | null; + value: string; + value_quote: number; + pretty_value_quote: string; + gas_metadata: GasMetadata; + gas_offered: number; + gas_spent: number; + gas_price: number; + fees_paid: string; + gas_quote: number; + pretty_gas_quote: string; + gas_quote_rate: number; + log_events: LogEvent[]; +} +export interface ContractTransactionsParams { + contractAddress: string; + chainName: "taiko" | "taikoHekla"; +} +export interface ContractTransactionsResponse { + data: { + address: string; + updated_at: string; + next_update_at: string; + quote_currency: string; + chain_id: number; + chain_name: string; + items: TransactionItem[]; + pagination: { + has_more: boolean; + page_number: number; + page_size: number; + total_count: number | null; + }; + }; + error: boolean; + error_message: string | null; + error_code: number | null; +} + +export interface TransactionAnalysis { + gasSpent: { + "1d": string; + "7d": string; + "30d": string; + }; + txCount: { + "1d": number; + "7d": number; + "30d": number; + }; + uniqueAddresses: { + "1d": number; + "7d": number; + "30d": number; + }; + topAddresses: { + "1d": string[]; + "7d": string[]; + "30d": string[]; + }; +} diff --git a/packages/plugin-taiko/src/utils/index.ts b/packages/plugin-taiko/src/utils/index.ts new file mode 100644 index 0000000000..c035a9f4c7 --- /dev/null +++ b/packages/plugin-taiko/src/utils/index.ts @@ -0,0 +1,40 @@ +import { TransactionAnalysis } from "../types"; + +function formatAnalysisResults(analysisResults: TransactionAnalysis) { + const { gasSpent, txCount, uniqueAddresses, topAddresses } = + analysisResults; + const formatTopAddresses = (addresses: string[]) => { + if (addresses.length === 0) return "None"; + return addresses + .map((address, index) => `${index + 1}. ${address}`) + .join("\n"); + }; + return ` + **Contract Analytics:** + + **Gas Spent:** + - Last 1 Day: ${gasSpent["1d"]} + - Last 7 Days: ${gasSpent["7d"]} + - Last 30 Days: ${gasSpent["30d"]} + + **Transaction Count:** + - Last 1 Day: ${txCount["1d"]} + - Last 7 Days: ${txCount["7d"]} + - Last 30 Days: ${txCount["30d"]} + + **Unique Addresses:** + - Last 1 Day: ${uniqueAddresses["1d"]} + - Last 7 Days: ${uniqueAddresses["7d"]} + - Last 30 Days: ${uniqueAddresses["30d"]} + + **Top Addresses by Interactions:** + - 1 Day: +${formatTopAddresses(topAddresses["1d"])} + - 7 Days: +${formatTopAddresses(topAddresses["7d"])} + - 30 Days: +${formatTopAddresses(topAddresses["30d"])} + `; +} + +export { formatAnalysisResults }; diff --git a/packages/plugin-taiko/tsconfig.json b/packages/plugin-taiko/tsconfig.json new file mode 100644 index 0000000000..834c4dce26 --- /dev/null +++ b/packages/plugin-taiko/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "types": [ + "node" + ] + }, + "include": [ + "src/**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/plugin-taiko/tsup.config.ts b/packages/plugin-taiko/tsup.config.ts new file mode 100644 index 0000000000..97ae93bf89 --- /dev/null +++ b/packages/plugin-taiko/tsup.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], + external: [ + "dotenv", + "fs", + "path", + "@reflink/reflink", + "@node-llama-cpp", + "https", + "http", + "agentkeepalive", + "viem", + "@elizaos/core", + "zod", + "@lifi/sdk", + "@web3-name-sdk/core", + ], +});