Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
638 changes: 634 additions & 4 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions typescript/agentkit/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

### Added

- Added `svmWalletProvider` abstract class for interacting with Solana.
- Added `solanaKeypairWalletProvider` to concretely implement `svmWalletProvider` with a local keypair.

## [0.1.2] - 2025-02-07

### Added
Expand Down
1 change: 1 addition & 0 deletions typescript/agentkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
],
"dependencies": {
"@coinbase/coinbase-sdk": "^0.17.0",
"@solana/web3.js": "^1.98.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer not to right now, since the interface is changing, and not all ecosystem packages have upgraded to use it.

But if you really want to get in the guts, it's absolutely more flexible than v1. Bit of a learning curve though

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it – that's fine. Only thing I would suggest is to preemptively rename solanaKeyPairWalletProvider and related to solanaV1KeyPairWalletProvider. BTW, why not call svmWalletProvider -> solanaWalletProvider?

@ngundotra ngundotra Feb 11, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically v1 and v2 of @solana/web3.js are compatible. @solana/web3.js@next is just a more opinionated & web-optimized version of v1. Therefore, I don't think it makes sense to name the implementation based on the version of the framework it uses.

That being said, I will version if you think it's better for longevity.

why not call svmWalletProvider -> solanaWalletProvider

I was following the evm naming convention. Here, svm refers to Solana Virtual Machine-like networks that have the same signing curve & similar virtual machine behavior (not formally defined). For example - Eclipse, Sonic, Soon, etc.

I reserved SolanaWalletProvider for an implementation that has some hard-coded checks against the canonical Solana clusters (mainnet-beta, testnet, and devnet)

"md5": "^2.3.0",
"reflect-metadata": "^0.2.2",
"twitter-api-v2": "^1.18.2",
Expand Down
42 changes: 42 additions & 0 deletions typescript/agentkit/src/network/svm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Network } from "./types";

// CDP Network IDs
export const SOLANA_MAINNET_NETWORK_ID = "solana-mainnet";
export const SOLANA_TESTNET_NETWORK_ID = "solana-testnet";
export const SOLANA_DEVNET_NETWORK_ID = "solana-devnet";

// AgentKit Protocol Family
export const SOLANA_PROTOCOL_FAMILY = "svm";

// Chain IDs - Genesis Block Hashes
export const SOLANA_MAINNET_GENESIS_BLOCK_HASH = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d";
export const SOLANA_TESTNET_GENESIS_BLOCK_HASH = "4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY";
export const SOLANA_DEVNET_GENESIS_BLOCK_HASH = "EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG";
export type SOLANA_CLUSTER =
| typeof SOLANA_MAINNET_GENESIS_BLOCK_HASH
| typeof SOLANA_TESTNET_GENESIS_BLOCK_HASH
| typeof SOLANA_DEVNET_GENESIS_BLOCK_HASH;

export const SOLANA_MAINNET_NETWORK: Network = {
protocolFamily: SOLANA_PROTOCOL_FAMILY,
chainId: SOLANA_MAINNET_GENESIS_BLOCK_HASH,
networkId: SOLANA_MAINNET_NETWORK_ID,
};

export const SOLANA_TESTNET_NETWORK: Network = {
protocolFamily: SOLANA_PROTOCOL_FAMILY,
chainId: SOLANA_TESTNET_GENESIS_BLOCK_HASH,
networkId: SOLANA_TESTNET_NETWORK_ID,
};

export const SOLANA_DEVNET_NETWORK: Network = {
protocolFamily: SOLANA_PROTOCOL_FAMILY,
chainId: SOLANA_DEVNET_GENESIS_BLOCK_HASH,
networkId: SOLANA_DEVNET_NETWORK_ID,
};

export const SOLANA_NETWORKS: Record<SOLANA_CLUSTER, Network> = {
[SOLANA_MAINNET_GENESIS_BLOCK_HASH]: SOLANA_MAINNET_NETWORK,
[SOLANA_TESTNET_GENESIS_BLOCK_HASH]: SOLANA_TESTNET_NETWORK,
[SOLANA_DEVNET_GENESIS_BLOCK_HASH]: SOLANA_DEVNET_NETWORK,
};
2 changes: 2 additions & 0 deletions typescript/agentkit/src/wallet-providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export * from "./walletProvider";
export * from "./evmWalletProvider";
export * from "./viemWalletProvider";
export * from "./cdpWalletProvider";
export * from "./svmWalletProvider";
export * from "./solanaKeypairWalletProvider";
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { clusterApiUrl, Keypair } from "@solana/web3.js";
import { SolanaKeypairWalletProvider } from "./solanaKeypairWalletProvider";
import {
SOLANA_DEVNET_GENESIS_BLOCK_HASH,
SOLANA_MAINNET_GENESIS_BLOCK_HASH,
SOLANA_NETWORKS,
SOLANA_TESTNET_GENESIS_BLOCK_HASH,
} from "../network/svm";

describe("Solana Keypair Wallet", () => {
it("should initialize correctly via convenience getters", async () => {
const keypair = Keypair.generate();

let wallet = await SolanaKeypairWalletProvider.fromRpcUrl(
clusterApiUrl("devnet"),
keypair.secretKey,
);
expect(wallet.getNetwork()).toEqual(SOLANA_NETWORKS[SOLANA_DEVNET_GENESIS_BLOCK_HASH]);

wallet = await SolanaKeypairWalletProvider.fromRpcUrl(
clusterApiUrl("testnet"),
keypair.secretKey,
);
expect(wallet.getNetwork()).toEqual(SOLANA_NETWORKS[SOLANA_TESTNET_GENESIS_BLOCK_HASH]);

wallet = await SolanaKeypairWalletProvider.fromRpcUrl(
clusterApiUrl("mainnet-beta"),
keypair.secretKey,
);
expect(wallet.getNetwork()).toEqual(SOLANA_NETWORKS[SOLANA_MAINNET_GENESIS_BLOCK_HASH]);
expect(wallet.getAddress()).toEqual(keypair.publicKey.toBase58());
});

it("should error when the network genesis hash is unknown", async () => {
expect(
() =>
new SolanaKeypairWalletProvider({
keypair: Keypair.generate().secretKey,
rpcUrl: clusterApiUrl("mainnet-beta"),
genesisHash: "0x123",
}),
).toThrowError("Unknown network with genesis hash");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import { SvmWalletProvider } from "./svmWalletProvider";
import { Network } from "../network";
import {
Connection,
Keypair,
PublicKey,
VersionedTransaction,
LAMPORTS_PER_SOL,
SystemProgram,
MessageV0,
ComputeBudgetProgram,
clusterApiUrl,
} from "@solana/web3.js";
import bs58 from "bs58";
import {
SOLANA_CLUSTER,
SOLANA_DEVNET_GENESIS_BLOCK_HASH,
SOLANA_MAINNET_GENESIS_BLOCK_HASH,
SOLANA_NETWORKS,
SOLANA_TESTNET_GENESIS_BLOCK_HASH,
} from "../network/svm";

/**
* SolanaKeypairWalletProvider is a wallet provider that uses a local Solana keypair.
*
* @augments SvmWalletProvider
*/
export class SolanaKeypairWalletProvider extends SvmWalletProvider {
#keypair: Keypair;
#connection: Connection;
#genesisHash: string;

/**
* Creates a new SolanaKeypairWalletProvider
*
* @param args - Configuration arguments
* @param args.keypair - Either a Uint8Array or a base58 encoded string representing a 32-byte secret key
* @param args.rpcUrl - URL of the Solana RPC endpoint
* @param args.genesisHash - The genesis hash of the network
*/
constructor({
keypair,
rpcUrl,
genesisHash,
}: {
keypair: Uint8Array | string;
rpcUrl: string;
genesisHash: string;
}) {
super();

this.#keypair =
typeof keypair === "string"
? Keypair.fromSecretKey(bs58.decode(keypair))
: Keypair.fromSecretKey(keypair);
this.#connection = new Connection(rpcUrl);
if (genesisHash in SOLANA_NETWORKS) {
this.#genesisHash = genesisHash;
} else {
throw new Error(`Unknown network with genesis hash: ${genesisHash}`);
}
}

/**
* Get the default RPC URL for a Solana cluster
*
* @param cluster - The cluster to get the RPC URL for
* @returns The RPC URL for the cluster
*/
static urlForCluster(cluster: SOLANA_CLUSTER): string {
if (cluster in SOLANA_NETWORKS) {
switch (cluster) {
case SOLANA_MAINNET_GENESIS_BLOCK_HASH:
return clusterApiUrl("mainnet-beta");
case SOLANA_TESTNET_GENESIS_BLOCK_HASH:
return clusterApiUrl("testnet");
case SOLANA_DEVNET_GENESIS_BLOCK_HASH:
return clusterApiUrl("devnet");
default:
throw new Error(`Unknown cluster: ${cluster}`);
}
} else {
throw new Error(`Unknown cluster: ${cluster}`);
}
}

/**
* Create a new SolanaKeypairWalletProvider from an RPC URL and a keypair
*
* @param rpcUrl - The URL of the Solana RPC endpoint
* @param keypair - Either a Uint8Array or a base58 encoded string representing a 32-byte secret key
* @returns The new SolanaKeypairWalletProvider
*/
static async fromRpcUrl<T extends SolanaKeypairWalletProvider>(
rpcUrl: string,
keypair: Uint8Array | string,
): Promise<T> {
const connection = new Connection(rpcUrl);
return await this.fromConnection(connection, keypair);
}

/**
* Create a new SolanaKeypairWalletProvider from a Connection and a keypair
*
* @param connection - The Connection to use
* @param keypair - Either a Uint8Array or a base58 encoded string representing a 32-byte secret key
* @returns The new SolanaKeypairWalletProvider
*/
static async fromConnection<T extends SolanaKeypairWalletProvider>(
connection: Connection,
keypair: Uint8Array | string,
): Promise<T> {
const genesisHash = await connection.getGenesisHash();
return new SolanaKeypairWalletProvider({
keypair,
rpcUrl: connection.rpcEndpoint,
genesisHash: genesisHash,
}) as T;
}

/**
* Get the address of the wallet
*
* @returns The base58 encoded address of the wallet
*/
getAddress(): string {
return this.#keypair.publicKey.toBase58();
}

/**
* Get the network
*
* @returns The network
*/
getNetwork(): Network {
return SOLANA_NETWORKS[this.#genesisHash];
}

/**
* Sign a transaction
*
* @param transaction - The transaction to sign
* @returns The signed transaction
*/
async signTransaction(transaction: VersionedTransaction): Promise<VersionedTransaction> {
transaction.sign([this.#keypair]);
return transaction;
}

/**
* Send a transaction
*
* @param transaction - The transaction to send
* @returns The transaction hash
*/
sendTransaction(transaction: VersionedTransaction): Promise<string> {
return this.#connection.sendTransaction(transaction);
}

/**
* Wait for a transaction receipt
*
* @param txHash - The transaction hash
* @returns The transaction receipt
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
waitForTransactionReceipt(txHash: string): Promise<any> {
return this.#connection.confirmTransaction(txHash);
}

/**
* Get the name of the wallet provider
*
* @returns The name of the wallet provider
*/
getName(): string {
return "solana_keypair_wallet_provider";
}

/**
* Get the balance of the wallet
*
* @returns The balance of the wallet
*/
getBalance(): Promise<bigint> {
return this.#connection.getBalance(this.#keypair.publicKey).then(balance => BigInt(balance));
}

/**
* Transfer SOL from the wallet to another address
*
* @param to - The base58 encoded address to transfer the SOL to
* @param value - The amount of SOL to transfer
* @returns The transaction hash
*/
async nativeTransfer(to: string, value: string): Promise<string> {
const toPubkey = new PublicKey(to);
const lamports = BigInt(LAMPORTS_PER_SOL) * BigInt(value);

const instructions = [
ComputeBudgetProgram.setComputeUnitPrice({
// TODO: Make this configurable
microLamports: 10000,
}),
ComputeBudgetProgram.setComputeUnitLimit({
units: 2000,
}),
SystemProgram.transfer({
fromPubkey: this.#keypair.publicKey,
toPubkey: toPubkey,
lamports: lamports,
}),
];
const tx = new VersionedTransaction(
MessageV0.compile({
payerKey: this.#keypair.publicKey,
instructions: instructions,
recentBlockhash: (await this.#connection.getLatestBlockhash()).blockhash,
}),
);

const txHash = await this.#connection.sendTransaction(tx);
return txHash;
}
}
35 changes: 35 additions & 0 deletions typescript/agentkit/src/wallet-providers/svmWalletProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { WalletProvider } from "./walletProvider";
import { VersionedTransaction } from "@solana/web3.js";

/**
* SvmWalletProvider is the abstract base class for all Solana wallet providers (non browsers).
*
* @abstract
*/
export abstract class SvmWalletProvider extends WalletProvider {
/**
* Sign a transaction.
*
* @param transaction - The transaction to sign.
* @returns The signed transaction.
*/
abstract signTransaction(transaction: VersionedTransaction): Promise<VersionedTransaction>;

/**
* Send a transaction.
*
* @param transaction - The transaction to send.
* @returns The transaction hash.
*/
abstract sendTransaction(transaction: VersionedTransaction): Promise<string>;

/**
* Wait for a transaction receipt.
*
* @param txHash - The transaction hash.
* @returns The transaction receipt.
*/
abstract waitForTransactionReceipt(txHash: string): Promise<any>;
}
2 changes: 1 addition & 1 deletion typescript/agentkit/src/wallet-providers/walletProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,5 @@ export abstract class WalletProvider {
* @param value - The amount to transfer in whole units (e.g. ETH)
* @returns The transaction hash.
*/
abstract nativeTransfer(to: `0x${string}`, value: string): Promise<`0x${string}`>;
abstract nativeTransfer(to: string, value: string): Promise<string>;
}