diff --git a/README.md b/README.md
index cc58a69d4..ddea43b92 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
+
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 9d41f9392..f2e5800fa 100644
--- a/typescript/agentkit/package.json
+++ b/typescript/agentkit/package.json
@@ -43,16 +43,19 @@
"@alloralabs/allora-sdk": "^0.1.0",
"@coinbase/cdp-sdk": "^1.34.0",
"@coinbase/coinbase-sdk": "^0.20.0",
+ "@dynamic-labs-wallet/node-evm": "0.0.0-preview.160.0",
+ "@dynamic-labs-wallet/node-svm": "0.0.0-preview.160.0",
+ "@dynamic-labs-wallet/node": "0.0.0-preview.160.0",
"@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",
"@zerodev/ecdsa-validator": "^5.4.5",
"@zerodev/intent": "^0.0.24",
"@zerodev/sdk": "^5.4.28",
"@zoralabs/coins-sdk": "^0.2.8",
"axios": "^1.9.0",
+ "@solana/web3.js": "^1.98.2",
"bs58": "^4.0.1",
"canonicalize": "^2.1.0",
"decimal.js": "^10.5.0",
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..e33cd50d4
--- /dev/null
+++ b/typescript/agentkit/src/wallet-providers/dynamicEvmWalletProvider.test.ts
@@ -0,0 +1,204 @@
+import { DynamicEvmWalletProvider } from "./dynamicEvmWalletProvider";
+import { DynamicEvmWalletClient } from "@dynamic-labs-wallet/node-evm";
+import { ThresholdSignatureScheme } from "@dynamic-labs-wallet/node";
+import type { Address, Hex } from "viem";
+import { createWalletClient, http } from "viem";
+import { getChain } from "../network/network";
+import { createDynamicWallet } from "./dynamicShared";
+
+jest.mock("@dynamic-labs-wallet/node-evm");
+jest.mock("viem");
+jest.mock("../network/network");
+jest.mock("./dynamicShared");
+
+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",
+ chainType: "ethereum" as const,
+ thresholdSignatureScheme: 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),
+ exportPrivateKey: jest.fn().mockResolvedValue({ derivedPrivateKey: "0xprivate" }),
+ importPrivateKey: jest.fn().mockResolvedValue({
+ accountAddress: MOCK_ADDRESS,
+ publicKeyHex: "0x123",
+ }),
+ };
+
+ const mockWalletClient = {
+ account: {
+ address: MOCK_ADDRESS,
+ type: "json-rpc",
+ },
+ chain: {
+ id: 84532,
+ name: "Base Goerli",
+ rpcUrls: {
+ default: { http: ["https://goerli.base.org"] },
+ },
+ nativeCurrency: {
+ name: "Ether",
+ symbol: "ETH",
+ decimals: 18,
+ },
+ },
+ signMessage: jest.fn().mockResolvedValue(MOCK_SIGNATURE_HASH),
+ signTypedData: jest.fn().mockResolvedValue(MOCK_SIGNATURE_HASH),
+ signTransaction: jest.fn().mockResolvedValue(MOCK_SIGNATURE_HASH),
+ sendTransaction: jest.fn().mockResolvedValue(MOCK_TRANSACTION_HASH),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ // Mock DynamicEvmWalletClient
+ (DynamicEvmWalletClient as jest.Mock).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 createWalletClient
+ (createWalletClient as jest.Mock).mockReturnValue(mockWalletClient);
+
+ // 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,
+ chainType: "ethereum",
+ });
+
+ expect(mockDynamicClient.createViemPublicClient).toHaveBeenCalledWith({
+ chain: expect.any(Object),
+ });
+
+ expect(getChain).toHaveBeenCalledWith(MOCK_CONFIG.chainId);
+ expect(createWalletClient).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, ...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 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 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({
+ walletId: MOCK_ADDRESS,
+ chainId: MOCK_CONFIG.chainId,
+ networkId: "base-sepolia",
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/typescript/agentkit/src/wallet-providers/dynamicEvmWalletProvider.ts b/typescript/agentkit/src/wallet-providers/dynamicEvmWalletProvider.ts
new file mode 100644
index 000000000..44d4cf8b5
--- /dev/null
+++ b/typescript/agentkit/src/wallet-providers/dynamicEvmWalletProvider.ts
@@ -0,0 +1,179 @@
+import { ViemWalletProvider } from "./viemWalletProvider";
+import { createWalletClient, http, type WalletClient } from "viem";
+import { getChain } from "../network/network";
+import {
+ type DynamicWalletConfig,
+ type DynamicWalletExport,
+ createDynamicWallet,
+} from "./dynamicShared";
+import type { DynamicEvmWalletClient } from "@dynamic-labs-wallet/node-evm";
+import { ThresholdSignatureScheme } from "@dynamic-labs-wallet/node";
+/**
+ * Configuration options for the Dynamic wallet provider.
+ *
+ * @interface
+ */
+export interface DynamicEvmWalletConfig extends DynamicWalletConfig {
+ /** Optional chain ID to connect to */
+ chainId?: string;
+}
+
+/**
+ * A wallet provider that uses Dynamic's wallet API.
+ * This provider extends the ViemWalletProvider to provide Dynamic-specific wallet functionality
+ * while maintaining compatibility with the base wallet provider interface.
+ */
+export class DynamicEvmWalletProvider extends ViemWalletProvider {
+ #accountAddress: string;
+ #dynamicClient: DynamicEvmWalletClient;
+
+ /**
+ * Private constructor to enforce use of factory method.
+ *
+ * @param walletClient - The Viem wallet client instance
+ * @param config - The configuration options for the Dynamic wallet
+ */
+ private constructor(
+ walletClient: WalletClient,
+ config: DynamicWalletConfig & { accountAddress: string },
+ dynamicClient: DynamicEvmWalletClient,
+ ) {
+ super(walletClient);
+ this.#accountAddress = config.accountAddress;
+ this.#dynamicClient = dynamicClient;
+ }
+
+ /**
+ * 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",
+ * baseApiUrl: "https://app.dynamicauth.com",
+ * baseMPCRelayApiUrl: "relay.dynamicauth.com",
+ * chainType: "ethereum",
+ * chainId: "84532",
+ * thresholdSignatureScheme: ThresholdSignatureScheme.TWO_OF_TWO
+ * });
+ * ```
+ */
+ public static async configureWithWallet(
+ config: DynamicEvmWalletConfig,
+ ): Promise {
+ const { wallet, dynamic } = await createDynamicWallet({
+ ...config,
+ chainType: "ethereum",
+ });
+
+ const chainId = config.chainId || "84532";
+ const chain = getChain(chainId);
+ if (!chain) {
+ throw new Error(`Chain with ID ${chainId} not found`);
+ }
+
+ const publicClient = (dynamic as DynamicEvmWalletClient).createViemPublicClient({
+ chain,
+ });
+
+ const walletClient = createWalletClient({
+ account: {
+ address: wallet.accountAddress as `0x${string}`,
+ type: "json-rpc",
+ },
+ chain,
+ transport: http(),
+ });
+
+ return new DynamicEvmWalletProvider(walletClient, {
+ ...config,
+ accountAddress: wallet.accountAddress,
+ }, dynamic as DynamicEvmWalletClient);
+ }
+
+ /**
+ * Signs a message using the wallet's private key.
+ *
+ * @param message - The message to sign
+ * @returns The signature as a hex string with 0x prefix
+ */
+ public async signMessage(message: string): Promise<`0x${string}`> {
+ const signature = await this.#dynamicClient.signMessage({
+ message,
+ accountAddress: this.#accountAddress,
+ });
+ return signature as `0x${string}`;
+ }
+
+ /**
+ * 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;
+ }> {
+ const result = await this.#dynamicClient.importPrivateKey({
+ privateKey,
+ chainName: "EVM",
+ thresholdSignatureScheme: ThresholdSignatureScheme.TWO_OF_TWO,
+ password,
+ });
+ return {
+ accountAddress: result.accountAddress,
+ publicKeyHex: result.publicKeyHex,
+ };
+ }
+
+ /**
+ * Exports the wallet information.
+ *
+ * @returns The wallet information
+ */
+ public async exportWallet(): Promise {
+ return {
+ walletId: this.#accountAddress,
+ chainId: this.getNetwork().chainId,
+ networkId: this.getNetwork().networkId,
+ };
+ }
+
+ /**
+ * Gets the name of the provider.
+ *
+ * @returns The provider name
+ */
+ public getName(): string {
+ return "dynamic_evm_wallet_provider";
+ }
+
+ /**
+ * Gets the address of the wallet.
+ *
+ * @returns The wallet address
+ */
+ public getAddress(): string {
+ return this.#accountAddress;
+ }
+}
\ No newline at end of file
diff --git a/typescript/agentkit/src/wallet-providers/dynamicShared.ts b/typescript/agentkit/src/wallet-providers/dynamicShared.ts
new file mode 100644
index 000000000..eec17268d
--- /dev/null
+++ b/typescript/agentkit/src/wallet-providers/dynamicShared.ts
@@ -0,0 +1,134 @@
+import { DynamicEvmWalletClient } from "@dynamic-labs-wallet/node-evm";
+import { DynamicSvmWalletClient } from "@dynamic-labs-wallet/node-svm";
+import type { ThresholdSignatureScheme } from "@dynamic-labs-wallet/node";
+/**
+ * Configuration options for the Dynamic wallet provider.
+ *
+ * @interface
+ */
+export interface DynamicWalletConfig {
+ /** The Dynamic authentication token */
+ authToken: string;
+ /** The Dynamic environment ID */
+ environmentId: string;
+ /** The base API URL for Dynamic */
+ baseApiUrl: string;
+ /** The base MPC relay API URL for Dynamic */
+ baseMPCRelayApiUrl: string;
+ /** The ID of the wallet to use, if not provided a new wallet will be created */
+ walletId?: string;
+ /** The chain ID to use for the wallet (for EVM) */
+ chainId?: string;
+ /** The network ID to use for the wallet (for SVM) */
+ networkId?: string;
+ /** The type of wallet to create */
+ chainType: "ethereum" | "solana";
+ /** The threshold signature scheme to use for wallet creation */
+ thresholdSignatureScheme?: ThresholdSignatureScheme;
+ /** Optional password for encrypted backup shares */
+ password?: string;
+}
+
+export type DynamicWalletExport = {
+ walletId: string;
+ chainId: string | undefined;
+ networkId: string | undefined;
+};
+
+type CreateDynamicWalletReturnType = {
+ wallet: {
+ accountAddress: string;
+ publicKeyHex?: string; // Only for EVM
+ rawPublicKey: Uint8Array;
+ externalServerKeyShares: unknown[]; // Specify a more appropriate type if known
+ };
+ dynamic: DynamicEvmWalletClient | DynamicSvmWalletClient;
+};
+
+/**
+ * Create a Dynamic client based on the chain type
+ *
+ * @param config - The configuration options for the Dynamic client
+ * @returns The created Dynamic client
+ */
+export const createDynamicClient = async (config: DynamicWalletConfig) => {
+ const clientConfig = {
+ authToken: config.authToken,
+ environmentId: config.environmentId,
+ baseApiUrl: config.baseApiUrl,
+ baseMPCRelayApiUrl: config.baseMPCRelayApiUrl,
+ };
+
+ try {
+ const client = config.chainType === "ethereum"
+ ? new DynamicEvmWalletClient(clientConfig)
+ : new DynamicSvmWalletClient(clientConfig);
+
+ console.log("[createDynamicClient] Client created successfully");
+ await client.authenticateApiToken(config.authToken);
+ console.log("[createDynamicClient] Client authenticated successfully");
+ 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
+ * @returns The created Dynamic wallet and client
+ */
+export const createDynamicWallet = async (config: DynamicWalletConfig): Promise => {
+ console.log("[createDynamicWallet] Starting wallet creation with config:", {
+ chainType: config.chainType,
+ networkId: config.networkId
+ });
+
+ if (!config.thresholdSignatureScheme) {
+ throw new Error("thresholdSignatureScheme is required for wallet creation");
+ }
+
+ const client = await createDynamicClient(config);
+ console.log("[createDynamicWallet] Dynamic client created");
+
+ let wallet: CreateDynamicWalletReturnType['wallet'];
+ if (config.walletId) {
+ console.log("[createDynamicWallet] Using existing wallet ID:", config.walletId);
+ if (config.chainType === "solana") {
+ const svmClient = client as DynamicSvmWalletClient;
+ const result = await svmClient.deriveAccountAddress(new TextEncoder().encode(config.walletId));
+ wallet = {
+ accountAddress: result.accountAddress,
+ rawPublicKey: new Uint8Array(),
+ externalServerKeyShares: []
+ };
+ } else {
+ throw new Error("deriveAccountAddress is only supported for Solana wallets");
+ }
+ } else {
+ console.log("[createDynamicWallet] Creating new wallet");
+ console.log("[createDynamicWallet] createWalletAccount params:", {
+ thresholdSignatureScheme: config.thresholdSignatureScheme,
+ password: config.password ? "***" : undefined,
+ networkId: config.networkId,
+ chainType: config.chainType
+ });
+ const result = await client.createWalletAccount({
+ thresholdSignatureScheme: config.thresholdSignatureScheme,
+ password: config.password,
+ });
+ wallet = {
+ accountAddress: result.accountAddress,
+ rawPublicKey: result.rawPublicKey,
+ externalServerKeyShares: result.externalServerKeyShares
+ };
+ }
+
+ console.log("[createDynamicWallet] Wallet created/retrieved:", {
+ accountAddress: wallet.accountAddress
+ });
+
+ return { wallet, dynamic: client };
+};
\ No newline at end of file
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..eed8a558f
--- /dev/null
+++ b/typescript/agentkit/src/wallet-providers/dynamicSvmWalletProvider.test.ts
@@ -0,0 +1,300 @@
+import { DynamicSvmWalletProvider } from "./dynamicSvmWalletProvider";
+import { DynamicSvmWalletClient } from "@dynamic-labs-wallet/node-svm";
+import { Connection, clusterApiUrl, PublicKey, VersionedTransaction, MessageV0 } from "@solana/web3.js";
+import { createDynamicWallet } from "./dynamicShared";
+import { ThresholdSignatureScheme } from "@dynamic-labs-wallet/node";
+
+jest.mock("@dynamic-labs-wallet/node-svm");
+jest.mock("../network/svm", () => ({
+ SOLANA_CLUSTER_ID_BY_NETWORK_ID: {
+ "": "mainnet-beta",
+ "mainnet-beta": "mainnet-beta",
+ "testnet": "testnet",
+ "devnet": "devnet",
+ },
+}));
+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("./dynamicShared");
+
+const MOCK_ADDRESS = "test-address";
+const MOCK_TRANSACTION_HASH = "test-tx-hash";
+const MOCK_SIGNATURE_HASH = "test-signature";
+const MOCK_NETWORK = {
+ protocolFamily: "svm",
+ chainId: undefined,
+ 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: ThresholdSignatureScheme.TWO_OF_TWO,
+ };
+
+ 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(() => {
+ jest.clearAllMocks();
+
+ // Mock DynamicSvmWalletClient
+ (DynamicSvmWalletClient as jest.Mock).mockImplementation(() => 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 default network ID when not provided", async () => {
+ const { networkId, ...configWithoutNetworkId } = MOCK_CONFIG;
+
+ await DynamicSvmWalletProvider.configureWithWallet(configWithoutNetworkId);
+
+ expect(clusterApiUrl).toHaveBeenCalledWith("mainnet-beta");
+ });
+ });
+
+ describe("wallet methods", () => {
+ let provider: DynamicSvmWalletProvider;
+
+ beforeEach(async () => {
+ provider = await DynamicSvmWalletProvider.configureWithWallet(MOCK_CONFIG);
+ // Mock getNetwork to return our test network
+ jest.spyOn(provider, "getNetwork").mockReturnValue(MOCK_NETWORK);
+ });
+
+ 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",
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/typescript/agentkit/src/wallet-providers/dynamicSvmWalletProvider.ts b/typescript/agentkit/src/wallet-providers/dynamicSvmWalletProvider.ts
new file mode 100644
index 000000000..262a3c70e
--- /dev/null
+++ b/typescript/agentkit/src/wallet-providers/dynamicSvmWalletProvider.ts
@@ -0,0 +1,339 @@
+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, createDynamicWallet } from "./dynamicShared";
+import type { DynamicSvmWalletClient } from "@dynamic-labs-wallet/node-svm";
+import type {
+ SignatureStatus,
+ SignatureStatusConfig,
+ RpcResponseAndContext,
+ SignatureResult,
+} from "@solana/web3.js";
+import type { Network } from "../network";
+
+/**
+ * Configuration options for the Dynamic Svm wallet provider.
+ */
+export interface DynamicSvmWalletConfig extends DynamicWalletConfig {
+ /** The network ID to use for the wallet */
+ networkId?: string;
+ /** The 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: DynamicSvmWalletClient;
+ #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: DynamicSvmWalletClient;
+ 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",
+ * baseApiUrl: "https://app.dynamicauth.com",
+ * baseMPCRelayApiUrl: "relay.dynamicauth.com",
+ * chainType: "solana",
+ * networkId: "mainnet-beta",
+ * thresholdSignatureScheme: ThresholdSignatureScheme.TWO_OF_TWO
+ * });
+ * ```
+ */
+ public static async configureWithWallet(
+ config: DynamicSvmWalletConfig,
+ ): Promise {
+ console.log("[DynamicSvmWalletProvider] Starting wallet configuration with config:", {
+ networkId: config.networkId,
+ chainType: config.chainType,
+ baseApiUrl: config.baseApiUrl,
+ environmentId: config.environmentId
+ });
+
+ try {
+ const { wallet, dynamic } = await createDynamicWallet({
+ ...config,
+ chainType: "solana",
+ });
+
+ console.log("[DynamicSvmWalletProvider] Wallet created:", {
+ accountAddress: wallet.accountAddress
+ });
+
+ const connection =
+ config.connection ??
+ new Connection(clusterApiUrl(SOLANA_CLUSTER_ID_BY_NETWORK_ID[config.networkId ?? ""]));
+
+ 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 as DynamicSvmWalletClient,
+ 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 {
+ const signedTransaction = await this.#dynamicClient.signTransaction({
+ senderAddress: this.#accountAddress,
+ transaction,
+ });
+ 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 {
+ walletId: this.#accountAddress,
+ chainId: undefined,
+ 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);
+ }
+}
\ No newline at end of file
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..e450df5e7
--- /dev/null
+++ b/typescript/agentkit/src/wallet-providers/dynamicWalletProvider.test.ts
@@ -0,0 +1,153 @@
+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",
+ baseApiUrl: "https://app.dynamicauth.com",
+ baseMPCRelayApiUrl: "relay.dynamicauth.com",
+ chainId: "1",
+ chainType: "ethereum",
+ };
+
+ const MOCK_SVM_CONFIG: DynamicSvmWalletConfig = {
+ authToken: "test-auth-token",
+ environmentId: "test-environment-id",
+ baseApiUrl: "https://app.dynamicauth.com",
+ baseMPCRelayApiUrl: "relay.dynamicauth.com",
+ chainType: "solana",
+ 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,
+ chainType: "ethereum",
+ };
+
+ 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",
+ baseApiUrl: "https://app.dynamicauth.com",
+ baseMPCRelayApiUrl: "relay.dynamicauth.com",
+ chainType: "ethereum",
+ };
+
+ await expect(DynamicWalletProvider.configureWithWallet(testConfig as DynamicEvmWalletConfig)).rejects.toThrow(
+ "Missing required authToken",
+ );
+
+ mockEvmConfigureWithWallet.mockImplementation(originalImplementation);
+ });
+});
\ No newline at end of file
diff --git a/typescript/agentkit/src/wallet-providers/dynamicWalletProvider.ts b/typescript/agentkit/src/wallet-providers/dynamicWalletProvider.ts
new file mode 100644
index 000000000..3403d8cf6
--- /dev/null
+++ b/typescript/agentkit/src/wallet-providers/dynamicWalletProvider.ts
@@ -0,0 +1,56 @@
+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",
+ * baseApiUrl: "https://app.dynamicauth.com",
+ * baseMPCRelayApiUrl: "relay.dynamicauth.com",
+ * chainType: "ethereum",
+ * chainId: "84532"
+ * });
+ *
+ * // Create an SVM wallet provider
+ * const svmProvider = await DynamicWalletProvider.configureWithWallet({
+ * authToken: "your-auth-token",
+ * environmentId: "your-environment-id",
+ * baseApiUrl: "https://app.dynamicauth.com",
+ * baseMPCRelayApiUrl: "relay.dynamicauth.com",
+ * 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/typescript/agentkit/src/wallet-providers/index.ts b/typescript/agentkit/src/wallet-providers/index.ts
index 0c727b42b..042e5e332 100644
--- a/typescript/agentkit/src/wallet-providers/index.ts
+++ b/typescript/agentkit/src/wallet-providers/index.ts
@@ -14,3 +14,6 @@ export * from "./privyEvmWalletProvider";
export * from "./privySvmWalletProvider";
export * from "./privyEvmDelegatedEmbeddedWalletProvider";
export * from "./zeroDevWalletProvider";
+export * from "./dynamicWalletProvider";
+export * from "./dynamicEvmWalletProvider";
+export * from "./dynamicSvmWalletProvider";
diff --git a/typescript/examples/langchain-dynamic-chatbot/.env-local b/typescript/examples/langchain-dynamic-chatbot/.env-local
new file mode 100644
index 000000000..4b1498e25
--- /dev/null
+++ b/typescript/examples/langchain-dynamic-chatbot/.env-local
@@ -0,0 +1,16 @@
+OPENAI_API_KEY=
+
+# Dynamic Configuration - get these from your Dynamic dashboard
+DYNAMIC_AUTH_TOKEN=
+DYNAMIC_ENVIRONMENT_ID=
+DYNAMIC_BASE_API_URL=https://app.dynamicauth.com
+DYNAMIC_BASE_MPC_RELAY_API_URL=https://relay.dynamicauth.com
+
+# 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..953f8fd43
--- /dev/null
+++ b/typescript/examples/langchain-dynamic-chatbot/README.md
@@ -0,0 +1,70 @@
+# Dynamic AgentKit LangChain Chatbot Example
+
+This example demonstrates how to use AgentKit with Dynamic wallet provider and LangChain to create a chatbot that can interact with the Solana blockchain.
+
+## Prerequisites
+
+- Node.js 18+
+- [Dynamic Account](https://www.dynamic.xyz/) and API credentials
+- [OpenAI API Key](https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key)
+
+## Setup
+
+1. Clone the repository and install dependencies:
+```bash
+npm install
+```
+
+2. Copy the example environment file and fill in your credentials:
+```bash
+cp .env-local .env
+```
+
+3. Update the `.env` file with your credentials:
+```
+OPENAI_API_KEY=your_openai_api_key
+
+# Dynamic Configuration - get these from your Dynamic dashboard
+DYNAMIC_AUTH_TOKEN=your_dynamic_auth_token
+DYNAMIC_ENVIRONMENT_ID=your_dynamic_environment_id
+DYNAMIC_BASE_API_URL=https://app.dynamicauth.com
+DYNAMIC_BASE_MPC_RELAY_API_URL=relay.dynamicauth.com
+
+# Optional Network ID. If you'd like to use a Dynamic Solana wallet, set to "solana-devnet". Otherwise, defaults to "mainnet-beta"
+NETWORK_ID=solana-devnet
+
+# 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=your_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=your_cdp_api_key_private_key
+```
+
+## Running the Example
+
+Start the chatbot:
+```bash
+npm start
+```
+
+The chatbot will start in either chat mode or autonomous mode:
+
+- **Chat Mode**: Interact with the agent through a command-line interface
+- **Autonomous Mode**: The agent will automatically perform actions on the blockchain at regular intervals
+
+## Features
+
+- Uses Dynamic's wallet API for 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)
diff --git a/typescript/examples/langchain-dynamic-chatbot/chatbot.ts b/typescript/examples/langchain-dynamic-chatbot/chatbot.ts
new file mode 100644
index 000000000..4f59ad492
--- /dev/null
+++ b/typescript/examples/langchain-dynamic-chatbot/chatbot.ts
@@ -0,0 +1,261 @@
+import {
+ AgentKit,
+ DynamicSvmWalletProvider,
+ wethActionProvider,
+ walletActionProvider,
+ erc20ActionProvider,
+ pythActionProvider,
+ cdpApiActionProvider,
+ splActionProvider,
+} 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 readline from "node:readline";
+import * as fs from "node:fs";
+import { ThresholdSignatureScheme } from "@dynamic-labs-wallet/node";
+
+dotenv.config();
+
+const WALLET_DATA_FILE = "wallet_data.txt";
+
+/**
+ * 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"];
+ for (const varName of requiredVars) {
+ 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");
+ for (const varName of missingVars) {
+ console.error(`${varName}=your_${varName.toLowerCase()}_here`);
+ }
+ process.exit(1);
+ }
+}
+
+// Add this right after imports and before any other code
+validateEnvironment();
+
+/**
+ * Initialize the agent with Dynamic Agentkit
+ *
+ * @returns Agent executor and config
+ */
+async function initializeAgent() {
+ try {
+ // Initialize LLM
+ const llm = new ChatOpenAI({
+ modelName: "gpt-4o-mini",
+ });
+
+ const networkId = process.env.NETWORK_ID || "mainnet-beta";
+
+ // Configure Dynamic wallet provider
+ const baseApiUrl = process.env.DYNAMIC_BASE_API_URL || "https://app.dynamicauth.com";
+ const baseMPCRelayApiUrl = process.env.DYNAMIC_BASE_MPC_RELAY_API_URL || "relay.dynamicauth.com";
+
+ const walletConfig = {
+ authToken: process.env.DYNAMIC_AUTH_TOKEN as string,
+ environmentId: process.env.DYNAMIC_ENVIRONMENT_ID as string,
+ baseApiUrl,
+ baseMPCRelayApiUrl,
+ networkId,
+ chainType: "solana" as const,
+ thresholdSignatureScheme: ThresholdSignatureScheme.TWO_OF_TWO,
+ };
+
+ const walletProvider = await DynamicSvmWalletProvider.configureWithWallet(walletConfig);
+
+ // Initialize AgentKit
+ const agentkit = await AgentKit.from({
+ walletProvider,
+ actionProviders: [
+ wethActionProvider(),
+ pythActionProvider(),
+ walletActionProvider(),
+ erc20ActionProvider(),
+ cdpApiActionProvider({
+ apiKeyName: process.env.CDP_API_KEY_NAME as string,
+ apiKeyPrivateKey: process.env.CDP_API_KEY_PRIVATE_KEY as string,
+ }),
+ splActionProvider(),
+ ],
+ });
+
+ 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 with a Dynamic wallet that can interact onchain using the Coinbase Developer
+ Platform AgentKit. 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 'solana-devnet'. 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
+ const exportedWallet = walletProvider.exportWallet();
+ fs.writeFileSync(WALLET_DATA_FILE, JSON.stringify(exportedWallet));
+
+ 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
+ */
+async function runAutonomousMode(
+ agent: ReturnType,
+ config: { configurable: { thread_id: string } },
+ 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);
+ }
+ }
+
+ console.log(`Waiting ${interval} seconds before next action...`);
+ await new Promise(resolve => setTimeout(resolve, interval * 1000));
+ } catch (error) {
+ console.error("Error in autonomous mode:", error);
+ await new Promise(resolve => setTimeout(resolve, interval * 1000));
+ }
+ }
+}
+
+/**
+ * Run the agent in chat mode
+ *
+ * @param agent - The agent executor
+ * @param config - Agent configuration
+ */
+async function runChatMode(
+ agent: ReturnType,
+ config: { configurable: { thread_id: string } }
+) {
+ const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ });
+
+ const question = (prompt: string): Promise =>
+ new Promise(resolve => rl.question(prompt, resolve));
+
+ console.log("Starting chat mode...");
+ console.log("Type 'exit' to quit");
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ try {
+ const userInput = await question("\nYou: ");
+
+ if (userInput.toLowerCase() === "exit") {
+ rl.close();
+ 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);
+ }
+ }
+ } catch (error) {
+ console.error("Error in chat mode:", error);
+ }
+ }
+}
+
+/**
+ * Choose between chat and autonomous modes
+ *
+ * @returns The chosen 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));
+
+ while (true) {
+ const mode = await question("Choose mode (chat/auto): ");
+ if (mode.toLowerCase() === "chat" || mode.toLowerCase() === "auto") {
+ rl.close();
+ return mode.toLowerCase() as "chat" | "auto";
+ }
+ console.log("Invalid mode. Please choose 'chat' or 'auto'");
+ }
+}
+
+/**
+ * Main function
+ */
+async function main() {
+ try {
+ const { agent, config } = await initializeAgent();
+ const mode = await chooseMode();
+
+ if (mode === "auto") {
+ await runAutonomousMode(agent, config);
+ } else {
+ await runChatMode(agent, config);
+ }
+ } catch (error) {
+ console.error("Failed to start agent:", error);
+ process.exit(1);
+ }
+}
+
+main();
diff --git a/typescript/examples/langchain-dynamic-chatbot/package.json b/typescript/examples/langchain-dynamic-chatbot/package.json
new file mode 100644
index 000000000..4983ca73e
--- /dev/null
+++ b/typescript/examples/langchain-dynamic-chatbot/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "langchain-dynamic-chatbot",
+ "version": "1.0.0",
+ "description": "A chatbot example using AgentKit with Dynamic wallet provider",
+ "main": "chatbot.ts",
+ "scripts": {
+ "start": "ts-node chatbot.ts",
+ "build": "tsc",
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "dependencies": {
+ "@coinbase/agentkit": "workspace:*",
+ "@coinbase/agentkit-langchain": "^0.3.0",
+ "@dynamic-labs-wallet/core": "^0.0.71",
+ "@dynamic-labs-wallet/node": "^0.0.71",
+ "@dynamic-labs-wallet/node-svm": "^0.0.71",
+ "@langchain/core": "^0.3.40",
+ "@langchain/langgraph": "^0.2.72",
+ "@langchain/openai": "*",
+ "dotenv": "^16.4.5"
+ },
+ "devDependencies": {
+ "@types/node": "^20.11.24",
+ "ts-node": "^10.9.2",
+ "typescript": "^5.3.3"
+ }
+}
diff --git a/typescript/examples/langchain-dynamic-chatbot/tsconfig.json b/typescript/examples/langchain-dynamic-chatbot/tsconfig.json
new file mode 100644
index 000000000..a37da3664
--- /dev/null
+++ b/typescript/examples/langchain-dynamic-chatbot/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "preserveSymlinks": true,
+ "outDir": "./dist",
+ "rootDir": ".",
+ "module": "Node16"
+ },
+ "include": ["*.ts"]
+}