diff --git a/README.md b/README.md index d00020f08..439ae5a8f 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ agentkit/ │ │ └── wallet-providers/ │ │ ├── cdp/ │ │ ├── privy/ +| | ├── dynamic/ │ │ └── viem/ │ │ └── scripts/generate-action-provider/ # use this to create new actions │ ├── create-onchain-agent/ @@ -156,6 +157,7 @@ agentkit/ │ ├── langchain-farcaster-chatbot/ │ ├── langchain-legacy-cdp-chatbot/ │ ├── langchain-privy-chatbot/ +| ├── langchain-dynamic-chatbot/ │ ├── langchain-solana-chatbot/ │ ├── langchain-twitter-chatbot/ │ ├── langchain-xmtp-chatbot/ @@ -273,6 +275,7 @@ AgentKit is proud to have support for the following protocols, frameworks, walle ### Wallets Coinbase +Dynamic Privy ViEM diff --git a/assets/wallets/dynamic.svg b/assets/wallets/dynamic.svg new file mode 100644 index 000000000..05a3b0399 --- /dev/null +++ b/assets/wallets/dynamic.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/typescript/.changeset/small-banks-reply.md b/typescript/.changeset/small-banks-reply.md new file mode 100644 index 000000000..8b7af6ff1 --- /dev/null +++ b/typescript/.changeset/small-banks-reply.md @@ -0,0 +1,6 @@ +--- +"langchain-dynamic-chatbot": minor +"@coinbase/agentkit": minor +--- + +Adds Dynamic as wallet provider diff --git a/typescript/agentkit/package.json b/typescript/agentkit/package.json index 537907c06..3a5c69df6 100644 --- a/typescript/agentkit/package.json +++ b/typescript/agentkit/package.json @@ -45,11 +45,14 @@ "@coinbase/cdp-sdk": "^1.38.0", "@coinbase/coinbase-sdk": "^0.20.0", "@coinbase/x402": "^0.6.3", + "@dynamic-labs-wallet/node": "0.0.169", + "@dynamic-labs-wallet/node-evm": "0.0.169", + "@dynamic-labs-wallet/node-svm": "0.0.169", "@jup-ag/api": "^6.0.39", "@privy-io/public-api": "2.18.5", "@privy-io/server-auth": "1.18.4", "@solana/spl-token": "^0.4.12", - "@solana/web3.js": "^1.98.1", + "@solana/web3.js": "^1.98.2", "@zerodev/ecdsa-validator": "^5.4.5", "@zerodev/intent": "^0.0.24", "@zerodev/sdk": "^5.4.28", diff --git a/typescript/agentkit/src/wallet-providers/dynamicEvmWalletProvider.test.ts b/typescript/agentkit/src/wallet-providers/dynamicEvmWalletProvider.test.ts new file mode 100644 index 000000000..061deb90d --- /dev/null +++ b/typescript/agentkit/src/wallet-providers/dynamicEvmWalletProvider.test.ts @@ -0,0 +1,370 @@ +import { DynamicEvmWalletProvider } from "./dynamicEvmWalletProvider"; +import { createPublicClient, http } from "viem"; +import { getChain } from "../network/network"; +import { createDynamicWallet, createDynamicClient } from "./dynamicShared"; + +// Mock dynamic imports +jest.mock("@dynamic-labs-wallet/node-evm", () => ({ + DynamicEvmWalletClient: jest.fn(), +})); +jest.mock("@dynamic-labs-wallet/node", () => ({ + ThresholdSignatureScheme: { + TWO_OF_TWO: "TWO_OF_TWO", + TWO_OF_THREE: "TWO_OF_THREE", + THREE_OF_FIVE: "THREE_OF_FIVE", + }, +})); + +jest.mock("viem", () => ({ + createPublicClient: jest.fn(), + http: jest.fn(), +})); +jest.mock("../network/network"); +jest.mock("../analytics", () => ({ + sendAnalyticsEvent: jest.fn().mockImplementation(() => Promise.resolve()), +})); +jest.mock("./dynamicShared", () => ({ + createDynamicWallet: jest.fn(), + createDynamicClient: jest.fn(), +})); + +const MOCK_ADDRESS = "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"; +const MOCK_TRANSACTION_HASH = "0xef01"; +const MOCK_SIGNATURE_HASH = "0x1234"; + +describe("DynamicEvmWalletProvider", () => { + const MOCK_CONFIG = { + authToken: "test-auth-token", + environmentId: "test-environment-id", + baseApiUrl: "https://app.dynamicauth.com", + baseMPCRelayApiUrl: "relay.dynamicauth.com", + chainId: "84532", + networkId: "base-sepolia", + chainType: "ethereum" as const, + thresholdSignatureScheme: "TWO_OF_TWO", + }; + + const mockWallet = { + accountAddress: MOCK_ADDRESS, + publicKeyHex: "0x123", + }; + + const mockDynamicClient = { + createViemPublicClient: jest.fn().mockReturnValue({ + getBalance: jest.fn(), + getTransactionCount: jest.fn(), + }), + signMessage: jest.fn().mockResolvedValue(MOCK_SIGNATURE_HASH), + signTransaction: jest.fn().mockResolvedValue(MOCK_SIGNATURE_HASH), + exportPrivateKey: jest.fn().mockResolvedValue({ derivedPrivateKey: "0xprivate" }), + importPrivateKey: jest.fn().mockResolvedValue({ + accountAddress: MOCK_ADDRESS, + publicKeyHex: "0x123", + }), + }; + + const mockPublicClient = { + chain: { + id: 84532, + name: "Base Goerli", + rpcUrls: { + default: { http: ["https://goerli.base.org"] }, + }, + nativeCurrency: { + name: "Ether", + symbol: "ETH", + decimals: 18, + }, + }, + getBalance: jest.fn().mockResolvedValue(BigInt(1000000000000000000)), + getTransactionCount: jest.fn(), + prepareTransactionRequest: jest.fn().mockResolvedValue({ + to: "0x123" as `0x${string}`, + value: BigInt(1000), + data: "0x" as `0x${string}`, + gas: BigInt(21000), + nonce: 1, + maxFeePerGas: BigInt(1000000), + maxPriorityFeePerGas: BigInt(1000000), + }), + sendRawTransaction: jest.fn().mockResolvedValue(MOCK_TRANSACTION_HASH), + waitForTransactionReceipt: jest.fn().mockResolvedValue({ + transactionHash: MOCK_TRANSACTION_HASH, + status: "success", + }), + readContract: jest.fn(), + }; + + beforeEach(async () => { + jest.clearAllMocks(); + + // Import the mocked modules + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { DynamicEvmWalletClient } = (await import("@dynamic-labs-wallet/node-evm")) as any; + + // Mock DynamicEvmWalletClient + DynamicEvmWalletClient.mockImplementation(() => mockDynamicClient); + + // Mock getChain + (getChain as jest.Mock).mockReturnValue({ + id: 84532, + name: "Base Goerli", + rpcUrls: { + default: { http: ["https://goerli.base.org"] }, + }, + nativeCurrency: { + name: "Ether", + symbol: "ETH", + decimals: 18, + }, + }); + + // Mock viem functions + (createPublicClient as jest.Mock).mockReturnValue(mockPublicClient); + (http as jest.Mock).mockReturnValue(jest.fn()); + + // Mock createDynamicClient + (createDynamicClient as jest.Mock).mockResolvedValue(mockDynamicClient); + + // Mock createDynamicWallet + (createDynamicWallet as jest.Mock).mockResolvedValue({ + wallet: mockWallet, + dynamic: mockDynamicClient, + }); + }); + + describe("configureWithWallet", () => { + it("should create a new wallet with Dynamic client", async () => { + const _provider = await DynamicEvmWalletProvider.configureWithWallet(MOCK_CONFIG); + + expect(createDynamicWallet).toHaveBeenCalledWith( + { + ...MOCK_CONFIG, + }, + "ethereum", + ); + + expect(getChain).toHaveBeenCalledWith(MOCK_CONFIG.chainId); + expect(createPublicClient).toHaveBeenCalled(); + }); + + it("should throw error when wallet creation fails", async () => { + (createDynamicWallet as jest.Mock).mockRejectedValue(new Error("Failed to create wallet")); + + await expect(DynamicEvmWalletProvider.configureWithWallet(MOCK_CONFIG)).rejects.toThrow( + "Failed to create wallet", + ); + }); + + it("should throw error when chain is not found", async () => { + (getChain as jest.Mock).mockReturnValue(null); + + await expect(DynamicEvmWalletProvider.configureWithWallet(MOCK_CONFIG)).rejects.toThrow( + `Chain with ID ${MOCK_CONFIG.chainId} not found`, + ); + }); + + it("should use default chain ID when not provided", async () => { + const { chainId: _chainId, ...configWithoutChainId } = MOCK_CONFIG; + + await DynamicEvmWalletProvider.configureWithWallet(configWithoutChainId); + + expect(getChain).toHaveBeenCalledWith("84532"); + }); + }); + + describe("wallet methods", () => { + let provider: DynamicEvmWalletProvider; + + beforeEach(async () => { + provider = await DynamicEvmWalletProvider.configureWithWallet(MOCK_CONFIG); + }); + + it("should get the wallet address", () => { + expect(provider.getAddress()).toBe(MOCK_ADDRESS); + }); + + it("should get the network information", () => { + expect(provider.getNetwork()).toEqual({ + protocolFamily: "evm", + chainId: MOCK_CONFIG.chainId, + networkId: "base-sepolia", + }); + }); + + it("should get the provider name", () => { + expect(provider.getName()).toBe("dynamic_evm_wallet_provider"); + }); + + it("should sign a string message using Dynamic client", async () => { + const result = await provider.signMessage("Hello, world!"); + expect(result).toBe(MOCK_SIGNATURE_HASH); + expect(mockDynamicClient.signMessage).toHaveBeenCalledWith({ + message: "Hello, world!", + accountAddress: MOCK_ADDRESS, + }); + }); + + it("should sign a Uint8Array message using Dynamic client", async () => { + const messageBytes = new TextEncoder().encode("Hello, world!"); + const result = await provider.signMessage(messageBytes); + expect(result).toBe(MOCK_SIGNATURE_HASH); + expect(mockDynamicClient.signMessage).toHaveBeenCalledWith({ + message: "Hello, world!", + accountAddress: MOCK_ADDRESS, + }); + }); + + it("should export private key", async () => { + const result = await provider.exportPrivateKey(); + expect(result).toBe("0xprivate"); + expect(mockDynamicClient.exportPrivateKey).toHaveBeenCalledWith({ + accountAddress: MOCK_ADDRESS, + }); + }); + + it("should import private key", async () => { + const result = await provider.importPrivateKey("0xprivate"); + expect(result).toEqual({ + accountAddress: MOCK_ADDRESS, + publicKeyHex: "0x123", + }); + expect(mockDynamicClient.importPrivateKey).toHaveBeenCalledWith({ + privateKey: "0xprivate", + chainName: "EVM", + thresholdSignatureScheme: "TWO_OF_TWO", + }); + }); + + it("should export wallet information", async () => { + const result = await provider.exportWallet(); + expect(result).toEqual({ + accountAddress: MOCK_ADDRESS, + networkId: "base-sepolia", + }); + }); + + it("should sign a transaction using Dynamic client", async () => { + const mockTransaction = { + to: "0x123" as `0x${string}`, + value: BigInt(1000), + data: "0x" as `0x${string}`, + }; + + const result = await provider.signTransaction(mockTransaction); + expect(result).toBe(MOCK_SIGNATURE_HASH); + + // Should prepare transaction with viem + expect(mockPublicClient.prepareTransactionRequest).toHaveBeenCalledWith({ + to: mockTransaction.to, + value: mockTransaction.value, + data: mockTransaction.data, + account: MOCK_ADDRESS, + chain: mockPublicClient.chain, + }); + + // Should sign with Dynamic + expect(mockDynamicClient.signTransaction).toHaveBeenCalledWith({ + senderAddress: MOCK_ADDRESS, + transaction: expect.objectContaining({ + to: mockTransaction.to, + value: mockTransaction.value, + data: mockTransaction.data, + gas: BigInt(21000), + nonce: 1, + }), + }); + }); + + it("should send a transaction by signing and broadcasting", async () => { + const mockTransaction = { + to: "0x123" as `0x${string}`, + value: BigInt(1000), + data: "0x" as `0x${string}`, + }; + + const result = await provider.sendTransaction(mockTransaction); + expect(result).toBe(MOCK_TRANSACTION_HASH); + + // Should prepare and sign transaction + expect(mockPublicClient.prepareTransactionRequest).toHaveBeenCalled(); + expect(mockDynamicClient.signTransaction).toHaveBeenCalled(); + + // Should broadcast signed transaction + expect(mockPublicClient.sendRawTransaction).toHaveBeenCalledWith({ + serializedTransaction: MOCK_SIGNATURE_HASH, + }); + }); + + it("should get wallet balance", async () => { + const balance = await provider.getBalance(); + expect(balance).toBe(BigInt(1000000000000000000)); + expect(mockPublicClient.getBalance).toHaveBeenCalledWith({ + address: MOCK_ADDRESS, + }); + }); + + it("should get public client", () => { + const publicClient = provider.getPublicClient(); + expect(publicClient).toBe(mockPublicClient); + }); + + it("should wait for transaction receipt", async () => { + const receipt = await provider.waitForTransactionReceipt( + MOCK_TRANSACTION_HASH as `0x${string}`, + ); + expect(receipt).toEqual({ + transactionHash: MOCK_TRANSACTION_HASH, + status: "success", + }); + expect(mockPublicClient.waitForTransactionReceipt).toHaveBeenCalledWith({ + hash: MOCK_TRANSACTION_HASH, + }); + }); + + it("should read contract", async () => { + const mockParams = { + address: "0x123" as `0x${string}`, + abi: [], + functionName: "balanceOf", + args: [MOCK_ADDRESS], + }; + + await provider.readContract(mockParams); + expect(mockPublicClient.readContract).toHaveBeenCalledWith(mockParams); + }); + + it("should perform native transfer", async () => { + const to = "0x456"; + const value = "1000000000000000000"; // 1 ETH in wei + + const txHash = await provider.nativeTransfer(to, value); + expect(txHash).toBe(MOCK_TRANSACTION_HASH); + expect(mockPublicClient.prepareTransactionRequest).toHaveBeenCalled(); + expect(mockDynamicClient.signTransaction).toHaveBeenCalled(); + expect(mockPublicClient.sendRawTransaction).toHaveBeenCalled(); + expect(mockPublicClient.waitForTransactionReceipt).toHaveBeenCalled(); + }); + + it("should throw error when signing raw hash", async () => { + await expect( + provider.sign( + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" as `0x${string}`, + ), + ).rejects.toThrow("Raw hash signing not implemented for Dynamic wallet provider"); + }); + + it("should throw error when signing typed data", async () => { + const typedData = { + domain: { name: "Test" }, + types: {}, + message: {}, + primaryType: "Test", + }; + + await expect(provider.signTypedData(typedData)).rejects.toThrow( + "Typed data signing not implemented for Dynamic wallet provider", + ); + }); + }); +}); diff --git a/typescript/agentkit/src/wallet-providers/dynamicEvmWalletProvider.ts b/typescript/agentkit/src/wallet-providers/dynamicEvmWalletProvider.ts new file mode 100644 index 000000000..60bc76d34 --- /dev/null +++ b/typescript/agentkit/src/wallet-providers/dynamicEvmWalletProvider.ts @@ -0,0 +1,411 @@ +import { EvmWalletProvider } from "./evmWalletProvider"; +import { + createPublicClient, + http, + type PublicClient, + type TransactionRequest, + type Hex, + type Abi, + type ContractFunctionName, + type ContractFunctionArgs, + type ReadContractParameters, + type ReadContractReturnType, +} from "viem"; +import { getChain, NETWORK_ID_TO_CHAIN_ID } from "../network/network"; +import { type Network } from "../network"; +import { + type DynamicWalletConfig, + type DynamicWalletExport, + type DynamicWalletClient, + createDynamicWallet, +} from "./dynamicShared"; + +/** + * Configuration options for the Dynamic wallet provider. + * + * @interface + */ +export interface DynamicEvmWalletConfig extends DynamicWalletConfig { + /** Optional chain ID to connect to */ + chainId?: string; + /** Optional RPC URL override for Viem public client */ + rpcUrl?: string; +} + +/** + * A wallet provider that uses Dynamic's wallet API. + * This provider extends EvmWalletProvider and implements all signing operations + * using Dynamic's embedded wallet infrastructure. + */ +export class DynamicEvmWalletProvider extends EvmWalletProvider { + #accountAddress: string; + #dynamicClient: DynamicWalletClient; + #publicClient: PublicClient; + #network: Network; + + /** + * Private constructor to enforce use of factory method. + * + * @param accountAddress - The wallet account address + * @param dynamicClient - The Dynamic wallet client instance + * @param publicClient - The public client for read operations and broadcasting + * @param network - The network configuration + */ + private constructor( + accountAddress: string, + dynamicClient: DynamicWalletClient, + publicClient: PublicClient, + network: Network, + ) { + super(); + this.#accountAddress = accountAddress; + this.#dynamicClient = dynamicClient; + this.#publicClient = publicClient; + this.#network = network; + } + + /** + * Creates and configures a new DynamicWalletProvider instance. + * + * @param config - The configuration options for the Dynamic wallet + * @returns A configured DynamicWalletProvider instance + * + * @example + * ```typescript + * const provider = await DynamicWalletProvider.configureWithWallet({ + * authToken: "your-auth-token", + * environmentId: "your-environment-id", + * networkId: "base-sepolia", + * thresholdSignatureScheme: "TWO_OF_TWO" + * }); + * ``` + */ + public static async configureWithWallet( + config: DynamicEvmWalletConfig, + ): Promise { + const networkId = config.networkId || "base-sepolia"; + const chainId = NETWORK_ID_TO_CHAIN_ID[networkId]; + + if (!chainId) { + throw new Error(`Unsupported network ID: ${networkId}`); + } + + console.log("[DynamicEvmWalletProvider] Starting wallet configuration with config:", { + networkId, + chainId, + environmentId: config.environmentId, + }); + + const { wallet, dynamic } = await createDynamicWallet(config, "ethereum"); + + const chain = getChain(chainId); + if (!chain) { + throw new Error(`Chain with ID ${chainId} not found`); + } + + const network: Network = { + protocolFamily: "evm", + chainId, + networkId, + }; + + const rpcUrl = config.rpcUrl || process.env.RPC_URL; + const publicClient = createPublicClient({ + chain, + transport: rpcUrl ? http(rpcUrl) : http(), + }); + + console.log("[DynamicEvmWalletProvider] Wallet configured successfully:", { + address: wallet.accountAddress, + network: networkId, + }); + + return new DynamicEvmWalletProvider(wallet.accountAddress, dynamic, publicClient, network); + } + + /** + * Signs a raw hash. + * + * @param _hash - The hash to sign + * @returns The signed hash + * @throws Error indicating this operation is not supported + */ + async sign(_hash: `0x${string}`): Promise<`0x${string}`> { + throw new Error( + "Raw hash signing not implemented for Dynamic wallet provider. Use signMessage or signTransaction instead.", + ); + } + + /** + * Signs a message using Dynamic's signing service. + * + * @param message - The message to sign (string or Uint8Array) + * @returns The signature as a hex string with 0x prefix + */ + async signMessage(message: string | Uint8Array): Promise<`0x${string}`> { + const messageStr = typeof message === "string" ? message : new TextDecoder().decode(message); + + const signature = await this.#dynamicClient.signMessage({ + message: messageStr, + accountAddress: this.#accountAddress, + }); + return signature as `0x${string}`; + } + + /** + * Signs typed data. + * + * @param _typedData - The typed data to sign + * @returns The signed typed data + * @throws Error indicating this operation is not supported + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async signTypedData(_typedData: any): Promise<`0x${string}`> { + throw new Error("Typed data signing not implemented for Dynamic wallet provider."); + } + + /** + * Signs a transaction using Dynamic's signing service. + * + * @param transaction - The transaction to sign + * @returns The signed transaction as a hex string + */ + async signTransaction(transaction: TransactionRequest): Promise { + if (!this.#publicClient.chain) { + throw new Error("Chain not found"); + } + + console.log("[DynamicEvmWalletProvider] Preparing transaction for signing:", { + to: transaction.to, + value: transaction.value?.toString(), + data: transaction.data, + }); + + // Prepare transaction with gas estimation using Viem + // This follows Dynamic's recommended pattern from their docs + const preparedTx = await this.#publicClient.prepareTransactionRequest({ + to: transaction.to, + value: transaction.value, + data: transaction.data, + account: this.#accountAddress as `0x${string}`, + chain: this.#publicClient.chain, + }); + + console.log("[DynamicEvmWalletProvider] Transaction prepared, signing with Dynamic:", { + to: preparedTx.to, + value: preparedTx.value?.toString(), + gas: preparedTx.gas?.toString(), + nonce: preparedTx.nonce, + }); + + try { + // Dynamic import for ESM compatibility + const { DynamicEvmWalletClient: _DynamicEvmWalletClient } = (await import( + "@dynamic-labs-wallet/node-evm" + // eslint-disable-next-line @typescript-eslint/no-explicit-any + )) as any; + + // Retrieve external server key shares required for signing + // This is required when wallet was created without backUpToClientShareService: true + console.log("[DynamicEvmWalletProvider] Retrieving external server key shares..."); + const keyShares = await ( + this.#dynamicClient as InstanceType + ).getExternalServerKeyShares({ + accountAddress: this.#accountAddress, + }); + if (keyShares && keyShares.length) + console.log("[DynamicEvmWalletProvider] Retrieved", keyShares.length, "key shares"); + + // Sign using Dynamic's signTransaction with external key shares + const signedTx = await ( + this.#dynamicClient as InstanceType + ).signTransaction({ + senderAddress: this.#accountAddress as `0x${string}`, + externalServerKeyShares: keyShares || [], + transaction: preparedTx, + }); + + console.log("[DynamicEvmWalletProvider] Transaction signed successfully"); + return signedTx as Hex; + } catch (error) { + console.error("[DynamicEvmWalletProvider] Error signing transaction:", error); + throw error; + } + } + + /** + * Sends a transaction by signing it with Dynamic and broadcasting it. + * + * @param transaction - The transaction to send + * @returns The transaction hash + */ + async sendTransaction(transaction: TransactionRequest): Promise { + console.log("[DynamicEvmWalletProvider] Sending transaction:", { + to: transaction.to, + value: transaction.value?.toString(), + data: transaction.data, + }); + + // Sign the transaction using Dynamic + const signedTx = await this.signTransaction(transaction); + + // Broadcast the signed transaction + console.log("[DynamicEvmWalletProvider] Broadcasting signed transaction..."); + const txHash = await this.#publicClient.sendRawTransaction({ + serializedTransaction: signedTx, + }); + + console.log("[DynamicEvmWalletProvider] Transaction sent successfully:", txHash); + return txHash; + } + + /** + * Exports the private key for the wallet. + * + * @param password - Optional password for encrypted backup shares + * @returns The private key + */ + public async exportPrivateKey(password?: string): Promise { + const result = await this.#dynamicClient.exportPrivateKey({ + accountAddress: this.getAddress(), + password, + }); + return result.derivedPrivateKey || ""; + } + + /** + * Imports a private key. + * + * @param privateKey - The private key to import + * @param password - Optional password for encrypted backup shares + * @returns The account address and public key + */ + public async importPrivateKey( + privateKey: string, + password?: string, + ): Promise<{ + accountAddress: string; + publicKeyHex: string; + }> { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { ThresholdSignatureScheme } = (await import("@dynamic-labs-wallet/node")) as any; + const result = await this.#dynamicClient.importPrivateKey({ + privateKey, + chainName: "EVM", + thresholdSignatureScheme: ThresholdSignatureScheme.TWO_OF_TWO, + password, + }); + return { + accountAddress: result.accountAddress, + publicKeyHex: "publicKeyHex" in result ? result.publicKeyHex : "", + }; + } + + /** + * Waits for a transaction receipt. + * + * @param txHash - The hash of the transaction to wait for + * @returns The transaction receipt + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async waitForTransactionReceipt(txHash: `0x${string}`): Promise { + return await this.#publicClient.waitForTransactionReceipt({ hash: txHash }); + } + + /** + * Reads a contract. + * + * @param params - The parameters to read the contract + * @returns The response from the contract + */ + async readContract< + const abi extends Abi | readonly unknown[], + functionName extends ContractFunctionName, + const args extends ContractFunctionArgs, + >( + params: ReadContractParameters, + ): Promise> { + return this.#publicClient.readContract(params); + } + + /** + * Gets the Viem PublicClient used for read-only operations. + * + * @returns The Viem PublicClient instance + */ + getPublicClient(): PublicClient { + return this.#publicClient; + } + + /** + * Gets the balance of the wallet. + * + * @returns The balance of the wallet in wei + */ + async getBalance(): Promise { + return await this.#publicClient.getBalance({ + address: this.#accountAddress as `0x${string}`, + }); + } + + /** + * Transfer the native asset of the network. + * + * @param to - The destination address + * @param value - The amount to transfer in atomic units (Wei) + * @returns The transaction hash + */ + async nativeTransfer(to: string, value: string): Promise { + const tx = await this.sendTransaction({ + to: to as `0x${string}`, + value: BigInt(value), + }); + + const receipt = await this.waitForTransactionReceipt(tx); + + if (!receipt) { + throw new Error("Transaction failed"); + } + + return receipt.transactionHash; + } + + /** + * Gets the address of the wallet. + * + * @returns The wallet address + */ + getAddress(): string { + return this.#accountAddress; + } + + /** + * Gets the network of the wallet. + * + * @returns The network of the wallet + */ + getNetwork(): Network { + return this.#network; + } + + /** + * Gets the name of the provider. + * + * @returns The provider name + */ + getName(): string { + return "dynamic_evm_wallet_provider"; + } + + /** + * Exports the wallet information. + * + * @returns The wallet information + */ + async exportWallet(): Promise { + return { + accountAddress: this.#accountAddress, + networkId: this.#network.networkId, + }; + } +} diff --git a/typescript/agentkit/src/wallet-providers/dynamicShared.ts b/typescript/agentkit/src/wallet-providers/dynamicShared.ts new file mode 100644 index 000000000..80db1dd53 --- /dev/null +++ b/typescript/agentkit/src/wallet-providers/dynamicShared.ts @@ -0,0 +1,157 @@ +/** + * Configuration options for the Dynamic wallet provider. + * + * @interface + */ +export interface DynamicWalletConfig { + /** The Dynamic authentication token */ + authToken: string; + /** The Dynamic environment ID */ + environmentId: string; + /** The account address of the wallet to use, if not provided a new wallet will be created */ + accountAddress?: string; + /** The network ID to use for the wallet */ + networkId: string; + /** The threshold signature scheme to use for wallet creation */ + thresholdSignatureScheme?: string; + /** Optional password for encrypted backup shares */ + password?: string; +} + +export type DynamicWalletExport = { + accountAddress: string; + networkId: string | undefined; +}; + +export type DynamicWalletClient = Awaited>; + +type CreateDynamicWalletReturnType = { + wallet: { + accountAddress: string; + publicKeyHex?: string; // Only for EVM + rawPublicKey: Uint8Array; + externalServerKeyShares: unknown[]; // Specify a more appropriate type if known + }; + dynamic: DynamicWalletClient; +}; + +/** + * Converts a string threshold signature scheme to the enum value + * + * @param scheme - The string representation of the threshold signature scheme + * @returns The corresponding ThresholdSignatureScheme enum value + */ +const convertThresholdSignatureScheme = async (scheme?: string) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { ThresholdSignatureScheme } = (await import("@dynamic-labs-wallet/node")) as any; + if (scheme === "TWO_OF_THREE") return ThresholdSignatureScheme.TWO_OF_THREE; + if (scheme === "THREE_OF_FIVE") return ThresholdSignatureScheme.THREE_OF_FIVE; + return ThresholdSignatureScheme.TWO_OF_TWO; +}; +/** + * Create a Dynamic client based on the chain type + * + * @param config - The configuration options for the Dynamic client + * @param chainType - The type of chain to create the client for + * @returns The created Dynamic client + */ +export const createDynamicClient = async ( + config: DynamicWalletConfig, + chainType: "ethereum" | "solana", +) => { + const clientConfig = { + authToken: config.authToken, + environmentId: config.environmentId, + }; + + try { + let client; + + if (chainType === "ethereum") { + // Dynamic import for ESM compatibility - only load EVM package when needed + // Using type assertion due to dual-format package export issues with Node16 module resolution + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const evmModule = (await import("@dynamic-labs-wallet/node-evm")) as any; + const { DynamicEvmWalletClient } = evmModule; + client = new DynamicEvmWalletClient(clientConfig); + } else { + // Dynamic import for ESM compatibility - only load SVM package when needed + // Using type assertion due to dual-format package export issues with Node16 module resolution + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const svmModule = (await import("@dynamic-labs-wallet/node-svm")) as any; + const { DynamicSvmWalletClient } = svmModule; + client = new DynamicSvmWalletClient(clientConfig); + } + + await client.authenticateApiToken(config.authToken); + const evmWallets = await client.getWallets(); + console.log("wallets:", evmWallets); + + return client; + } catch (error) { + console.error("[createDynamicClient] Error creating client:", error); + throw error; + } +}; + +/** + * Create a Dynamic wallet + * + * @param config - The configuration options for the Dynamic wallet + * @param chainType - The type of chain to create the wallet for + * @returns The created Dynamic wallet and client + */ +export const createDynamicWallet = async ( + config: DynamicWalletConfig, + chainType: "ethereum" | "solana", +): Promise => { + const client = await createDynamicClient(config, chainType); + console.log("[createDynamicWallet] Dynamic client created"); + + let wallet: CreateDynamicWalletReturnType["wallet"]; + const wallets = + chainType === "solana" ? await client.getSvmWallets() : await client.getEvmWallets(); + const existingWallet = wallets.find(wallet => wallet.accountAddress === config.accountAddress); + if (existingWallet) { + console.log( + "[createDynamicWallet] Found existing wallet with address:", + existingWallet.accountAddress, + ); + wallet = existingWallet; + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { ThresholdSignatureScheme } = (await import("@dynamic-labs-wallet/node")) as any; + console.log("[createDynamicWallet] Creating new wallet"); + console.log("[createDynamicWallet] createWalletAccount params:", { + thresholdSignatureScheme: + config.thresholdSignatureScheme || ThresholdSignatureScheme.TWO_OF_TWO, + password: config.password ? "***" : undefined, + networkId: config.networkId, + chainType: chainType, + }); + + const thresholdSignatureScheme = await convertThresholdSignatureScheme( + config.thresholdSignatureScheme, + ); + try { + const result = await client.createWalletAccount({ + thresholdSignatureScheme, + password: config.password, + backUpToClientShareService: true, + }); + wallet = { + accountAddress: result.accountAddress, + rawPublicKey: result.rawPublicKey, + externalServerKeyShares: result.externalServerKeyShares, + }; + } catch (error) { + throw new Error("Failed to create wallet: " + error); + } + } + + console.log("[createDynamicWallet] Wallet created/retrieved:", { + accountAddress: wallet.accountAddress, + }); + + return { wallet, dynamic: client }; +}; diff --git a/typescript/agentkit/src/wallet-providers/dynamicSvmWalletProvider.test.ts b/typescript/agentkit/src/wallet-providers/dynamicSvmWalletProvider.test.ts new file mode 100644 index 000000000..4d65c2a8c --- /dev/null +++ b/typescript/agentkit/src/wallet-providers/dynamicSvmWalletProvider.test.ts @@ -0,0 +1,330 @@ +import { DynamicSvmWalletProvider } from "./dynamicSvmWalletProvider"; +// import { DynamicSvmWalletClient } from "@dynamic-labs-wallet/node-svm"; +import { + Connection, + clusterApiUrl, + PublicKey, + VersionedTransaction, + MessageV0, +} from "@solana/web3.js"; +import { createDynamicWallet, createDynamicClient } from "./dynamicShared"; +// import { ThresholdSignatureScheme } from "@dynamic-labs-wallet/node"; + +// Mock dynamic imports +jest.mock("@dynamic-labs-wallet/node-svm", () => ({ + DynamicSvmWalletClient: jest.fn(), +})); +jest.mock("@dynamic-labs-wallet/node", () => ({ + ThresholdSignatureScheme: { + TWO_OF_TWO: "TWO_OF_TWO", + TWO_OF_THREE: "TWO_OF_THREE", + THREE_OF_FIVE: "THREE_OF_FIVE", + }, +})); +jest.mock("../network/svm", () => ({ + SOLANA_CLUSTER_ID_BY_NETWORK_ID: { + "": "mainnet-beta", + "mainnet-beta": "mainnet-beta", + testnet: "testnet", + devnet: "devnet", + }, + SOLANA_NETWORKS: { + "test-genesis-hash": { + protocolFamily: "svm", + chainId: "test-genesis-hash", + networkId: "mainnet-beta", + }, + }, +})); +jest.mock("@solana/web3.js", () => { + const actual = jest.requireActual("@solana/web3.js"); + const mockVersionedTransaction = jest.fn().mockImplementation(message => { + const tx = { + signatures: [], + message: message || { compiledMessage: Buffer.from([]) }, + }; + Object.setPrototypeOf(tx, actual.VersionedTransaction.prototype); + return tx; + }); + + return { + ...actual, + Connection: jest.fn().mockImplementation((endpoint, commitment = "confirmed") => { + // Store the commitment for verification + (Connection as jest.Mock).mock.lastCall = [endpoint, commitment]; + return { + getGenesisHash: jest.fn().mockResolvedValue("test-genesis-hash"), + commitment, + rpcEndpoint: endpoint, + getBalance: jest.fn(), + getBalanceAndContext: jest.fn(), + sendTransaction: jest.fn().mockResolvedValue(MOCK_TRANSACTION_HASH), + getSignatureStatus: jest.fn().mockResolvedValue({ + context: { slot: 123 }, + value: { slot: 123, confirmations: 10, err: null }, + }), + getLatestBlockhash: jest.fn().mockResolvedValue({ + blockhash: "test-blockhash", + lastValidBlockHeight: 123, + }), + }; + }), + PublicKey: jest.fn().mockImplementation(address => ({ + toBase58: jest.fn().mockReturnValue(address), + toString: jest.fn().mockReturnValue(address), + toBuffer: jest.fn().mockReturnValue(Buffer.from(address)), + toArrayLike: jest.fn().mockReturnValue(Buffer.from(address)), + })), + VersionedTransaction: mockVersionedTransaction, + MessageV0: { + compile: jest.fn().mockReturnValue({ + compiledMessage: Buffer.from([]), + }), + }, + clusterApiUrl: jest.fn().mockImplementation(network => { + // Always use mainnet-beta as default + const networkId = network || "mainnet-beta"; + return `https://api.${networkId}.solana.com`; + }), + }; +}); +jest.mock("../analytics", () => ({ + sendAnalyticsEvent: jest.fn().mockImplementation(() => Promise.resolve()), +})); +jest.mock("./dynamicShared", () => ({ + createDynamicWallet: jest.fn(), + createDynamicClient: jest.fn(), +})); + +const MOCK_ADDRESS = "test-address"; +const MOCK_TRANSACTION_HASH = "test-tx-hash"; +const MOCK_SIGNATURE_HASH = "test-signature"; +const MOCK_NETWORK = { + protocolFamily: "svm", + chainId: "test-genesis-hash", + networkId: "mainnet-beta", +}; + +describe("DynamicSvmWalletProvider", () => { + const MOCK_CONFIG = { + authToken: "test-auth-token", + environmentId: "test-environment-id", + baseApiUrl: "https://app.dynamicauth.com", + baseMPCRelayApiUrl: "relay.dynamicauth.com", + networkId: "mainnet-beta", + chainType: "solana" as const, + thresholdSignatureScheme: "TWO_OF_TWO", // Will be converted by the function + }; + + const mockWallet = { + accountAddress: MOCK_ADDRESS, + publicKeyHex: "0x123", + }; + + const mockDynamicClient = { + signMessage: jest.fn().mockResolvedValue(MOCK_SIGNATURE_HASH), + signTransaction: jest.fn().mockImplementation(({ transaction }) => { + // Return the transaction directly, not the whole object + if (!(transaction instanceof VersionedTransaction)) { + Object.setPrototypeOf(transaction, VersionedTransaction.prototype); + } + return transaction; + }), + createWalletAccount: jest.fn().mockResolvedValue({ + accountAddress: MOCK_ADDRESS, + rawPublicKey: new Uint8Array(), + externalServerKeyShares: [], + }), + deriveAccountAddress: jest.fn().mockResolvedValue({ + accountAddress: MOCK_ADDRESS, + }), + exportPrivateKey: jest.fn().mockResolvedValue({ + derivedPrivateKey: "test-private-key", + }), + importPrivateKey: jest.fn().mockResolvedValue({ + accountAddress: MOCK_ADDRESS, + rawPublicKey: new Uint8Array(), + externalServerKeyShares: [], + }), + getSvmWallets: jest.fn().mockResolvedValue([]), + }; + + beforeEach(async () => { + jest.clearAllMocks(); + + // Import the mocked modules + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { DynamicSvmWalletClient } = (await import("@dynamic-labs-wallet/node-svm")) as any; + + // Mock DynamicSvmWalletClient + DynamicSvmWalletClient.mockImplementation(() => mockDynamicClient); + + // Mock createDynamicClient + (createDynamicClient as jest.Mock).mockResolvedValue(mockDynamicClient); + + // Mock createDynamicWallet + (createDynamicWallet as jest.Mock).mockResolvedValue({ + wallet: mockWallet, + dynamic: mockDynamicClient, + }); + }); + + describe("configureWithWallet", () => { + it("should create a new wallet with Dynamic client", async () => { + const _provider = await DynamicSvmWalletProvider.configureWithWallet(MOCK_CONFIG); + + expect(createDynamicWallet).toHaveBeenCalledWith({ + ...MOCK_CONFIG, + chainType: "solana", + }); + + expect(Connection).toHaveBeenCalledWith("https://api.mainnet-beta.solana.com"); + const connection = (Connection as jest.Mock).mock.results[0].value; + expect(connection.getGenesisHash).toHaveBeenCalled(); + }); + + it("should throw error when wallet creation fails", async () => { + (createDynamicWallet as jest.Mock).mockRejectedValue(new Error("Failed to create wallet")); + + await expect(DynamicSvmWalletProvider.configureWithWallet(MOCK_CONFIG)).rejects.toThrow( + "Failed to create wallet", + ); + }); + + it("should use provided connection when available", async () => { + const mockConnection = { + getGenesisHash: jest.fn().mockResolvedValue("test-genesis-hash"), + commitment: "confirmed", + rpcEndpoint: "https://custom-rpc.example.com", + getBalance: jest.fn(), + getBalanceAndContext: jest.fn(), + sendTransaction: jest.fn().mockResolvedValue(MOCK_TRANSACTION_HASH), + getSignatureStatus: jest.fn().mockResolvedValue({ + context: { slot: 123 }, + value: { slot: 123, confirmations: 10, err: null }, + }), + getLatestBlockhash: jest.fn().mockResolvedValue({ + blockhash: "test-blockhash", + lastValidBlockHeight: 123, + }), + }; + const config = { + ...MOCK_CONFIG, + connection: mockConnection as unknown as Connection, + }; + const _provider = await DynamicSvmWalletProvider.configureWithWallet(config); + + expect(Connection).not.toHaveBeenCalled(); + expect(mockConnection.getGenesisHash).toHaveBeenCalled(); + }); + + it("should use provided network ID", async () => { + await DynamicSvmWalletProvider.configureWithWallet(MOCK_CONFIG); + + expect(clusterApiUrl).toHaveBeenCalledWith("mainnet-beta"); + }); + }); + + describe("wallet methods", () => { + let provider: DynamicSvmWalletProvider; + + beforeEach(async () => { + provider = await DynamicSvmWalletProvider.configureWithWallet(MOCK_CONFIG); + }); + + it("should get the wallet address", () => { + expect(provider.getAddress()).toBe(MOCK_ADDRESS); + }); + + it("should get the network information", () => { + expect(provider.getNetwork()).toEqual(MOCK_NETWORK); + }); + + it("should get the provider name", () => { + expect(provider.getName()).toBe("dynamic_svm_wallet_provider"); + }); + + it("should sign a message using Dynamic client", async () => { + const result = await provider.signMessage("Hello, world!"); + expect(result).toBe(MOCK_SIGNATURE_HASH); + expect(mockDynamicClient.signMessage).toHaveBeenCalledWith({ + message: "Hello, world!", + accountAddress: MOCK_ADDRESS, + }); + }); + + it("should sign a transaction using Dynamic client", async () => { + const message = MessageV0.compile({ + payerKey: new PublicKey(MOCK_ADDRESS), + instructions: [], + recentBlockhash: "test-blockhash", + }); + const transaction = new VersionedTransaction(message); + const result = await provider.signTransaction(transaction); + expect(result).toBe(transaction); + expect(mockDynamicClient.signTransaction).toHaveBeenCalledWith({ + senderAddress: MOCK_ADDRESS, + transaction, + }); + }); + + it("should send a transaction", async () => { + const message = MessageV0.compile({ + payerKey: new PublicKey(MOCK_ADDRESS), + instructions: [], + recentBlockhash: "test-blockhash", + }); + const transaction = new VersionedTransaction(message); + const result = await provider.sendTransaction(transaction); + expect(result).toBe(MOCK_TRANSACTION_HASH); + const connection = (Connection as jest.Mock).mock.results[0].value; + expect(connection.sendTransaction).toHaveBeenCalledWith(transaction); + }); + + it("should sign and send a transaction", async () => { + const message = MessageV0.compile({ + payerKey: new PublicKey(MOCK_ADDRESS), + instructions: [], + recentBlockhash: "test-blockhash", + }); + const transaction = new VersionedTransaction(message); + const result = await provider.signAndSendTransaction(transaction); + expect(result).toBe(MOCK_TRANSACTION_HASH); + expect(mockDynamicClient.signTransaction).toHaveBeenCalledWith({ + senderAddress: MOCK_ADDRESS, + transaction, + }); + const connection = (Connection as jest.Mock).mock.results[0].value; + expect(connection.sendTransaction).toHaveBeenCalledWith(transaction); + }); + + it("should get signature status", async () => { + const result = await provider.getSignatureStatus(MOCK_TRANSACTION_HASH); + expect(result).toEqual({ + context: { slot: 123 }, + value: { slot: 123, confirmations: 10, err: null }, + }); + const connection = (Connection as jest.Mock).mock.results[0].value; + expect(connection.getSignatureStatus).toHaveBeenCalledWith(MOCK_TRANSACTION_HASH, undefined); + }); + + it("should wait for signature result", async () => { + const result = await provider.waitForSignatureResult(MOCK_TRANSACTION_HASH); + expect(result).toEqual({ + context: { slot: 123 }, + value: { slot: 123, confirmations: 10, err: null }, + }); + const connection = (Connection as jest.Mock).mock.results[0].value; + expect(connection.getSignatureStatus).toHaveBeenCalledWith(MOCK_TRANSACTION_HASH); + }); + + it("should export wallet information", async () => { + const result = await provider.exportWallet(); + expect(result).toEqual({ + walletId: MOCK_ADDRESS, + chainId: undefined, + networkId: "mainnet-beta", + }); + }); + }); +}); diff --git a/typescript/agentkit/src/wallet-providers/dynamicSvmWalletProvider.ts b/typescript/agentkit/src/wallet-providers/dynamicSvmWalletProvider.ts new file mode 100644 index 000000000..52fd1aa24 --- /dev/null +++ b/typescript/agentkit/src/wallet-providers/dynamicSvmWalletProvider.ts @@ -0,0 +1,354 @@ +import { SvmWalletProvider } from "./svmWalletProvider"; +import { + clusterApiUrl, + LAMPORTS_PER_SOL, + SystemProgram, + ComputeBudgetProgram, + Connection, + PublicKey, + VersionedTransaction, + MessageV0, +} from "@solana/web3.js"; +import { SOLANA_CLUSTER_ID_BY_NETWORK_ID, SOLANA_NETWORKS } from "../network/svm"; +import { + type DynamicWalletConfig, + type DynamicWalletExport, + type DynamicWalletClient, + createDynamicWallet, +} from "./dynamicShared"; +import type { + SignatureStatus, + SignatureStatusConfig, + RpcResponseAndContext, + SignatureResult, + Cluster, +} from "@solana/web3.js"; +import type { Network } from "../network"; + +/** + * Configuration options for the Dynamic Svm wallet provider. + */ +export interface DynamicSvmWalletConfig extends DynamicWalletConfig { + /** Optional custom connection to use for the wallet */ + connection?: Connection; +} + +/** + * A wallet provider that uses Dynamic's wallet API. + * This provider extends the SvmWalletProvider to provide Dynamic-specific wallet functionality + * while maintaining compatibility with the base wallet provider interface. + */ +export class DynamicSvmWalletProvider extends SvmWalletProvider { + #accountAddress: string; + #dynamicClient: DynamicWalletClient; + #connection: Connection; + #genesisHash: string; + #publicKey: PublicKey; + + /** + * Private constructor to enforce use of factory method. + * + * @param config - The configuration options for the Dynamic wallet + */ + private constructor( + config: DynamicSvmWalletConfig & { + accountAddress: string; + dynamicClient: DynamicWalletClient; + connection: Connection; + genesisHash: string; + }, + ) { + super(); + console.log("[DynamicSvmWalletProvider] Initializing provider with:", { + accountAddress: config.accountAddress, + genesisHash: config.genesisHash, + networkId: config.networkId, + }); + + this.#accountAddress = config.accountAddress; + this.#dynamicClient = config.dynamicClient; + this.#connection = config.connection; + this.#genesisHash = config.genesisHash; + this.#publicKey = new PublicKey(config.accountAddress); + + console.log("[DynamicSvmWalletProvider] Provider initialization complete"); + } + + /** + * Creates and configures a new DynamicSvmWalletProvider instance. + * + * @param config - The configuration options for the Dynamic wallet + * @returns A configured DynamicSvmWalletProvider instance + * + * @example + * ```typescript + * const provider = await DynamicSvmWalletProvider.configureWithWallet({ + * authToken: "your-auth-token", + * environmentId: "your-environment-id", + * chainType: "solana", + * networkId: "mainnet-beta", + * thresholdSignatureScheme: ThresholdSignatureScheme.TWO_OF_TWO + * }); + * ``` + */ + public static async configureWithWallet( + config: DynamicSvmWalletConfig, + ): Promise { + // Derive cluster config from networkId using existing mappings + const clusterId = SOLANA_CLUSTER_ID_BY_NETWORK_ID[ + config.networkId as keyof typeof SOLANA_CLUSTER_ID_BY_NETWORK_ID + ] as Cluster; + if (!clusterId) { + throw new Error( + `Unsupported Solana network ID: ${config.networkId}. Use DynamicEvmWalletProvider for EVM networks.`, + ); + } + + console.log("[DynamicSvmWalletProvider] Starting wallet configuration with config:", { + networkId: config.networkId, + clusterId, + environmentId: config.environmentId, + }); + + try { + const { wallet, dynamic } = await createDynamicWallet(config, "solana"); + + console.log("[DynamicSvmWalletProvider] Wallet created:", { + accountAddress: wallet.accountAddress, + }); + + const connection = config.connection ?? new Connection(clusterApiUrl(clusterId)); + + console.log( + "[DynamicSvmWalletProvider] Connection established with endpoint:", + connection.rpcEndpoint, + ); + + const genesisHash = await connection.getGenesisHash(); + console.log("[DynamicSvmWalletProvider] Genesis hash retrieved:", genesisHash); + + const provider = new DynamicSvmWalletProvider({ + ...config, + accountAddress: wallet.accountAddress, + dynamicClient: dynamic, + connection, + genesisHash, + }); + + console.log("[DynamicSvmWalletProvider] Provider initialized with:", { + address: provider.getAddress(), + network: provider.getNetwork(), + name: provider.getName(), + }); + + return provider; + } catch (error) { + console.error("[DynamicSvmWalletProvider] Error during configuration:", error); + throw error; + } + } + + /** + * Signs a message. + * + * @param message - The message to sign + * @returns The signature + */ + public async signMessage(message: string): Promise { + return this.#dynamicClient.signMessage({ + message, + accountAddress: this.getAddress(), + }); + } + + /** + * Signs a transaction. + * + * @param transaction - The transaction to sign + * @returns The signed transaction + */ + public async signTransaction(transaction: VersionedTransaction): Promise { + // Dynamic import for ESM compatibility + const { DynamicSvmWalletClient: _DynamicSvmWalletClient } = (await import( + "@dynamic-labs-wallet/node-svm" + // eslint-disable-next-line @typescript-eslint/no-explicit-any + )) as any; + const signedTransaction = await ( + this.#dynamicClient as InstanceType + ).signTransaction({ + senderAddress: this.#accountAddress, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + transaction: transaction as any, + }); + if (!(signedTransaction instanceof VersionedTransaction)) { + throw new Error("Expected VersionedTransaction from signTransaction"); + } + return signedTransaction; + } + + /** + * Sends a transaction. + * + * @param transaction - The transaction to send + * @returns The transaction signature + */ + public async sendTransaction(transaction: VersionedTransaction): Promise { + const result = await this.#connection.sendTransaction(transaction); + return result; + } + + /** + * Signs and sends a transaction. + * + * @param transaction - The transaction to sign and send + * @returns The transaction signature + */ + public async signAndSendTransaction(transaction: VersionedTransaction): Promise { + const signedTransaction = await this.signTransaction(transaction); + return this.sendTransaction(signedTransaction); + } + + /** + * Gets the status of a transaction. + * + * @param signature - The transaction signature + * @param options - Optional configuration for the status check + * @returns The transaction status + */ + public async getSignatureStatus( + signature: string, + options?: SignatureStatusConfig, + ): Promise> { + return this.#connection.getSignatureStatus(signature, options); + } + + /** + * Waits for a transaction signature result. + * + * @param signature - The transaction signature + * @returns The transaction result + */ + public async waitForSignatureResult( + signature: string, + ): Promise> { + const status = await this.#connection.getSignatureStatus(signature); + if (!status.value) { + throw new Error(`Transaction ${signature} not found`); + } + return status as RpcResponseAndContext; + } + + /** + * Gets the network of the wallet. + * + * @returns The network + */ + public getNetwork(): Network { + return SOLANA_NETWORKS[this.#genesisHash]; + } + + /** + * Gets the name of the wallet provider. + * + * @returns The wallet provider name + */ + public getName(): string { + return "dynamic_svm_wallet_provider"; + } + + /** + * Exports the wallet information. + * + * @returns The wallet information + */ + public async exportWallet(): Promise { + return { + accountAddress: this.#accountAddress, + networkId: this.getNetwork().networkId, + }; + } + + /** + * Gets the Solana connection. + * + * @returns The Solana connection + */ + public getConnection(): Connection { + return this.#connection; + } + + /** + * Gets the public key of the wallet. + * + * @returns The public key + */ + public getPublicKey(): PublicKey { + return this.#publicKey; + } + + /** + * Gets the address of the wallet. + * + * @returns The wallet address + */ + public getAddress(): string { + return this.#accountAddress; + } + + /** + * Gets the balance of the wallet. + * + * @returns The wallet balance in lamports + */ + public async getBalance(): Promise { + const balance = await this.#connection.getBalance(this.#publicKey); + return BigInt(balance); + } + + /** + * Performs a native transfer. + * + * @param to - The recipient address + * @param value - The amount to transfer in SOL (as a decimal string, e.g. "0.0001") + * @returns The transaction signature + */ + public async nativeTransfer(to: string, value: string): Promise { + const initialBalance = await this.getBalance(); + const solAmount = Number.parseFloat(value); + const lamports = BigInt(Math.floor(solAmount * LAMPORTS_PER_SOL)); + + // Check if we have enough balance (including estimated fees) + if (initialBalance < lamports + BigInt(5000)) { + throw new Error( + `Insufficient balance. Have ${Number(initialBalance) / LAMPORTS_PER_SOL} SOL, need ${ + solAmount + 0.000005 + } SOL (including fees)`, + ); + } + + const toPubkey = new PublicKey(to); + const instructions = [ + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: 10000, + }), + ComputeBudgetProgram.setComputeUnitLimit({ + units: 2000, + }), + SystemProgram.transfer({ + fromPubkey: this.getPublicKey(), + toPubkey: toPubkey, + lamports: lamports, + }), + ]; + + const tx = new VersionedTransaction( + MessageV0.compile({ + payerKey: this.getPublicKey(), + instructions: instructions, + recentBlockhash: (await this.#connection.getLatestBlockhash()).blockhash, + }), + ); + + return this.signAndSendTransaction(tx); + } +} diff --git a/typescript/agentkit/src/wallet-providers/dynamicWalletProvider.test.ts b/typescript/agentkit/src/wallet-providers/dynamicWalletProvider.test.ts new file mode 100644 index 000000000..6f0cf71fd --- /dev/null +++ b/typescript/agentkit/src/wallet-providers/dynamicWalletProvider.test.ts @@ -0,0 +1,144 @@ +import { DynamicWalletProvider } from "./dynamicWalletProvider"; +import type { DynamicEvmWalletConfig } from "./dynamicEvmWalletProvider"; +import type { DynamicSvmWalletConfig } from "./dynamicSvmWalletProvider"; +import { DynamicEvmWalletProvider } from "./dynamicEvmWalletProvider"; +import { DynamicSvmWalletProvider } from "./dynamicSvmWalletProvider"; + +global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve({}), + } as Response), +); + +jest.mock("../analytics", () => ({ + sendAnalyticsEvent: jest.fn().mockImplementation(() => Promise.resolve()), +})); + +jest.mock("./dynamicEvmWalletProvider", () => ({ + DynamicEvmWalletProvider: { + configureWithWallet: jest.fn().mockResolvedValue({ + getAddress: jest.fn().mockReturnValue("0x742d35Cc6634C0532925a3b844Bc454e4438f44e"), + getNetwork: jest.fn().mockReturnValue({ + protocolFamily: "evm", + chainId: "1", + networkId: "mainnet", + }), + }), + }, +})); + +jest.mock("./dynamicSvmWalletProvider", () => ({ + DynamicSvmWalletProvider: { + configureWithWallet: jest.fn().mockResolvedValue({ + getAddress: jest.fn().mockReturnValue("AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM"), + getNetwork: jest.fn().mockReturnValue({ + protocolFamily: "solana", + chainId: "mainnet-beta", + networkId: "mainnet-beta", + }), + }), + }, +})); + +describe("DynamicWalletProvider", () => { + const MOCK_EVM_CONFIG: DynamicEvmWalletConfig = { + authToken: "test-auth-token", + environmentId: "test-environment-id", + chainId: "1", + networkId: "mainnet", + }; + + const MOCK_SVM_CONFIG: DynamicSvmWalletConfig = { + authToken: "test-auth-token", + environmentId: "test-environment-id", + networkId: "mainnet-beta", + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should create an EVM wallet provider by default", async () => { + const provider = await DynamicWalletProvider.configureWithWallet(MOCK_EVM_CONFIG); + + expect(DynamicEvmWalletProvider.configureWithWallet).toHaveBeenCalledWith(MOCK_EVM_CONFIG); + expect(DynamicSvmWalletProvider.configureWithWallet).not.toHaveBeenCalled(); + + expect(provider.getAddress()).toBe("0x742d35Cc6634C0532925a3b844Bc454e4438f44e"); + expect(provider.getNetwork().protocolFamily).toBe("evm"); + }); + + it("should create an EVM wallet provider when explicitly requested", async () => { + const config: DynamicEvmWalletConfig = { + ...MOCK_EVM_CONFIG, + }; + + const provider = await DynamicWalletProvider.configureWithWallet(config); + + expect(DynamicEvmWalletProvider.configureWithWallet).toHaveBeenCalledWith(config); + expect(DynamicSvmWalletProvider.configureWithWallet).not.toHaveBeenCalled(); + + expect(provider.getAddress()).toBe("0x742d35Cc6634C0532925a3b844Bc454e4438f44e"); + expect(provider.getNetwork().protocolFamily).toBe("evm"); + }); + + it("should create an SVM wallet provider when solana is specified", async () => { + const provider = await DynamicWalletProvider.configureWithWallet(MOCK_SVM_CONFIG); + + expect(DynamicSvmWalletProvider.configureWithWallet).toHaveBeenCalledWith(MOCK_SVM_CONFIG); + expect(DynamicEvmWalletProvider.configureWithWallet).not.toHaveBeenCalled(); + + expect(provider.getAddress()).toBe("AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM"); + expect(provider.getNetwork().protocolFamily).toBe("solana"); + }); + + it("should pass through all config properties", async () => { + const fullConfig: DynamicEvmWalletConfig = { + ...MOCK_EVM_CONFIG, + chainId: "5", + }; + + await DynamicWalletProvider.configureWithWallet(fullConfig); + + expect(DynamicEvmWalletProvider.configureWithWallet).toHaveBeenCalledWith(fullConfig); + }); + + it("should handle initialization failures properly", async () => { + const mockEvmConfigureWithWallet = DynamicEvmWalletProvider.configureWithWallet as jest.Mock; + + const originalImplementation = mockEvmConfigureWithWallet.getMockImplementation(); + + mockEvmConfigureWithWallet.mockImplementation(() => { + throw new Error("Auth token not found"); + }); + + await expect(DynamicWalletProvider.configureWithWallet(MOCK_EVM_CONFIG)).rejects.toThrow( + "Auth token not found", + ); + + mockEvmConfigureWithWallet.mockImplementation(originalImplementation); + }); + + it("should validate config properly", async () => { + const mockEvmConfigureWithWallet = DynamicEvmWalletProvider.configureWithWallet as jest.Mock; + const originalImplementation = mockEvmConfigureWithWallet.getMockImplementation(); + + mockEvmConfigureWithWallet.mockImplementation(config => { + if (!config.authToken) { + throw new Error("Missing required authToken"); + } + return Promise.resolve({}); + }); + + const testConfig: Partial = { + environmentId: "test-environment-id", + }; + + await expect( + DynamicWalletProvider.configureWithWallet(testConfig as DynamicEvmWalletConfig), + ).rejects.toThrow("Missing required authToken"); + + mockEvmConfigureWithWallet.mockImplementation(originalImplementation); + }); +}); diff --git a/typescript/agentkit/src/wallet-providers/dynamicWalletProvider.ts b/typescript/agentkit/src/wallet-providers/dynamicWalletProvider.ts new file mode 100644 index 000000000..53413efa0 --- /dev/null +++ b/typescript/agentkit/src/wallet-providers/dynamicWalletProvider.ts @@ -0,0 +1,49 @@ +import { DynamicEvmWalletProvider, DynamicEvmWalletConfig } from "./dynamicEvmWalletProvider"; +import { DynamicSvmWalletProvider, DynamicSvmWalletConfig } from "./dynamicSvmWalletProvider"; + +type DynamicWalletConfig = (DynamicEvmWalletConfig | DynamicSvmWalletConfig) & { + chainType?: "ethereum" | "solana"; +}; + +/** + * Factory class for creating Dynamic wallet providers. + * This class provides a unified interface for creating both EVM and SVM wallet providers. + */ +export class DynamicWalletProvider { + /** + * Creates and configures a new Dynamic wallet provider instance. + * + * @param config - The configuration options for the Dynamic wallet + * @returns A configured Dynamic wallet provider instance + * + * @example + * ```typescript + * // Create an EVM wallet provider + * const evmProvider = await DynamicWalletProvider.configureWithWallet({ + * authToken: "your-auth-token", + * environmentId: "your-environment-id", + * chainType: "ethereum", + * chainId: "84532" + * }); + * + * // Create an SVM wallet provider + * const svmProvider = await DynamicWalletProvider.configureWithWallet({ + * authToken: "your-auth-token", + * environmentId: "your-environment-id", + * chainType: "solana", + * networkId: "mainnet-beta" + * }); + * ``` + */ + public static async configureWithWallet( + config: DynamicWalletConfig, + ): Promise { + const chainType = config.chainType || "ethereum"; + + if (chainType === "ethereum") { + return DynamicEvmWalletProvider.configureWithWallet(config as DynamicEvmWalletConfig); + } else { + return DynamicSvmWalletProvider.configureWithWallet(config as DynamicSvmWalletConfig); + } + } +} diff --git a/typescript/agentkit/src/wallet-providers/index.ts b/typescript/agentkit/src/wallet-providers/index.ts index 0c727b42b..0ac796474 100644 --- a/typescript/agentkit/src/wallet-providers/index.ts +++ b/typescript/agentkit/src/wallet-providers/index.ts @@ -14,3 +14,7 @@ export * from "./privyEvmWalletProvider"; export * from "./privySvmWalletProvider"; export * from "./privyEvmDelegatedEmbeddedWalletProvider"; export * from "./zeroDevWalletProvider"; +export * from "./dynamicWalletProvider"; +export * from "./dynamicEvmWalletProvider"; +export * from "./dynamicSvmWalletProvider"; +export * from "./dynamicShared"; diff --git a/typescript/examples/langchain-dynamic-chatbot/.env-local b/typescript/examples/langchain-dynamic-chatbot/.env-local new file mode 100644 index 000000000..cc9af4974 --- /dev/null +++ b/typescript/examples/langchain-dynamic-chatbot/.env-local @@ -0,0 +1,14 @@ +OPENAI_API_KEY= + +# Dynamic Configuration - get these from your Dynamic dashboard +DYNAMIC_AUTH_TOKEN= +DYNAMIC_ENVIRONMENT_ID= + +# Optional Network ID. If you'd like to use a Dynamic Solana wallet, set to "solana-devnet". Otherwise, defaults to "base-sepolia" +NETWORK_ID= + +# Optional CDP API Key Name. If you'd like to use the CDP API, for example to faucet funds, set this to the name of the CDP API key +CDP_API_KEY_NAME= + +# Optional CDP API Key Private Key. If you'd like to use the CDP API, for example to faucet funds, set this to the private key of the CDP API key +CDP_API_KEY_PRIVATE_KEY= \ No newline at end of file diff --git a/typescript/examples/langchain-dynamic-chatbot/.eslintrc.json b/typescript/examples/langchain-dynamic-chatbot/.eslintrc.json new file mode 100644 index 000000000..91571ba7a --- /dev/null +++ b/typescript/examples/langchain-dynamic-chatbot/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": ["../../.eslintrc.base.json"] +} diff --git a/typescript/examples/langchain-dynamic-chatbot/.prettierrc b/typescript/examples/langchain-dynamic-chatbot/.prettierrc new file mode 100644 index 000000000..ffb416b74 --- /dev/null +++ b/typescript/examples/langchain-dynamic-chatbot/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/typescript/examples/langchain-dynamic-chatbot/README.md b/typescript/examples/langchain-dynamic-chatbot/README.md new file mode 100644 index 000000000..257aee14c --- /dev/null +++ b/typescript/examples/langchain-dynamic-chatbot/README.md @@ -0,0 +1,80 @@ +# Dynamic AgentKit LangChain Extension Examples - Chatbot Typescript + +This example demonstrates an agent setup as a terminal style chatbot with access to the full set of CDP AgentKit actions using Dynamic wallet provider. + +## Ask the chatbot to engage in the Web3 ecosystem! + +- "Transfer a portion of your ETH to a random address" +- "What is the price of BTC?" +- "Deploy an NFT that will go super viral!" +- "Deploy an ERC-20 token with total supply 1 billion" + +## Prerequisites + +### Checking Node Version + +Before using the example, ensure that you have the correct version of Node.js installed. The example requires Node.js 20 or higher. You can check your Node version by running: + +```bash +node --version +``` + +If you don't have the correct version, you can install it using [nvm](https://github.com/nvm-sh/nvm): + +```bash +nvm install node +``` + +This will automatically install and use the latest version of Node. + +### API Keys + +You'll need the following API keys: +- [Dynamic Account](https://www.dynamic.xyz/) and API credentials +- [OpenAI API Key](https://platform.openai.com/docs/quickstart#create-and-export-an-api-key) + +Once you have them, rename the `.env-local` file to `.env` and make sure you set the API keys to their corresponding environment variables: + +- "OPENAI_API_KEY" +- "DYNAMIC_AUTH_TOKEN" +- "DYNAMIC_ENVIRONMENT_ID" + +## Running the example + +From the root directory, run: + +```bash +pnpm install +pnpm build +``` + +This will install the dependencies and build the packages locally. The chatbot example uses the local `@coinbase/agentkit-langchain` and `@coinbase/agentkit` packages. If you make changes to the packages, you can run `pnpm build` from root again to rebuild the packages, and your changes will be reflected in the chatbot example. + +Now from the `typescript/examples/langchain-dynamic-chatbot` directory, run: + +```bash +pnpm start +``` + +Select "1. chat mode" and start telling your Agent to do things onchain! + +## Features + +- Uses Dynamic's wallet API for EVM and Solana blockchain interactions +- Integrates with LangChain for natural language processing +- Supports both interactive chat and autonomous modes +- Can perform various blockchain actions like: + - Checking wallet balance + - Sending transactions + - Interacting with smart contracts + - And more! + +## Learn More + +- [AgentKit Documentation](https://docs.cdp.coinbase.com) +- [Dynamic Documentation](https://docs.dynamic.xyz) +- [LangChain Documentation](https://js.langchain.com/docs) + +## License + +Apache-2.0 diff --git a/typescript/examples/langchain-dynamic-chatbot/chatbot.ts b/typescript/examples/langchain-dynamic-chatbot/chatbot.ts new file mode 100644 index 000000000..5852b7715 --- /dev/null +++ b/typescript/examples/langchain-dynamic-chatbot/chatbot.ts @@ -0,0 +1,350 @@ +import { + AgentKit, + DynamicEvmWalletProvider, + DynamicSvmWalletProvider, + wethActionProvider, + walletActionProvider, + erc20ActionProvider, + pythActionProvider, + cdpApiActionProvider, + splActionProvider, + x402ActionProvider, +} from "@coinbase/agentkit"; +import { getLangChainTools } from "@coinbase/agentkit-langchain"; +import { HumanMessage } from "@langchain/core/messages"; +import { MemorySaver } from "@langchain/langgraph"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { ChatOpenAI } from "@langchain/openai"; +import * as dotenv from "dotenv"; +import * as fs from "fs"; +import * as readline from "readline"; + +dotenv.config(); + +/** + * Validates that required environment variables are set + * + * @throws {Error} - If required environment variables are missing + * @returns {void} + */ +function validateEnvironment(): void { + const missingVars: string[] = []; + + // Check required variables + const requiredVars = [ + "OPENAI_API_KEY", + "DYNAMIC_AUTH_TOKEN", + "DYNAMIC_ENVIRONMENT_ID", + ]; + requiredVars.forEach(varName => { + if (!process.env[varName]) { + missingVars.push(varName); + } + }); + + // Exit if any required variables are missing + if (missingVars.length > 0) { + console.error("Error: Required environment variables are not set"); + missingVars.forEach(varName => { + console.error(`${varName}=your_${varName.toLowerCase()}_here`); + }); + process.exit(1); + } + + // Warn about optional NETWORK_ID + if (!process.env.NETWORK_ID) { + console.warn("Warning: NETWORK_ID not set, defaulting to base-sepolia testnet"); + } +} + +// Add this right after imports and before any other code +validateEnvironment(); + +type WalletData = { + accountAddress: string; + networkId: string; +}; + +/** + * Type guard to check if the wallet provider is an EVM provider + * + * @param walletProvider - The wallet provider to check + * @returns True if the wallet provider is an EVM provider, false otherwise + */ +function isEvmWalletProvider( + walletProvider: DynamicEvmWalletProvider | DynamicSvmWalletProvider, +): walletProvider is DynamicEvmWalletProvider { + return walletProvider instanceof DynamicEvmWalletProvider; +} + +/** + * Type guard to check if the wallet provider is a Solana provider + * + * @param walletProvider - The wallet provider to check + * @returns True if the wallet provider is a Solana provider, false otherwise + */ +function isSolanaWalletProvider( + walletProvider: DynamicEvmWalletProvider | DynamicSvmWalletProvider, +): walletProvider is DynamicSvmWalletProvider { + return walletProvider instanceof DynamicSvmWalletProvider; +} + +/** + * Initialize the agent with Dynamic Agentkit + * + * @returns Agent executor and config + */ +async function initializeAgent() { + try { + // Initialize LLM + const llm = new ChatOpenAI({ + model: "gpt-4o-mini", + }); + + // Configure Dynamic Wallet Provider + const networkId = process.env.NETWORK_ID || "base-sepolia"; + const isSolana = networkId.includes("solana"); + const thresholdSignatureScheme = process.env.DYNAMIC_THRESHOLD_SIGNATURE_SCHEME || "TWO_OF_TWO"; + const walletDataFile = `wallet_data_${networkId.replace(/-/g, "_")}.txt`; + + let walletData: WalletData | null = null; + + // Read existing wallet data if available + if (fs.existsSync(walletDataFile)) { + try { + walletData = JSON.parse(fs.readFileSync(walletDataFile, "utf8")) as WalletData; + console.log(`Loaded existing wallet data for ${networkId}:`, walletData.accountAddress); + } catch (error) { + console.error(`Error reading wallet data for ${networkId}:`, error); + // Continue without wallet data + } + } + + const dynamicWalletConfig = { + authToken: process.env.DYNAMIC_AUTH_TOKEN as string, + environmentId: process.env.DYNAMIC_ENVIRONMENT_ID as string, + networkId, + thresholdSignatureScheme, + accountAddress: walletData?.accountAddress, + }; + + const walletProvider = isSolana + ? await DynamicSvmWalletProvider.configureWithWallet(dynamicWalletConfig) + : await DynamicEvmWalletProvider.configureWithWallet(dynamicWalletConfig); + + + const actionProviders = [ + walletActionProvider(), + cdpApiActionProvider(), + pythActionProvider(), + ...(isEvmWalletProvider(walletProvider) + ? [ + wethActionProvider(), + erc20ActionProvider(), + x402ActionProvider(), + ] + : isSolanaWalletProvider(walletProvider) + ? [splActionProvider()] + : []), + ]; + + // Initialize AgentKit + const agentkit = await AgentKit.from({ + walletProvider, + actionProviders, + }); + + const tools = await getLangChainTools(agentkit); + + // Store buffered conversation history in memory + const memory = new MemorySaver(); + const agentConfig = { configurable: { thread_id: "Dynamic AgentKit Chatbot Example!" } }; + + // Create React Agent using the LLM and Dynamic AgentKit tools + const agent = createReactAgent({ + llm, + tools, + checkpointSaver: memory, + messageModifier: ` + You are a helpful agent that can interact onchain using the Coinbase Developer Platform AgentKit with a Dynamic wallet. You are + empowered to interact onchain using your tools. If you ever need funds, you can request them from the + faucet if you are on network ID 'base-sepolia'. If not, you can provide your wallet details and request + funds from the user. Before executing your first action, get the wallet details to see what network + you're on. If there is a 5XX (internal) HTTP error code, ask the user to try again later. If someone + asks you to do something you can't do with your currently available tools, you must say so, and + encourage them to implement it themselves using the CDP SDK + Agentkit, recommend they go to + docs.cdp.coinbase.com for more information. Be concise and helpful with your responses. Refrain from + restating your tools' descriptions unless it is explicitly requested. + `, + }); + + // Save wallet data + if (!walletData) { + const exportedWallet = await walletProvider.exportWallet(); + fs.writeFileSync( + walletDataFile, + JSON.stringify({ + accountAddress: exportedWallet.accountAddress, + networkId: exportedWallet.networkId, + } as WalletData), + ); + console.log(`Saved wallet data to ${walletDataFile}`); + } + + return { agent, config: agentConfig }; + } catch (error) { + console.error("Failed to initialize agent:", error); + throw error; // Re-throw to be handled by caller + } +} + +/** + * Run the agent autonomously with specified intervals + * + * @param agent - The agent executor + * @param config - Agent configuration + * @param interval - Time interval between actions in seconds + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function runAutonomousMode(agent: any, config: any, interval = 10) { + console.log("Starting autonomous mode..."); + + // eslint-disable-next-line no-constant-condition + while (true) { + try { + const thought = + "Be creative and do something interesting on the blockchain. " + + "Choose an action or set of actions and execute it that highlights your abilities."; + + const stream = await agent.stream({ messages: [new HumanMessage(thought)] }, config); + + for await (const chunk of stream) { + if ("agent" in chunk) { + console.log(chunk.agent.messages[0].content); + } else if ("tools" in chunk) { + console.log(chunk.tools.messages[0].content); + } + console.log("-------------------"); + } + + await new Promise(resolve => setTimeout(resolve, interval * 1000)); + } catch (error) { + if (error instanceof Error) { + console.error("Error:", error.message); + } + process.exit(1); + } + } +} + +/** + * Run the agent interactively based on user input + * + * @param agent - The agent executor + * @param config - Agent configuration + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function runChatMode(agent: any, config: any) { + console.log("Starting chat mode... Type 'exit' to end."); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const question = (prompt: string): Promise => + new Promise(resolve => rl.question(prompt, resolve)); + + try { + // eslint-disable-next-line no-constant-condition + while (true) { + const userInput = await question("\nPrompt: "); + + if (userInput.toLowerCase() === "exit") { + break; + } + + const stream = await agent.stream({ messages: [new HumanMessage(userInput)] }, config); + + for await (const chunk of stream) { + if ("agent" in chunk) { + console.log(chunk.agent.messages[0].content); + } else if ("tools" in chunk) { + console.log(chunk.tools.messages[0].content); + } + console.log("-------------------"); + } + } + } catch (error) { + if (error instanceof Error) { + console.error("Error:", error.message); + } + process.exit(1); + } finally { + rl.close(); + } +} + +/** + * Choose whether to run in autonomous or chat mode based on user input + * + * @returns Selected mode + */ +async function chooseMode(): Promise<"chat" | "auto"> { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const question = (prompt: string): Promise => + new Promise(resolve => rl.question(prompt, resolve)); + + // eslint-disable-next-line no-constant-condition + while (true) { + console.log("\nAvailable modes:"); + console.log("1. chat - Interactive chat mode"); + console.log("2. auto - Autonomous action mode"); + + const choice = (await question("\nChoose a mode (enter number or name): ")) + .toLowerCase() + .trim(); + + if (choice === "1" || choice === "chat") { + rl.close(); + return "chat"; + } else if (choice === "2" || choice === "auto") { + rl.close(); + return "auto"; + } + console.log("Invalid choice. Please try again."); + } +} + +/** + * Start the chatbot agent + */ +async function main() { + try { + const { agent, config } = await initializeAgent(); + const mode = await chooseMode(); + + if (mode === "chat") { + await runChatMode(agent, config); + } else { + await runAutonomousMode(agent, config); + } + } catch (error) { + if (error instanceof Error) { + console.error("Error:", error.message); + } + process.exit(1); + } +} + +if (require.main === module) { + console.log("Starting Agent..."); + main().catch(error => { + console.error("Fatal error:", error); + process.exit(1); + }); +} \ No newline at end of file diff --git a/typescript/examples/langchain-dynamic-chatbot/package.json b/typescript/examples/langchain-dynamic-chatbot/package.json new file mode 100644 index 000000000..d2f060475 --- /dev/null +++ b/typescript/examples/langchain-dynamic-chatbot/package.json @@ -0,0 +1,28 @@ +{ + "name": "@coinbase/dynamic-langchain-chatbot-example", + "description": "Dynamic AgentKit Node.js SDK Chatbot Example", + "version": "1.0.0", + "private": true, + "license": "Apache-2.0", + "scripts": { + "start": "NODE_OPTIONS='--no-warnings' ts-node ./chatbot.ts", + "dev": "nodemon ./chatbot.ts", + "lint": "eslint -c .eslintrc.json *.ts", + "lint:fix": "eslint -c .eslintrc.json *.ts --fix", + "format": "prettier --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"" + }, + "dependencies": { + "@coinbase/agentkit": "workspace:*", + "@coinbase/agentkit-langchain": "workspace:*", + "@langchain/langgraph": "^0.2.21", + "@langchain/openai": "^0.3.14", + "@langchain/core": "^0.3.19", + "dotenv": "^16.4.5", + "zod": "^3.22.4" + }, + "devDependencies": { + "nodemon": "^3.1.0", + "ts-node": "^10.9.2" + } +} diff --git a/typescript/examples/langchain-dynamic-chatbot/tsconfig.json b/typescript/examples/langchain-dynamic-chatbot/tsconfig.json new file mode 100644 index 000000000..6fee1565b --- /dev/null +++ b/typescript/examples/langchain-dynamic-chatbot/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "preserveSymlinks": true, + "outDir": "./dist", + "rootDir": "." + }, + "include": ["*.ts"] +} diff --git a/typescript/pnpm-lock.yaml b/typescript/pnpm-lock.yaml index 8972eac54..9f0c3c2ec 100644 --- a/typescript/pnpm-lock.yaml +++ b/typescript/pnpm-lock.yaml @@ -80,6 +80,15 @@ importers: '@coinbase/x402': specifier: ^0.6.3 version: 0.6.4(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2))(@tanstack/query-core@5.89.0)(@tanstack/react-query@5.89.0(react@18.3.1))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@18.3.1)(typescript@5.8.2)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@dynamic-labs-wallet/node': + specifier: 0.0.169 + version: 0.0.169 + '@dynamic-labs-wallet/node-evm': + specifier: 0.0.169 + version: 0.0.169(viem@2.23.15(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)) + '@dynamic-labs-wallet/node-svm': + specifier: 0.0.169 + version: 0.0.169(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) '@jup-ag/api': specifier: ^6.0.39 version: 6.0.40 @@ -91,10 +100,10 @@ importers: version: 1.18.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.23.15(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)) '@solana/spl-token': specifier: ^0.4.12 - version: 0.4.13(@solana/web3.js@1.98.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(utf-8-validate@5.0.10) + version: 0.4.13(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(utf-8-validate@5.0.10) '@solana/web3.js': - specifier: ^1.98.1 - version: 1.98.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) + specifier: ^1.98.2 + version: 1.98.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) '@zerodev/ecdsa-validator': specifier: ^5.4.5 version: 5.4.5(@zerodev/sdk@5.4.28(viem@2.23.15(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)))(viem@2.23.15(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)) @@ -118,7 +127,7 @@ importers: version: 2.1.0 clanker-sdk: specifier: ^4.1.18 - version: 4.1.19(@types/node@20.17.27)(typescript@5.8.2)(viem@2.23.15(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)) + version: 4.1.19(@types/node@22.13.14)(typescript@5.8.2)(viem@2.23.15(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)) decimal.js: specifier: ^10.5.0 version: 10.5.0 @@ -176,7 +185,7 @@ importers: version: 14.1.1 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.17.27)(ts-node@10.9.2(@types/node@20.17.27)(typescript@5.8.2)) + version: 29.7.0(@types/node@22.13.14)(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.8.2)) mock-fs: specifier: ^5.2.0 version: 5.5.0 @@ -194,7 +203,7 @@ importers: version: 2.4.2 ts-jest: specifier: ^29.2.5 - version: 29.3.0(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(jest@29.7.0(@types/node@20.17.27)(ts-node@10.9.2(@types/node@20.17.27)(typescript@5.8.2)))(typescript@5.8.2) + version: 29.3.0(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(jest@29.7.0(@types/node@22.13.14)(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.8.2)))(typescript@5.8.2) tsd: specifier: ^0.31.2 version: 0.31.2 @@ -307,6 +316,37 @@ importers: specifier: ^10.9.2 version: 10.9.2(@types/node@22.13.14)(typescript@5.8.2) + examples/langchain-dynamic-chatbot: + dependencies: + '@coinbase/agentkit': + specifier: workspace:* + version: link:../../agentkit + '@coinbase/agentkit-langchain': + specifier: workspace:* + version: link:../../framework-extensions/langchain + '@langchain/core': + specifier: ^0.3.19 + version: 0.3.30(openai@4.89.1(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.56)) + '@langchain/langgraph': + specifier: ^0.2.21 + version: 0.2.74(@langchain/core@0.3.30(openai@4.89.1(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.56)))(react@18.3.1)(zod-to-json-schema@3.24.5(zod@3.25.56)) + '@langchain/openai': + specifier: ^0.3.14 + version: 0.3.17(@langchain/core@0.3.30(openai@4.89.1(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.56)))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + dotenv: + specifier: ^16.4.5 + version: 16.4.7 + zod: + specifier: ^3.22.4 + version: 3.25.56 + devDependencies: + nodemon: + specifier: ^3.1.0 + version: 3.1.9 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@22.13.14)(typescript@5.8.2) + examples/langchain-farcaster-chatbot: dependencies: '@coinbase/agentkit': @@ -966,6 +1006,26 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@dynamic-labs-wallet/core@0.0.169': + resolution: {integrity: sha512-NT/sWD0K6VQy4BG3gjnTTg4hFXtYEhmCT1jPUADpbWkKhrf2DPGlfeIcDA6quvBvWVg4oagAGU4P2N0fTKiNxA==} + + '@dynamic-labs-wallet/node-evm@0.0.169': + resolution: {integrity: sha512-Q/7Ln+/iB2SgFOTT/CgGj2K0wozyyjxryuG0ORicLFAS7xTyRs442zHusmXj6UMAm5Zps0dao6UEHVVhiTHRkw==} + peerDependencies: + viem: ^2.22.1 + + '@dynamic-labs-wallet/node-svm@0.0.169': + resolution: {integrity: sha512-TCHL1TFN+GZpOZxH8xUZHoyE8ETV09nnaWsbcxlFzePslInKXRNa75sRW+Z8TjIsfkucr+bnADrD+jgBNNopCw==} + + '@dynamic-labs-wallet/node@0.0.169': + resolution: {integrity: sha512-3CuqNIhRobxtkVrun+fRplButEfRoLyD3kbfQiZIOK+Kf3atcKGcQnbJrraTiRpBqGHwMaD/dupMyPFk6R93Xw==} + + '@dynamic-labs/logger@4.32.1': + resolution: {integrity: sha512-6A1xrzQiE5yMOt/3HAzLlzNhMKBfpDEgy/gqfxWOKe+ZfqOwMboC+R6QNd0SWo75agUBfipRCiflPioH4ELVcA==} + + '@dynamic-labs/sdk-api-core@0.0.764': + resolution: {integrity: sha512-79JptJTTClLc9qhioThtwMuzTHJ+mrj8sTEglb7Mcx3lJub9YbXqNdzS9mLRxZsr2et3aqqpzymXdUBzSEaMng==} + '@ecies/ciphers@0.2.4': resolution: {integrity: sha512-t+iX+Wf5nRKyNzk8dviW3Ikb/280+aEJAnw9YXvCp2tYGPSkMki+NRY+8aNLmVFv3eNtMdvViPNOPxS8SZNP+w==} engines: {bun: '>=1', deno: '>=2', node: '>=16'} @@ -1401,6 +1461,12 @@ packages: peerDependencies: '@langchain/core': '>=0.2.31 <0.4.0' + '@langchain/langgraph-checkpoint@0.0.18': + resolution: {integrity: sha512-IS7zJj36VgY+4pf8ZjsVuUWef7oTwt1y9ylvwu0aLuOn1d0fg05Om9DLm3v2GZ2Df6bhLV1kfWAM0IAl9O5rQQ==} + engines: {node: '>=18'} + peerDependencies: + '@langchain/core': '>=0.2.31 <0.4.0' + '@langchain/langgraph-sdk@0.0.60': resolution: {integrity: sha512-7ndeAdw1afVY72HpKEGw7AyuDlD7U3e4jxaJflxA+PXaFPiE0d/hQYvlPT84YmvqNzJN605hv7YcrOju2573bQ==} peerDependencies: @@ -1422,6 +1488,16 @@ packages: zod-to-json-schema: optional: true + '@langchain/langgraph@0.2.74': + resolution: {integrity: sha512-oHpEi5sTZTPaeZX1UnzfM2OAJ21QGQrwReTV6+QnX7h8nDCBzhtipAw1cK616S+X8zpcVOjgOtJuaJhXa4mN8w==} + engines: {node: '>=18'} + peerDependencies: + '@langchain/core': '>=0.2.36 <0.3.0 || >=0.3.40 < 0.4.0' + zod-to-json-schema: ^3.x + peerDependenciesMeta: + zod-to-json-schema: + optional: true + '@langchain/openai@0.3.17': resolution: {integrity: sha512-uw4po32OKptVjq+CYHrumgbfh4NuD7LqyE+ZgqY9I/LrLc6bHLMc+sisHmI17vgek0K/yqtarI0alPJbzrwyag==} engines: {node: '>=18'} @@ -1819,12 +1895,6 @@ packages: peerDependencies: typescript: '>=5' - '@solana/codecs-core@2.1.0': - resolution: {integrity: sha512-SR7pKtmJBg2mhmkel2NeHA1pz06QeQXdMv8WJoIR9m8F/hw80K/612uaYbwTt2nkK0jg/Qn/rNSd7EcJ4SBGjw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5' - '@solana/codecs-core@2.3.0': resolution: {integrity: sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==} engines: {node: '>=20.18.0'} @@ -1847,12 +1917,6 @@ packages: peerDependencies: typescript: '>=5' - '@solana/codecs-numbers@2.1.0': - resolution: {integrity: sha512-XMu4yw5iCgQnMKsxSWPPOrGgtaohmupN3eyAtYv3K3C/MJEc5V90h74k5B1GUCiHvcrdUDO9RclNjD9lgbjFag==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5' - '@solana/codecs-numbers@2.3.0': resolution: {integrity: sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==} engines: {node: '>=20.18.0'} @@ -1889,13 +1953,6 @@ packages: peerDependencies: typescript: '>=5' - '@solana/errors@2.1.0': - resolution: {integrity: sha512-l+GxAv0Ar4d3c3PlZdA9G++wFYZREEbbRyAFP8+n8HSg0vudCuzogh/13io6hYuUhG/9Ve8ARZNamhV7UScKNw==} - engines: {node: '>=20.18.0'} - hasBin: true - peerDependencies: - typescript: '>=5' - '@solana/errors@2.3.0': resolution: {integrity: sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==} engines: {node: '>=20.18.0'} @@ -2092,8 +2149,8 @@ packages: '@solana/web3.js@1.98.0': resolution: {integrity: sha512-nz3Q5OeyGFpFCR+erX2f6JPt3sKhzhYcSycBCSPkWjzSVDh/Rr1FqTVMRe58FKO16/ivTUcuJjeS5MyBvpkbzA==} - '@solana/web3.js@1.98.1': - resolution: {integrity: sha512-gRAq1YPbfSDAbmho4kY7P/8iLIjMWXAzBJdP9iENFR+dFQSBSueHzjK/ou8fxhqHP9j+J4Msl4p/oDemFcIjlg==} + '@solana/web3.js@1.98.4': + resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==} '@stablelib/base64@1.0.1': resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} @@ -2974,10 +3031,6 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} - commander@13.1.0: - resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} - engines: {node: '>=18'} - commander@14.0.1: resolution: {integrity: sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==} engines: {node: '>=20'} @@ -5979,6 +6032,10 @@ packages: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -6756,8 +6813,8 @@ snapshots: '@coinbase/cdp-sdk@1.38.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(utf-8-validate@5.0.10)': dependencies: - '@solana/spl-token': 0.4.13(@solana/web3.js@1.98.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.98.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) + '@solana/spl-token': 0.4.13(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) abitype: 1.0.6(typescript@5.8.2)(zod@3.25.56) axios: 1.12.2 axios-retry: 4.5.0(axios@1.12.2) @@ -6876,6 +6933,49 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@dynamic-labs-wallet/core@0.0.169': + dependencies: + '@dynamic-labs/sdk-api-core': 0.0.764 + axios: 1.9.0 + uuid: 11.1.0 + transitivePeerDependencies: + - debug + + '@dynamic-labs-wallet/node-evm@0.0.169(viem@2.23.15(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2))': + dependencies: + '@dynamic-labs-wallet/node': 0.0.169 + '@dynamic-labs/sdk-api-core': 0.0.764 + viem: 2.23.15(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) + transitivePeerDependencies: + - debug + + '@dynamic-labs-wallet/node-svm@0.0.169(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)': + dependencies: + '@dynamic-labs-wallet/node': 0.0.169 + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - debug + - encoding + - typescript + - utf-8-validate + + '@dynamic-labs-wallet/node@0.0.169': + dependencies: + '@dynamic-labs-wallet/core': 0.0.169 + '@dynamic-labs/logger': 4.32.1 + '@dynamic-labs/sdk-api-core': 0.0.764 + '@noble/hashes': 1.7.1 + uuid: 11.1.0 + transitivePeerDependencies: + - debug + + '@dynamic-labs/logger@4.32.1': + dependencies: + eventemitter3: 5.0.1 + + '@dynamic-labs/sdk-api-core@0.0.764': {} + '@ecies/ciphers@0.2.4(@noble/ciphers@1.3.0)': dependencies: '@noble/ciphers': 1.3.0 @@ -7293,12 +7393,12 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} - '@inquirer/external-editor@1.0.1(@types/node@20.17.27)': + '@inquirer/external-editor@1.0.1(@types/node@22.13.14)': dependencies: chardet: 2.1.0 iconv-lite: 0.6.3 optionalDependencies: - '@types/node': 20.17.27 + '@types/node': 22.13.14 '@istanbuljs/load-nyc-config@1.1.0': dependencies: @@ -7354,6 +7454,41 @@ snapshots: - supports-color - ts-node + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.8.2))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.17.27 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.17.27)(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.8.2)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + '@jest/environment@29.7.0': dependencies: '@jest/fake-timers': 29.7.0 @@ -7584,6 +7719,11 @@ snapshots: '@langchain/core': 0.3.30(openai@4.89.1(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.56)) uuid: 10.0.0 + '@langchain/langgraph-checkpoint@0.0.18(@langchain/core@0.3.30(openai@4.89.1(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.56)))': + dependencies: + '@langchain/core': 0.3.30(openai@4.89.1(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.56)) + uuid: 10.0.0 + '@langchain/langgraph-sdk@0.0.60(@langchain/core@0.3.30(openai@4.89.1(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.56)))(react@18.3.1)': dependencies: '@types/json-schema': 7.0.15 @@ -7672,6 +7812,18 @@ snapshots: transitivePeerDependencies: - react + '@langchain/langgraph@0.2.74(@langchain/core@0.3.30(openai@4.89.1(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.56)))(react@18.3.1)(zod-to-json-schema@3.24.5(zod@3.25.56))': + dependencies: + '@langchain/core': 0.3.30(openai@4.89.1(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.56)) + '@langchain/langgraph-checkpoint': 0.0.18(@langchain/core@0.3.30(openai@4.89.1(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.56))) + '@langchain/langgraph-sdk': 0.0.60(@langchain/core@0.3.30(openai@4.89.1(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.56)))(react@18.3.1) + uuid: 10.0.0 + zod: 3.25.56 + optionalDependencies: + zod-to-json-schema: 3.24.5(zod@3.25.56) + transitivePeerDependencies: + - react + '@langchain/openai@0.3.17(@langchain/core@0.3.30(openai@4.89.1(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.56)))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@langchain/core': 0.3.30(openai@4.89.1(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.56)) @@ -8039,7 +8191,7 @@ snapshots: dependencies: '@noble/curves': 1.8.1 '@noble/hashes': 1.7.1 - '@solana/web3.js': 1.98.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) canonicalize: 2.1.0 dotenv: 16.4.7 jose: 4.15.9 @@ -8485,7 +8637,7 @@ snapshots: '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 - '@solana/web3.js': 1.98.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) bigint-buffer: 1.1.5 bignumber.js: 9.1.2 transitivePeerDependencies: @@ -8503,11 +8655,6 @@ snapshots: '@solana/errors': 2.0.0-rc.1(typescript@5.8.2) typescript: 5.8.2 - '@solana/codecs-core@2.1.0(typescript@5.8.2)': - dependencies: - '@solana/errors': 2.1.0(typescript@5.8.2) - typescript: 5.8.2 - '@solana/codecs-core@2.3.0(typescript@5.8.2)': dependencies: '@solana/errors': 2.3.0(typescript@5.8.2) @@ -8533,12 +8680,6 @@ snapshots: '@solana/errors': 2.0.0-rc.1(typescript@5.8.2) typescript: 5.8.2 - '@solana/codecs-numbers@2.1.0(typescript@5.8.2)': - dependencies: - '@solana/codecs-core': 2.1.0(typescript@5.8.2) - '@solana/errors': 2.1.0(typescript@5.8.2) - typescript: 5.8.2 - '@solana/codecs-numbers@2.3.0(typescript@5.8.2)': dependencies: '@solana/codecs-core': 2.3.0(typescript@5.8.2) @@ -8589,12 +8730,6 @@ snapshots: commander: 12.1.0 typescript: 5.8.2 - '@solana/errors@2.1.0(typescript@5.8.2)': - dependencies: - chalk: 5.4.1 - commander: 13.1.0 - typescript: 5.8.2 - '@solana/errors@2.3.0(typescript@5.8.2)': dependencies: chalk: 5.4.1 @@ -8828,29 +8963,29 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/spl-token-group@0.0.7(@solana/web3.js@1.98.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)': + '@solana/spl-token-group@0.0.7(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)': dependencies: '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) - '@solana/web3.js': 1.98.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) transitivePeerDependencies: - fastestsmallesttextencoderdecoder - typescript - '@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.98.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)': + '@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)': dependencies: '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) - '@solana/web3.js': 1.98.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) transitivePeerDependencies: - fastestsmallesttextencoderdecoder - typescript - '@solana/spl-token@0.4.13(@solana/web3.js@1.98.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(utf-8-validate@5.0.10)': + '@solana/spl-token@0.4.13(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) - '@solana/spl-token-group': 0.0.7(@solana/web3.js@1.98.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) - '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.98.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) - '@solana/web3.js': 1.98.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) + '@solana/spl-token-group': 0.0.7(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) + '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.2) + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10) buffer: 6.0.3 transitivePeerDependencies: - bufferutil @@ -8946,13 +9081,13 @@ snapshots: - encoding - utf-8-validate - '@solana/web3.js@1.98.1(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)': + '@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)': dependencies: '@babel/runtime': 7.27.0 - '@noble/curves': 1.8.1 - '@noble/hashes': 1.7.1 + '@noble/curves': 1.9.6 + '@noble/hashes': 1.8.0 '@solana/buffer-layout': 4.0.1 - '@solana/codecs-numbers': 2.1.0(typescript@5.8.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.8.2) agentkeepalive: 4.6.0 bn.js: 5.2.1 borsh: 0.7.0 @@ -10402,12 +10537,12 @@ snapshots: cjs-module-lexer@1.4.3: {} - clanker-sdk@4.1.19(@types/node@20.17.27)(typescript@5.8.2)(viem@2.23.15(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)): + clanker-sdk@4.1.19(@types/node@22.13.14)(typescript@5.8.2)(viem@2.23.15(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)): dependencies: '@openzeppelin/merkle-tree': 1.0.8 abitype: 1.0.8(typescript@5.8.2)(zod@3.25.56) dotenv: 16.4.7 - inquirer: 8.2.7(@types/node@20.17.27) + inquirer: 8.2.7(@types/node@22.13.14) viem: 2.23.15(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) zod: 3.25.56 transitivePeerDependencies: @@ -10465,8 +10600,6 @@ snapshots: commander@12.1.0: {} - commander@13.1.0: {} - commander@14.0.1: {} commander@2.20.3: {} @@ -10525,6 +10658,21 @@ snapshots: - supports-color - ts-node + create-jest@29.7.0(@types/node@22.13.14)(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.8.2)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@22.13.14)(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.8.2)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + create-require@1.1.1: {} cross-fetch@3.2.0: @@ -11636,9 +11784,9 @@ snapshots: inherits@2.0.4: {} - inquirer@8.2.7(@types/node@20.17.27): + inquirer@8.2.7(@types/node@22.13.14): dependencies: - '@inquirer/external-editor': 1.0.1(@types/node@20.17.27) + '@inquirer/external-editor': 1.0.1(@types/node@22.13.14) ansi-escapes: 4.3.2 chalk: 4.1.2 cli-cursor: 3.1.0 @@ -11959,6 +12107,25 @@ snapshots: - supports-color - ts-node + jest-cli@29.7.0(@types/node@22.13.14)(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.8.2)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.8.2)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@22.13.14)(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.8.2)) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@22.13.14)(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.8.2)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest-config@29.7.0(@types/node@20.17.27)(ts-node@10.9.2(@types/node@20.17.27)(typescript@5.8.2)): dependencies: '@babel/core': 7.26.10 @@ -11990,6 +12157,68 @@ snapshots: - babel-plugin-macros - supports-color + jest-config@29.7.0(@types/node@20.17.27)(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.8.2)): + dependencies: + '@babel/core': 7.26.10 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.26.10) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.17.27 + ts-node: 10.9.2(@types/node@22.13.14)(typescript@5.8.2) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-config@29.7.0(@types/node@22.13.14)(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.8.2)): + dependencies: + '@babel/core': 7.26.10 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.26.10) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.13.14 + ts-node: 10.9.2(@types/node@22.13.14)(typescript@5.8.2) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-diff@29.7.0: dependencies: chalk: 4.1.2 @@ -12217,6 +12446,18 @@ snapshots: - supports-color - ts-node + jest@29.7.0(@types/node@22.13.14)(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.8.2)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.8.2)) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@22.13.14)(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.8.2)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jose@4.15.9: {} jose@5.10.0: {} @@ -13767,6 +14008,26 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.26.10) + ts-jest@29.3.0(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(jest@29.7.0(@types/node@22.13.14)(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.8.2)))(typescript@5.8.2): + dependencies: + bs-logger: 0.2.6 + ejs: 3.1.10 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@22.13.14)(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.8.2)) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.1 + type-fest: 4.38.0 + typescript: 5.8.2 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.26.10 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.26.10) + ts-node@10.9.2(@types/node@18.19.83)(typescript@5.8.2): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -14056,6 +14317,8 @@ snapshots: uuid@10.0.0: {} + uuid@11.1.0: {} + uuid@8.3.2: {} uuid@9.0.1: {}