-
Notifications
You must be signed in to change notification settings - Fork 756
feat: add svm keypair wallet provider #246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
249fcbb
add svm keypair wallet provider
ngundotra 18a7afd
add network constants
ngundotra 6d7915d
update abstract method for getNetwork
ngundotra 65aac3c
remove async interface for getNetwork, require genesisHash instead
ngundotra d500a0f
address nits
ngundotra 0518fe4
add tests, convenience inits
ngundotra f8fb32e
update walletProvider interface, remove typing
ngundotra b3e30ba
Update typescript/agentkit/src/wallet-providers/solanaKeypairWalletPr…
ngundotra a2d52a1
add async signing interface
ngundotra ff7fa55
add test for solana wallet
ngundotra ec68690
update package-lock.json
ngundotra 57db34f
use npm 8.9.0
ngundotra 13bcdd8
reset lock
ngundotra df4ce01
address test nit
ngundotra 666e190
fix lockfile
ngundotra f66f248
fix lint
ngundotra 5417f9b
add docs
ngundotra 2ce2e13
fix comments
ngundotra 494588e
update changelog
ngundotra File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
44 changes: 44 additions & 0 deletions
44
typescript/agentkit/src/wallet-providers/solanaKeypairWalletProvider.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"); | ||
| }); | ||
| }); |
225 changes: 225 additions & 0 deletions
225
typescript/agentkit/src/wallet-providers/solanaKeypairWalletProvider.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
35
typescript/agentkit/src/wallet-providers/svmWalletProvider.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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>; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we use v2? https://github.com/anza-xyz/solana-web3.js?tab=readme-ov-file#whats-new-in-version-20
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
solanaKeyPairWalletProviderand related tosolanaV1KeyPairWalletProvider. BTW, why not callsvmWalletProvider->solanaWalletProvider?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically
v1andv2of@solana/web3.jsare compatible.@solana/web3.js@nextis just a more opinionated & web-optimized version ofv1. 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.
I was following the
evmnaming convention. Here,svmrefers 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
SolanaWalletProviderfor an implementation that has some hard-coded checks against the canonical Solana clusters (mainnet-beta,testnet, anddevnet)