diff --git a/.env.example b/.env.example index 174008c1378..3b5a75cc1f5 100644 --- a/.env.example +++ b/.env.example @@ -197,6 +197,9 @@ EVM_PROVIDER_URL= AVALANCHE_PRIVATE_KEY= AVALANCHE_PUBLIC_KEY= +# Arthera +ARTHERA_PRIVATE_KEY= + # Solana SOLANA_PRIVATE_KEY= SOLANA_PUBLIC_KEY= diff --git a/agent/package.json b/agent/package.json index eb4a23a2752..9755bcbf48d 100644 --- a/agent/package.json +++ b/agent/package.json @@ -69,6 +69,7 @@ "@elizaos/plugin-web-search": "workspace:*", "@elizaos/plugin-genlayer": "workspace:*", "@elizaos/plugin-open-weather": "workspace:*", + "@elizaos/plugin-arthera": "workspace:*", "readline": "1.3.0", "ws": "8.18.0", "yargs": "17.7.2" diff --git a/agent/src/index.ts b/agent/src/index.ts index 296d0313fea..77a4f3e238a 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -71,7 +71,10 @@ import { zksyncEraPlugin } from "@elizaos/plugin-zksync-era"; import { availPlugin } from "@elizaos/plugin-avail"; import { openWeatherPlugin } from "@elizaos/plugin-open-weather"; + +import { artheraPlugin } from "@elizaos/plugin-arthera"; import { stargazePlugin } from "@elizaos/plugin-stargaze"; + import Database from "better-sqlite3"; import fs from "fs"; import net from "net"; @@ -653,6 +656,9 @@ export async function createAgent( getSecret(character, "OPEN_WEATHER_API_KEY") ? openWeatherPlugin : null, + getSecret(character, "ARTHERA_PRIVATE_KEY")?.startsWith("0x") + ? artheraPlugin + : null, ].filter(Boolean), providers: [], actions: [], diff --git a/packages/plugin-arthera/README.md b/packages/plugin-arthera/README.md new file mode 100644 index 00000000000..b634635d469 --- /dev/null +++ b/packages/plugin-arthera/README.md @@ -0,0 +1,68 @@ +# `@elizaos/plugin-arthera` + +This plugin provides actions and providers for interacting with Arthera. + +--- + +## Configuration + +### Default Setup + +By default, **Arthera** is enabled. To use it, simply add your private key to the `.env` file: + +```env +ARTHERA_PRIVATE_KEY=your-private-key-here +``` + +### Custom RPC URLs + +By default, the RPC URL is inferred from the `viem/chains` config. To use a custom RPC URL for a specific chain, add the following to your `.env` file: + +```env +ETHEREUM_PROVIDER_=https://your-custom-rpc-url +``` + +**Example usage:** + +```env +ETHEREUM_PROVIDER_ARTHERA=https://rpc.arthera.net +``` + +## Provider + +The **Wallet Provider** initializes with Arthera. It: + +- Provides the **context** of the currently connected address and its balance. +- Creates **Public** and **Wallet clients** to interact with the supported chain. + +--- + +## Actions + +### Transfer + +Transfer tokens from one address to another on Arthera. Just specify the: + +- **Amount** +- **Chain** +- **Recipient Address** + +**Example usage:** + +```bash +Transfer 1 AA to 0xRecipient on arthera. +``` + +--- + +## Contribution + +The plugin contains tests. Whether you're using **TDD** or not, please make sure to run the tests before submitting a PR. + +### Running Tests + +Navigate to the `plugin-arthera` directory and run: + +```bash +pnpm test +``` diff --git a/packages/plugin-arthera/eslint.config.mjs b/packages/plugin-arthera/eslint.config.mjs new file mode 100644 index 00000000000..92fe5bbebef --- /dev/null +++ b/packages/plugin-arthera/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/plugin-arthera/package.json b/packages/plugin-arthera/package.json new file mode 100644 index 00000000000..db58990809f --- /dev/null +++ b/packages/plugin-arthera/package.json @@ -0,0 +1,24 @@ +{ + "name": "@elizaos/plugin-arthera", + "version": "0.1.8-alpha.1", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@elizaos/core": "workspace:*", + "tsup": "8.3.5", + "viem": "2.21.58" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "test": "vitest run", + "lint": "eslint --fix --cache ." + }, + "devDependencies": { + "whatwg-url": "7.1.0" + }, + "peerDependencies": { + "whatwg-url": "7.1.0" + } +} diff --git a/packages/plugin-arthera/src/actions/transfer.ts b/packages/plugin-arthera/src/actions/transfer.ts new file mode 100644 index 00000000000..2ad25281de5 --- /dev/null +++ b/packages/plugin-arthera/src/actions/transfer.ts @@ -0,0 +1,176 @@ +import { ByteArray, formatEther, parseEther, type Hex } from "viem"; +import { + composeContext, + generateObjectDeprecated, + HandlerCallback, + ModelClass, + type IAgentRuntime, + type Memory, + type State, +} from "@elizaos/core"; + +import { initWalletProvider, WalletProvider } from "../providers/wallet"; +import type { Transaction, TransferParams } from "../types"; +import { transferTemplate } from "../templates"; + +export { transferTemplate }; + +// Exported for tests +export class TransferAction { + constructor(private walletProvider: WalletProvider) {} + + async transfer(params: TransferParams): Promise { + const walletClient = this.walletProvider.getWalletClient( + params.fromChain + ); + + console.log( + `Transferring: ${params.amount} tokens from (${walletClient.account.address} to (${params.toAddress} on ${params.fromChain})` + ); + + if (!params.data) { + params.data = "0x"; + } + + try { + const hash = await walletClient.sendTransaction({ + account: walletClient.account, + to: params.toAddress, + value: parseEther(params.amount), + data: params.data as Hex, + kzg: { + blobToKzgCommitment: function (_: ByteArray): ByteArray { + throw new Error("Function not implemented."); + }, + computeBlobKzgProof: function ( + _blob: ByteArray, + _commitment: ByteArray + ): ByteArray { + throw new Error("Function not implemented."); + }, + }, + chain: undefined, + }); + + return { + hash, + from: walletClient.account.address, + to: params.toAddress, + value: parseEther(params.amount), + data: params.data as Hex, + }; + } catch (error) { + throw new Error(`Transfer failed: ${error.message}`); + } + } +} + +const buildTransferDetails = async ( + state: State, + runtime: IAgentRuntime, + wp: WalletProvider +): Promise => { + const context = composeContext({ + state, + template: transferTemplate, + }); + + const chains = Object.keys(wp.chains); + + const contextWithChains = context.replace( + "SUPPORTED_CHAINS", + chains.map((item) => `"${item}"`).join("|") + ); + + const transferDetails = (await generateObjectDeprecated({ + runtime, + context: contextWithChains, + modelClass: ModelClass.SMALL, + })) as TransferParams; + + const existingChain = wp.chains[transferDetails.fromChain]; + + if (!existingChain) { + throw new Error( + "The chain " + + transferDetails.fromChain + + " not configured yet. Add the chain or choose one from configured: " + + chains.toString() + ); + } + + return transferDetails; +}; + +export const transferAction = { + name: "transfer", + description: "Transfer tokens between addresses on the same chain", + handler: async ( + runtime: IAgentRuntime, + _message: Memory, + state: State, + _options: Record, + callback?: HandlerCallback + ) => { + console.log("Transfer action handler called"); + const walletProvider = initWalletProvider(runtime); + const action = new TransferAction(walletProvider); + + // Compose transfer context + const paramOptions = await buildTransferDetails( + state, + runtime, + walletProvider + ); + + try { + const transferResp = await action.transfer(paramOptions); + if (callback) { + callback({ + text: `Successfully transferred ${paramOptions.amount} tokens to ${paramOptions.toAddress}\nTransaction Hash: ${transferResp.hash}`, + content: { + success: true, + hash: transferResp.hash, + amount: formatEther(transferResp.value), + recipient: transferResp.to, + chain: paramOptions.fromChain, + }, + }); + } + return true; + } catch (error) { + console.error("Error during token transfer:", error); + if (callback) { + callback({ + text: `Error transferring tokens: ${error.message}`, + content: { error: error.message }, + }); + } + return false; + } + }, + template: transferTemplate, + validate: async (runtime: IAgentRuntime) => { + const privateKey = runtime.getSetting("ARTHERA_PRIVATE_KEY"); + return typeof privateKey === "string" && privateKey.startsWith("0x"); + }, + examples: [ + [ + { + user: "assistant", + content: { + text: "I'll help you transfer 1 AA to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + action: "SEND_TOKENS", + }, + }, + { + user: "user", + content: { + text: "Transfer 1 AA to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + action: "SEND_TOKENS", + }, + }, + ], + ], + similes: ["SEND_TOKENS", "TOKEN_TRANSFER", "MOVE_TOKENS"], +}; diff --git a/packages/plugin-arthera/src/index.ts b/packages/plugin-arthera/src/index.ts new file mode 100644 index 00000000000..3fe8d585945 --- /dev/null +++ b/packages/plugin-arthera/src/index.ts @@ -0,0 +1,18 @@ +export * from "./actions/transfer"; +export * from "./providers/wallet"; +export * from "./types"; + +import type { Plugin } from "@elizaos/core"; +import { transferAction } from "./actions/transfer"; +import { artheraWalletProvider } from "./providers/wallet"; + +export const artheraPlugin: Plugin = { + name: "arthera", + description: "Arthera blockchain integration plugin", + providers: [artheraWalletProvider], + evaluators: [], + services: [], + actions: [transferAction], +}; + +export default artheraPlugin; diff --git a/packages/plugin-arthera/src/providers/wallet.ts b/packages/plugin-arthera/src/providers/wallet.ts new file mode 100644 index 00000000000..7d724fbf4a7 --- /dev/null +++ b/packages/plugin-arthera/src/providers/wallet.ts @@ -0,0 +1,203 @@ +import { + createPublicClient, + createWalletClient, + formatUnits, + http, +} from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import type { IAgentRuntime, Provider, Memory, State } from "@elizaos/core"; +import type { + Address, + WalletClient, + PublicClient, + Chain, + HttpTransport, + Account, + PrivateKeyAccount, +} from "viem"; +import * as viemChains from "viem/chains"; + +import type { SupportedChain } from "../types"; + +export class WalletProvider { + private currentChain: SupportedChain = "arthera"; + chains: Record = { arthera: viemChains.arthera }; + 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); + } + } + + 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 getWalletBalance(): Promise { + try { + const client = this.getPublicClient(this.currentChain); + const balance = await client.getBalance({ + address: this.account.address, + }); + return formatUnits(balance, 18); + } catch (error) { + console.error("Error getting wallet balance:", error); + return null; + } + } + + async getWalletBalanceForChain( + chainName: SupportedChain + ): Promise { + try { + const client = this.getPublicClient(chainName); + const balance = await client.getBalance({ + address: this.account.address, + }); + return formatUnits(balance, 18); + } catch (error) { + console.error("Error getting wallet balance:", error); + return null; + } + } + + private setAccount = (pk: `0x${string}`) => { + this.account = privateKeyToAccount(pk); + }; + + private setChains = (chains?: Record) => { + if (!chains) { + return; + } + Object.keys(chains).forEach((chain: string) => { + 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 = ["arthera"]; + const chains = {}; + + chainNames.forEach((chainName) => { + const rpcUrl = runtime.getSetting( + "ETHEREUM_PROVIDER_" + chainName.toUpperCase() + ); + const chain = WalletProvider.genChainFromName(chainName, rpcUrl); + chains[chainName] = chain; + }); + + return chains; +}; + +export const initWalletProvider = (runtime: IAgentRuntime) => { + const privateKey = runtime.getSetting("ARTHERA_PRIVATE_KEY"); + if (!privateKey) { + throw new Error("ARTHERA_PRIVATE_KEY is missing"); + } + + const chains = genChainsFromRuntime(runtime); + + return new WalletProvider(privateKey as `0x${string}`, chains); +}; + +export const artheraWalletProvider: Provider = { + async get( + runtime: IAgentRuntime, + _message: Memory, + _state?: State + ): Promise { + try { + const walletProvider = initWalletProvider(runtime); + const address = walletProvider.getAddress(); + const balance = await walletProvider.getWalletBalance(); + const chain = walletProvider.getCurrentChain(); + return `Arthera Wallet Address: ${address}\nBalance: ${balance} ${chain.nativeCurrency.symbol}\nChain ID: ${chain.id}, Name: ${chain.name}`; + } catch (error) { + console.error("Error in Arthera wallet provider:", error); + return null; + } + }, +}; diff --git a/packages/plugin-arthera/src/templates/index.ts b/packages/plugin-arthera/src/templates/index.ts new file mode 100644 index 00000000000..d8206074bce --- /dev/null +++ b/packages/plugin-arthera/src/templates/index.ts @@ -0,0 +1,23 @@ +export const transferTemplate = `Given the recent messages and wallet information below: + +{{recentMessages}} + +{{walletInfo}} + +Extract the following information about the requested transfer: +- Chain to execute on: Must be one of ["arthera", "base", ...] (like in viem/chains) +- Amount to transfer: Must be a string representing the amount in AA (only number without coin symbol, e.g., "0.1") +- Recipient address: Must be a valid Arthera address starting with "0x" +- Token symbol or address (if not native token): Optional, leave as null for AA transfers + +Respond with a JSON markdown block containing only the extracted values. All fields except 'token' are required: + +\`\`\`json +{ + "fromChain": SUPPORTED_CHAINS, + "amount": string, + "toAddress": string, + "token": string | null +} +\`\`\` +`; diff --git a/packages/plugin-arthera/src/tests/transfer.test.ts b/packages/plugin-arthera/src/tests/transfer.test.ts new file mode 100644 index 00000000000..aaecf38d7c7 --- /dev/null +++ b/packages/plugin-arthera/src/tests/transfer.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { generatePrivateKey } from "viem/accounts"; +import { Chain } from "viem"; +import { getEnvVariable } from "@elizaos/core"; + +import { TransferAction } from "../actions/transfer"; +import { WalletProvider } from "../providers/wallet"; + +describe("Transfer Action", () => { + let wp: WalletProvider; + let wp1: WalletProvider; + + beforeEach(async () => { + const pk = generatePrivateKey(); + const pk1 = getEnvVariable("ARTHERA_PRIVATE_KEY") as `0x${string}`; + const customChains = prepareChains(); + wp = new WalletProvider(pk, customChains); + if (pk1) { + wp1 = new WalletProvider(pk1, customChains); + } + }); + describe("Constructor", () => { + it("should initialize with wallet provider", () => { + const ta = new TransferAction(wp); + + expect(ta).toBeDefined(); + }); + }); + describe("Transfer", () => { + let ta: TransferAction; + let ta1: TransferAction; + let receiverAddress: `0x${string}`; + + beforeEach(() => { + ta = new TransferAction(wp); + if (wp1) { + ta1 = new TransferAction(wp1); + receiverAddress = wp1.getAddress(); + } + else { + receiverAddress = wp.getAddress(); + } + }); + + it("throws if not enough gas", async () => { + await expect( + ta.transfer({ + fromChain: "arthera", + toAddress: receiverAddress, + amount: "1", + }) + ).rejects.toThrow( + "Transfer failed: The total cost (gas * gas fee + value) of executing this transaction exceeds the balance of the account." + ); + }); + + if (wp1) { + it("transfers tokens", async () => { + const tx = await ta1.transfer({ + fromChain: "arthera", + toAddress: receiverAddress, + amount: "0.001", + }); + + expect(tx).toBeDefined(); + expect(tx.from).toEqual(wp1.getAddress()); + expect(tx.to).toEqual(receiverAddress); + expect(tx.value).toEqual(1000000000000000n); + }); + } + }); +}); + +const prepareChains = () => { + const customChains: Record = {}; + const chainNames = ["arthera"]; + chainNames.forEach( + (chain) => + (customChains[chain] = WalletProvider.genChainFromName(chain)) + ); + + return customChains; +}; diff --git a/packages/plugin-arthera/src/tests/wallet.test.ts b/packages/plugin-arthera/src/tests/wallet.test.ts new file mode 100644 index 00000000000..07cb1494ed3 --- /dev/null +++ b/packages/plugin-arthera/src/tests/wallet.test.ts @@ -0,0 +1,175 @@ +import { describe, it, expect, beforeAll, beforeEach } from "vitest"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import { arthera, Chain } from "viem/chains"; + +import { WalletProvider } from "../providers/wallet"; + +const customRpcUrls = { + arthera: "custom-rpc.arthera.io", +}; + +describe("Wallet provider", () => { + let walletProvider: WalletProvider; + let pk: `0x${string}`; + const customChains: Record = {}; + + beforeAll(() => { + pk = generatePrivateKey(); + + const chainNames = ["arthera"]; + chainNames.forEach( + (chain) => + (customChains[chain] = WalletProvider.genChainFromName(chain)) + ); + }); + + describe("Constructor", () => { + it("sets address", () => { + const account = privateKeyToAccount(pk); + const expectedAddress = account.address; + + walletProvider = new WalletProvider(pk); + + expect(walletProvider.getAddress()).toEqual(expectedAddress); + }); + it("sets default chain to arthera", () => { + walletProvider = new WalletProvider(pk); + + expect(walletProvider.chains.arthera.id).toEqual(arthera.id); + expect(walletProvider.getCurrentChain().id).toEqual(arthera.id); + }); + it("sets custom chains", () => { + walletProvider = new WalletProvider(pk, customChains); + + expect(walletProvider.chains.arthera.id).toEqual(arthera.id); + }); + it("sets the first provided custom chain as current chain", () => { + walletProvider = new WalletProvider(pk, customChains); + + expect(walletProvider.getCurrentChain().id).toEqual(arthera.id); + }); + }); + describe("Clients", () => { + beforeEach(() => { + walletProvider = new WalletProvider(pk); + }); + it("generates public client", () => { + const client = walletProvider.getPublicClient("arthera"); + expect(client.chain.id).toEqual(arthera.id); + expect(client.transport.url).toEqual( + arthera.rpcUrls.default.http[0] + ); + }); + it("generates public client with custom rpcurl", () => { + const chain = WalletProvider.genChainFromName( + "arthera", + customRpcUrls.arthera + ); + const wp = new WalletProvider(pk, { ["arthera"]: chain }); + + const client = wp.getPublicClient("arthera"); + expect(client.chain.id).toEqual(arthera.id); + expect(client.chain.rpcUrls.default.http[0]).toEqual( + arthera.rpcUrls.default.http[0] + ); + expect(client.chain.rpcUrls.custom.http[0]).toEqual( + customRpcUrls.arthera + ); + expect(client.transport.url).toEqual(customRpcUrls.arthera); + }); + it("generates wallet client", () => { + const account = privateKeyToAccount(pk); + const expectedAddress = account.address; + + const client = walletProvider.getWalletClient("arthera"); + + expect(client.account.address).toEqual(expectedAddress); + expect(client.transport.url).toEqual( + arthera.rpcUrls.default.http[0] + ); + }); + it("generates wallet client with custom rpcurl", () => { + const account = privateKeyToAccount(pk); + const expectedAddress = account.address; + const chain = WalletProvider.genChainFromName( + "arthera", + customRpcUrls.arthera + ); + const wp = new WalletProvider(pk, { ["arthera"]: chain }); + + const client = wp.getWalletClient("arthera"); + + expect(client.account.address).toEqual(expectedAddress); + expect(client.chain.id).toEqual(arthera.id); + expect(client.chain.rpcUrls.default.http[0]).toEqual( + arthera.rpcUrls.default.http[0] + ); + expect(client.chain.rpcUrls.custom.http[0]).toEqual( + customRpcUrls.arthera + ); + expect(client.transport.url).toEqual(customRpcUrls.arthera); + }); + }); + describe("Balance", () => { + beforeEach(() => { + walletProvider = new WalletProvider(pk, customChains); + }); + it("should fetch balance", async () => { + const bal = await walletProvider.getWalletBalance(); + + expect(bal).toEqual("0"); + }); + it("should fetch balance for a specific added chain", async () => { + const bal = await walletProvider.getWalletBalanceForChain("arthera"); + + expect(bal).toEqual("0"); + }); + it("should return null if chain is not added", async () => { + const bal = await walletProvider.getWalletBalanceForChain("base"); + expect(bal).toBeNull(); + }); + }); + describe("Chain", () => { + beforeEach(() => { + walletProvider = new WalletProvider(pk, customChains); + }); + it("generates chains from chain name", () => { + const chainName = "arthera"; + const chain: Chain = WalletProvider.genChainFromName(chainName); + + expect(chain.rpcUrls.default.http[0]).toEqual( + arthera.rpcUrls.default.http[0] + ); + }); + it("generates chains from chain name with custom rpc url", () => { + const chainName = "arthera"; + const customRpcUrl = customRpcUrls.arthera; + const chain: Chain = WalletProvider.genChainFromName( + chainName, + customRpcUrl + ); + + expect(chain.rpcUrls.default.http[0]).toEqual( + arthera.rpcUrls.default.http[0] + ); + expect(chain.rpcUrls.custom.http[0]).toEqual(customRpcUrl); + }); + it("gets chain configs", () => { + const chain = walletProvider.getChainConfigs("arthera"); + + expect(chain.id).toEqual(arthera.id); + }); + it("throws if unsupported chain name", () => { + // intentionally set unsupported chain, ts will complain + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(() => WalletProvider.genChainFromName("ethereum")).toThrow(); + }); + it("throws if invalid chain name", () => { + // intentionally set incorrect chain, ts will complain + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(() => WalletProvider.genChainFromName("eth")).toThrow(); + }); + }); +}); diff --git a/packages/plugin-arthera/src/types/index.ts b/packages/plugin-arthera/src/types/index.ts new file mode 100644 index 00000000000..f772c842a62 --- /dev/null +++ b/packages/plugin-arthera/src/types/index.ts @@ -0,0 +1,73 @@ +import type { + Account, + Address, + Chain, + Hash, + HttpTransport, + PublicClient, + WalletClient, +} from "viem"; +import * as viemChains from "viem/chains"; + +const _SupportedChainList = Object.keys(viemChains) as Array< + keyof typeof viemChains +>; +export type SupportedChain = (typeof _SupportedChainList)[number]; + +// Transaction types +export interface Transaction { + hash: Hash; + from: Address; + to: Address; + value: bigint; + data?: `0x${string}`; + chainId?: number; +} + +// Chain configuration +export interface ChainMetadata { + chainId: number; + name: string; + chain: Chain; + rpcUrl: string; + nativeCurrency: { + name: string; + symbol: string; + decimals: number; + }; + blockExplorerUrl: string; +} + +export interface ChainConfig { + chain: Chain; + publicClient: PublicClient; + walletClient?: WalletClient; +} + +// Action parameters +export interface TransferParams { + fromChain: SupportedChain; + toAddress: Address; + amount: string; + data?: `0x${string}`; +} + +// Plugin configuration +export interface ArtheraPluginConfig { + rpcUrl?: { + arthera?: string; + }; + secrets?: { + ARTHERA_PRIVATE_KEY: string; + }; + testMode?: boolean; + multicall?: { + batchSize?: number; + wait?: number; + }; +} + +export interface ProviderError extends Error { + code?: number; + data?: unknown; +} diff --git a/packages/plugin-arthera/tsconfig.json b/packages/plugin-arthera/tsconfig.json new file mode 100644 index 00000000000..b6ce190d989 --- /dev/null +++ b/packages/plugin-arthera/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "./src", + "typeRoots": [ + "./node_modules/@types", + "./src/types" + ], + "declaration": true + }, + "include": [ + "src" + ] +} diff --git a/packages/plugin-arthera/tsup.config.ts b/packages/plugin-arthera/tsup.config.ts new file mode 100644 index 00000000000..04abb285562 --- /dev/null +++ b/packages/plugin-arthera/tsup.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], // Ensure you're targeting CommonJS + external: [ + "dotenv", // Externalize dotenv to prevent bundling + "fs", // Externalize fs to use Node.js built-in module + "path", // Externalize other built-ins if necessary + "@reflink/reflink", + "@node-llama-cpp", + "https", + "http", + "agentkeepalive", + "viem", + ], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dde34c9e572..919188c7184 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -166,6 +166,9 @@ importers: '@elizaos/plugin-avail': specifier: workspace:* version: link:../packages/plugin-avail + '@elizaos/plugin-arthera': + specifier: workspace:* + version: link:../packages/plugin-arthera '@elizaos/plugin-avalanche': specifier: workspace:* version: link:../packages/plugin-avalanche @@ -1083,6 +1086,22 @@ importers: specifier: 7.1.0 version: 7.1.0 + packages/plugin-arthera: + dependencies: + '@elizaos/core': + specifier: workspace:* + version: link:../core + tsup: + specifier: 8.3.5 + version: 8.3.5(@swc/core@1.10.1(@swc/helpers@0.5.15))(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.1) + viem: + specifier: 2.21.58 + version: 2.21.58(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.23.8) + devDependencies: + whatwg-url: + specifier: 7.1.0 + version: 7.1.0 + packages/plugin-avail: dependencies: '@elizaos/core':