From da2530173a2b25c34b354658f7f8f8a1e39b1e16 Mon Sep 17 00:00:00 2001 From: Florent Bartoli Date: Mon, 10 Feb 2025 16:31:34 +0100 Subject: [PATCH] refactor(safe-wallet): Minor code formatting and import reorganization - Reorder imports in multiple files for consistency - Remove unnecessary whitespace and adjust code formatting - Maintain existing functionality while improving code readability --- typescript/examples/langchain/safe/index.ts | 2 +- typescript/packages/wallets/safe/package.json | 12 +- .../wallets/safe/src/SafeWalletClient.ts | 586 +++++++++--------- .../wallets/safe/src/getAddress.plugin.ts | 49 +- typescript/packages/wallets/safe/src/index.ts | 2 +- 5 files changed, 307 insertions(+), 344 deletions(-) diff --git a/typescript/examples/langchain/safe/index.ts b/typescript/examples/langchain/safe/index.ts index 09aa05b8..d2fff12c 100644 --- a/typescript/examples/langchain/safe/index.ts +++ b/typescript/examples/langchain/safe/index.ts @@ -8,7 +8,7 @@ import { baseSepolia } from "viem/chains"; import { getOnChainTools } from "@goat-sdk/adapter-langchain"; import { sendETH } from "@goat-sdk/wallet-evm"; -import { safe, getAddressPlugin } from "@goat-sdk/wallet-safe"; +import { getAddressPlugin, safe } from "@goat-sdk/wallet-safe"; require("dotenv").config(); const pk = process.env.WALLET_PRIVATE_KEY as `0x${string}`; diff --git a/typescript/packages/wallets/safe/package.json b/typescript/packages/wallets/safe/package.json index a07c745f..c17dc672 100644 --- a/typescript/packages/wallets/safe/package.json +++ b/typescript/packages/wallets/safe/package.json @@ -2,11 +2,7 @@ "name": "@goat-sdk/wallet-safe", "version": "0.1.1", "sideEffects": false, - "files": [ - "dist/**/*", - "README.md", - "package.json" - ], + "files": ["dist/**/*", "README.md", "package.json"], "scripts": { "build": "tsup", "clean": "rm -rf dist", @@ -39,9 +35,5 @@ "bugs": { "url": "https://github.com/goat-sdk/goat/issues" }, - "keywords": [ - "ai", - "agents", - "web3" - ] + "keywords": ["ai", "agents", "web3"] } diff --git a/typescript/packages/wallets/safe/src/SafeWalletClient.ts b/typescript/packages/wallets/safe/src/SafeWalletClient.ts index 2196a6df..26223e3a 100644 --- a/typescript/packages/wallets/safe/src/SafeWalletClient.ts +++ b/typescript/packages/wallets/safe/src/SafeWalletClient.ts @@ -1,330 +1,306 @@ import { - type EVMReadRequest, - EVMSmartWalletClient, - type EVMTransaction, - type EVMTypedData, + type EVMReadRequest, + EVMSmartWalletClient, + type EVMTransaction, + type EVMTypedData, } from "@goat-sdk/wallet-evm"; +import { SigningMethod } from "@safe-global/protocol-kit"; import { - type PredictedSafeProps, - type SafeAccountConfig, - SigningMethod, -} from "@safe-global/protocol-kit"; + type SafeClient, + type SafeClientResult, + type SdkStarterKitConfig, + createSafeClient, +} from "@safe-global/sdk-starter-kit"; import { - http, - type Account, - type Address, - type Chain, - type WalletClient as ViemWalletClient, - createWalletClient, - formatUnits, - publicActions, + http, + type Account, + type Address, + type Chain, + type WalletClient as ViemWalletClient, + createWalletClient, + formatUnits, + publicActions, } from "viem"; -import { - createSafeClient, - type SdkStarterKitConfig, - type SafeClient, - type SafeClientResult, -} from "@safe-global/sdk-starter-kit"; import { privateKeyToAccount } from "viem/accounts"; import { mainnet } from "viem/chains"; import { normalize } from "viem/ens"; class SafeWalletError extends Error { - constructor(message: string) { - super(message); - this.name = "SafeWalletError"; - } + constructor(message: string) { + super(message); + this.name = "SafeWalletError"; + } } class SafeNotInitializedError extends SafeWalletError { - constructor() { - super("Safe account not initialized"); - } + constructor() { + super("Safe account not initialized"); + } } class SafeNotDeployedError extends SafeWalletError { - constructor() { - super("Safe is not deployed"); - } + constructor() { + super("Safe is not deployed"); + } } class ChainNotConfiguredError extends SafeWalletError { - constructor() { - super("Chain not properly configured"); - } + constructor() { + super("Chain not properly configured"); + } } export class SafeWalletClient extends EVMSmartWalletClient { - #client: ViemWalletClient; - #account: Account; - #safeAccount: SafeClient | undefined; - #safeAddress: Address | undefined; - #isDeployed = false; - #saltNonce?: string; - #privateKey: `0x${string}`; - - constructor(privateKey: `0x${string}`, chain: Chain, saltNonce?: string) { - super(); - if (!privateKey || !privateKey.startsWith("0x")) { - throw new Error("Invalid private key format"); - } - this.#privateKey = privateKey; - this.#client = createWalletClient({ - account: privateKeyToAccount(privateKey), - chain: chain, - transport: http(), - }); - if (!this.#client.account) { - throw new Error("Client does not have an account"); - } - this.#account = this.#client.account; - this.#saltNonce = saltNonce; - } - - async initialize(): Promise { - if (!this.#client.chain?.rpcUrls.default.http[0]) { - throw new ChainNotConfiguredError(); - } - - const safeAccountConfig = { - owners: [this.#account.address], - threshold: 1, - saltNonce: this.#saltNonce ?? "0", - }; - - const safeConfig: SdkStarterKitConfig = - this.#isDeployed && this.#safeAddress - ? { - provider: this.#client.chain.rpcUrls.default.http[0], - signer: this.#privateKey, - safeAddress: this.#safeAddress, - } - : { - provider: this.#client.chain.rpcUrls.default.http[0], - signer: this.#privateKey, - safeOptions: safeAccountConfig, - }; - try { - this.#safeAccount = await createSafeClient(safeConfig); - this.#safeAddress = (await this.#safeAccount.getAddress()) as Address; - this.#isDeployed = await this.#safeAccount.isDeployed(); - } catch (error) { - throw new SafeWalletError( - `Failed to initialize Safe: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - getAddress(): string { - if (!this.#safeAddress) throw new SafeNotInitializedError(); - console.log(this.#safeAddress, 'safe address'); - return this.#safeAddress; - } - - async #createAndExecuteTransaction( - metaTransactions: Array<{ to: string; value: string; data: string }>, - ): Promise { - if (!this.#safeAccount) throw new SafeNotInitializedError(); - const transaction = await this.#safeAccount.send({ - transactions: metaTransactions, - }); - if (!transaction) - throw new SafeWalletError("Failed to create Safe transaction"); - - return transaction; - } - - async sendBatchOfTransactions( - transactions: EVMTransaction[], - ): Promise<{ hash: string }> { - try { - const metaTransactions = transactions.map((tx) => ({ - to: tx.to, - value: tx.value?.toString() ?? "0", - data: tx.data ?? "0x", - })); - - const result = await this.#createAndExecuteTransaction(metaTransactions); - return { hash: result.transactions?.ethereumTxHash ?? "" }; - } catch (error) { - throw new SafeWalletError( - `Failed to send batch transactions: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - async sendTransaction( - transaction: EVMTransaction, - ): Promise<{ hash: string }> { - try { - const metaTransaction = { - to: transaction.to, - value: transaction.value?.toString() ?? "0", - data: transaction.data ?? "0x", - }; - - const result = await this.#createAndExecuteTransaction([metaTransaction]); - return { hash: result.transactions?.ethereumTxHash ?? "" }; - } catch (error) { - throw new SafeWalletError( - `Failed to send transaction: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - async read(request: EVMReadRequest) { - const { address, abi, functionName, args } = request; - if (!abi) throw new Error("Read request must include ABI for EVM"); - - const result = await this.#client.extend(publicActions).readContract({ - address: await this.resolveAddress(address), - abi, - functionName, - args, - }); - - return { value: result }; - } - - async resolveAddress(address: string): Promise<`0x${string}`> { - if (!this.#safeAccount) throw new Error("Safe account not initialized"); - if (/^0x[a-fA-F0-9]{40}$/.test(address)) return address as `0x${string}`; - - try { - const resolvedAddress = (await this.#client - .extend(publicActions) - .getEnsAddress({ - name: normalize(address), - })) as `0x${string}`; - if (!resolvedAddress) { - throw new Error("ENS name could not be resolved."); - } - return resolvedAddress as `0x${string}`; - } catch (error) { - throw new Error(`Failed to resolve ENS name: ${error}`); - } - } - - async signMessage(message: string) { - if (!this.#safeAccount) throw new SafeNotInitializedError(); - - try { - const safeMessage = this.#safeAccount.protocolKit.createMessage(message); - const signature = await this.#safeAccount.protocolKit.signMessage( - safeMessage, - SigningMethod.ETH_SIGN, - ); - return { signature: signature.encodedSignatures() }; - } catch (error) { - throw new SafeWalletError( - `Failed to sign message: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - async #createTypedDataDomain(data: EVMTypedData) { - const domainFields = Object.entries(data.domain) - .filter(([key]) => key !== "chainId") - .map(([name, value]) => ({ - name, - type: typeof value === "string" ? "string" : "uint256", - })); - - return { - ...domainFields, - chainId: this.#client.chain?.id ?? 0, - }; - } - - async #createTypedDataTypes( - data: EVMTypedData, - domainFields: Array<{ name: string; type: string }>, - ) { - const types = Object.entries(data.types).reduce( - (acc, [key, value]) => { - acc[key] = Array.isArray(value) ? value : []; - return acc; - }, - {} as Record>, - ); - - return { - EIP712Domain: [{ name: "chainId", type: "uint256" }, ...domainFields], - ...types, - }; - } - - async signTypedData(data: EVMTypedData) { - if (!this.#safeAccount) throw new SafeNotInitializedError(); - - try { - const domainFields = Object.entries(data.domain) - .filter(([key]) => key !== "chainId") - .map(([name, value]) => ({ - name, - type: typeof value === "string" ? "string" : "uint256", - })); - - const typeData = { - domain: await this.#createTypedDataDomain(data), - message: data.message, - types: await this.#createTypedDataTypes(data, domainFields), - primaryType: data.primaryType, - }; - - const safeMessage = this.#safeAccount.protocolKit.createMessage(typeData); - const signature = await this.#safeAccount.protocolKit.signMessage( - safeMessage, - SigningMethod.ETH_SIGN, - ); - return { signature: signature.encodedSignatures() }; - } catch (error) { - throw new SafeWalletError( - `Failed to sign typed data: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - async balanceOf(address: string) { - const resolvedAddress = await this.resolveAddress(address); - const balance = await this.#client.extend(publicActions).getBalance({ - address: resolvedAddress, - }); - - const chain = this.#client.chain ?? mainnet; - - return { - value: formatUnits(BigInt(balance), chain.nativeCurrency.decimals), - decimals: chain.nativeCurrency.decimals, - symbol: chain.nativeCurrency.symbol, - name: chain.nativeCurrency.name, - inBaseUnits: balance.toString(), - }; - } - - getChain() { - if (!this.#client.chain) throw new Error("Chain not initialized"); - return { - type: "evm" as const, - id: this.#client.chain.id, - }; - } - - async isDeployed() { - if (!this.#safeAccount) throw new Error("Safe account not initialized"); - if (!this.#safeAddress) throw new Error("Safe address not initialized"); - if (this.#isDeployed) return true; - - return await this.#safeAccount.isDeployed(); - } + #client: ViemWalletClient; + #account: Account; + #safeAccount: SafeClient | undefined; + #safeAddress: Address | undefined; + #isDeployed = false; + #saltNonce?: string; + #privateKey: `0x${string}`; + + constructor(privateKey: `0x${string}`, chain: Chain, saltNonce?: string) { + super(); + if (!privateKey || !privateKey.startsWith("0x")) { + throw new Error("Invalid private key format"); + } + this.#privateKey = privateKey; + this.#client = createWalletClient({ + account: privateKeyToAccount(privateKey), + chain: chain, + transport: http(), + }); + if (!this.#client.account) { + throw new Error("Client does not have an account"); + } + this.#account = this.#client.account; + this.#saltNonce = saltNonce; + } + + async initialize(): Promise { + if (!this.#client.chain?.rpcUrls.default.http[0]) { + throw new ChainNotConfiguredError(); + } + + const safeAccountConfig = { + owners: [this.#account.address], + threshold: 1, + saltNonce: this.#saltNonce ?? "0", + }; + + const safeConfig: SdkStarterKitConfig = + this.#isDeployed && this.#safeAddress + ? { + provider: this.#client.chain.rpcUrls.default.http[0], + signer: this.#privateKey, + safeAddress: this.#safeAddress, + } + : { + provider: this.#client.chain.rpcUrls.default.http[0], + signer: this.#privateKey, + safeOptions: safeAccountConfig, + }; + try { + this.#safeAccount = await createSafeClient(safeConfig); + this.#safeAddress = (await this.#safeAccount.getAddress()) as Address; + this.#isDeployed = await this.#safeAccount.isDeployed(); + } catch (error) { + throw new SafeWalletError( + `Failed to initialize Safe: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + getAddress(): string { + if (!this.#safeAddress) throw new SafeNotInitializedError(); + console.log(this.#safeAddress, "safe address"); + return this.#safeAddress; + } + + async #createAndExecuteTransaction( + metaTransactions: Array<{ to: string; value: string; data: string }>, + ): Promise { + if (!this.#safeAccount) throw new SafeNotInitializedError(); + const transaction = await this.#safeAccount.send({ + transactions: metaTransactions, + }); + if (!transaction) throw new SafeWalletError("Failed to create Safe transaction"); + + return transaction; + } + + async sendBatchOfTransactions(transactions: EVMTransaction[]): Promise<{ hash: string }> { + try { + const metaTransactions = transactions.map((tx) => ({ + to: tx.to, + value: tx.value?.toString() ?? "0", + data: tx.data ?? "0x", + })); + + const result = await this.#createAndExecuteTransaction(metaTransactions); + return { hash: result.transactions?.ethereumTxHash ?? "" }; + } catch (error) { + throw new SafeWalletError( + `Failed to send batch transactions: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + async sendTransaction(transaction: EVMTransaction): Promise<{ hash: string }> { + try { + const metaTransaction = { + to: transaction.to, + value: transaction.value?.toString() ?? "0", + data: transaction.data ?? "0x", + }; + + const result = await this.#createAndExecuteTransaction([metaTransaction]); + return { hash: result.transactions?.ethereumTxHash ?? "" }; + } catch (error) { + throw new SafeWalletError( + `Failed to send transaction: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + async read(request: EVMReadRequest) { + const { address, abi, functionName, args } = request; + if (!abi) throw new Error("Read request must include ABI for EVM"); + + const result = await this.#client.extend(publicActions).readContract({ + address: await this.resolveAddress(address), + abi, + functionName, + args, + }); + + return { value: result }; + } + + async resolveAddress(address: string): Promise<`0x${string}`> { + if (!this.#safeAccount) throw new Error("Safe account not initialized"); + if (/^0x[a-fA-F0-9]{40}$/.test(address)) return address as `0x${string}`; + + try { + const resolvedAddress = (await this.#client.extend(publicActions).getEnsAddress({ + name: normalize(address), + })) as `0x${string}`; + if (!resolvedAddress) { + throw new Error("ENS name could not be resolved."); + } + return resolvedAddress as `0x${string}`; + } catch (error) { + throw new Error(`Failed to resolve ENS name: ${error}`); + } + } + + async signMessage(message: string) { + if (!this.#safeAccount) throw new SafeNotInitializedError(); + + try { + const safeMessage = this.#safeAccount.protocolKit.createMessage(message); + const signature = await this.#safeAccount.protocolKit.signMessage(safeMessage, SigningMethod.ETH_SIGN); + return { signature: signature.encodedSignatures() }; + } catch (error) { + throw new SafeWalletError( + `Failed to sign message: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + async #createTypedDataDomain(data: EVMTypedData) { + const domainFields = Object.entries(data.domain) + .filter(([key]) => key !== "chainId") + .map(([name, value]) => ({ + name, + type: typeof value === "string" ? "string" : "uint256", + })); + + return { + ...domainFields, + chainId: this.#client.chain?.id ?? 0, + }; + } + + async #createTypedDataTypes(data: EVMTypedData, domainFields: Array<{ name: string; type: string }>) { + const types = Object.entries(data.types).reduce( + (acc, [key, value]) => { + acc[key] = Array.isArray(value) ? value : []; + return acc; + }, + {} as Record>, + ); + + return { + EIP712Domain: [{ name: "chainId", type: "uint256" }, ...domainFields], + ...types, + }; + } + + async signTypedData(data: EVMTypedData) { + if (!this.#safeAccount) throw new SafeNotInitializedError(); + + try { + const domainFields = Object.entries(data.domain) + .filter(([key]) => key !== "chainId") + .map(([name, value]) => ({ + name, + type: typeof value === "string" ? "string" : "uint256", + })); + + const typeData = { + domain: await this.#createTypedDataDomain(data), + message: data.message, + types: await this.#createTypedDataTypes(data, domainFields), + primaryType: data.primaryType, + }; + + const safeMessage = this.#safeAccount.protocolKit.createMessage(typeData); + const signature = await this.#safeAccount.protocolKit.signMessage(safeMessage, SigningMethod.ETH_SIGN); + return { signature: signature.encodedSignatures() }; + } catch (error) { + throw new SafeWalletError( + `Failed to sign typed data: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + async balanceOf(address: string) { + const resolvedAddress = await this.resolveAddress(address); + const balance = await this.#client.extend(publicActions).getBalance({ + address: resolvedAddress, + }); + + const chain = this.#client.chain ?? mainnet; + + return { + value: formatUnits(BigInt(balance), chain.nativeCurrency.decimals), + decimals: chain.nativeCurrency.decimals, + symbol: chain.nativeCurrency.symbol, + name: chain.nativeCurrency.name, + inBaseUnits: balance.toString(), + }; + } + + getChain() { + if (!this.#client.chain) throw new Error("Chain not initialized"); + return { + type: "evm" as const, + id: this.#client.chain.id, + }; + } + + async isDeployed() { + if (!this.#safeAccount) throw new Error("Safe account not initialized"); + if (!this.#safeAddress) throw new Error("Safe address not initialized"); + if (this.#isDeployed) return true; + + return await this.#safeAccount.isDeployed(); + } } -export async function safe( - privateKey: `0x${string}`, - chain: Chain, - saltNonce?: string, -): Promise { - const wallet = new SafeWalletClient(privateKey, chain, saltNonce); - await wallet.initialize(); - return wallet; +export async function safe(privateKey: `0x${string}`, chain: Chain, saltNonce?: string): Promise { + const wallet = new SafeWalletClient(privateKey, chain, saltNonce); + await wallet.initialize(); + return wallet; } diff --git a/typescript/packages/wallets/safe/src/getAddress.plugin.ts b/typescript/packages/wallets/safe/src/getAddress.plugin.ts index d81fb1d9..84e7d562 100644 --- a/typescript/packages/wallets/safe/src/getAddress.plugin.ts +++ b/typescript/packages/wallets/safe/src/getAddress.plugin.ts @@ -1,35 +1,30 @@ -import { - PluginBase, - type WalletClientBase, - createTool, - type Chain, -} from "@goat-sdk/core"; +import { type Chain, PluginBase, type WalletClientBase, createTool } from "@goat-sdk/core"; import { z } from "zod"; // Since we are creating a chain-agnostic plugin, we can use the WalletClientBase interface export class GetAddressPlugin extends PluginBase { - constructor() { - // We define the name of the plugin - super("getSafeAddress", []); - } + constructor() { + // We define the name of the plugin + super("getSafeAddress", []); + } - // We define the chain support for the plugin, in this case we support all chains - supportsChain = (chain: Chain) => chain.type === "evm"; + // We define the chain support for the plugin, in this case we support all chains + supportsChain = (chain: Chain) => chain.type === "evm"; - getTools(walletClient: WalletClientBase) { - return [ - createTool( - { - name: "get_safe_address", - description: "Get the address of the agent's Safe", - parameters: z.NEVER, - }, - () => { - // Return just the address without additional formatting - return walletClient.getAddress(); - }, - ), - ]; - } + getTools(walletClient: WalletClientBase) { + return [ + createTool( + { + name: "get_safe_address", + description: "Get the address of the agent's Safe", + parameters: z.NEVER, + }, + () => { + // Return just the address without additional formatting + return walletClient.getAddress(); + }, + ), + ]; + } } // We export a factory function to create a new instance of the plugin diff --git a/typescript/packages/wallets/safe/src/index.ts b/typescript/packages/wallets/safe/src/index.ts index 4bc52d13..d021d504 100644 --- a/typescript/packages/wallets/safe/src/index.ts +++ b/typescript/packages/wallets/safe/src/index.ts @@ -1,2 +1,2 @@ export * from "./SafeWalletClient"; -export * from "./getAddress.plugin"; \ No newline at end of file +export * from "./getAddress.plugin";