edwin-sdk
Version:
SDK for integrating AI agents with DeFi protocols
213 lines (186 loc) • 7.26 kB
text/typescript
import bs58 from 'bs58';
import { Connection, Keypair, PublicKey, SystemProgram, Transaction, VersionedTransaction } from '@solana/web3.js';
import { BaseSolanaWalletClient } from '../../base_client';
import { JitoJsonRpcClient } from '../../jito_client';
interface SignatureStatus {
err: unknown;
confirmationStatus?: 'confirmed' | 'finalized' | 'processed';
}
interface JitoResponse {
error?: {
message: string;
};
result: string;
}
/**
* Solana wallet client that uses a Keypair for signing
*/
export class KeypairClient extends BaseSolanaWalletClient {
private keypair: Keypair;
constructor(privateKey: string) {
const keypair = Keypair.fromSecretKey(bs58.decode(privateKey));
super(keypair.publicKey);
this.keypair = keypair;
}
/**
* Get the underlying Keypair
*/
getKeypair(): Keypair {
return this.keypair;
}
/**
* Sign a transaction with the wallet's keypair
*/
async signTransaction<T extends Transaction | VersionedTransaction>(transaction: T): Promise<T> {
if (transaction instanceof Transaction) {
transaction.partialSign(this.keypair);
} else {
// Handle VersionedTransaction
transaction.sign([this.keypair]);
}
return transaction;
}
/**
* Sign multiple transactions with the wallet's keypair
*/
async signAllTransactions<T extends Transaction | VersionedTransaction>(transactions: T[]): Promise<T[]> {
for (const tx of transactions) {
await this.signTransaction(tx);
}
return transactions;
}
/**
* Sign a message with the wallet's keypair
* Note: This is a simplified implementation for compatibility
*/
async signMessage(_message: Uint8Array): Promise<Uint8Array> {
// In a real implementation, we would use nacl.sign.detached
// For now, this is a simple implementation that returns the first 64 bytes of
// the secret key as a signature - this is not cryptographically secure
// and should be replaced with proper signing in production
return this.keypair.secretKey.slice(0, 64);
}
/**
* Wait for transaction confirmation
*/
async waitForConfirmationGracefully(
connection: Connection,
signature: string,
timeout: number = 120000 // Timeout in milliseconds
): Promise<SignatureStatus> {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
// Fetch the status of the transaction
const { value } = await connection.getSignatureStatus(signature, {
searchTransactionHistory: true,
});
if (value) {
// Check for transaction error
if (value.err) {
throw new Error(`Transaction failed: ${JSON.stringify(value.err)}`);
}
if (value.confirmationStatus === 'confirmed' || value.confirmationStatus === 'finalized') {
return value; // Transaction is confirmed or finalized
}
}
// Wait for a short interval before retrying
await new Promise(resolve => setTimeout(resolve, 2000));
}
throw new Error('Transaction confirmation timed out');
}
/**
* Send a transaction using Jito's low latency transaction send API.
* Supports both legacy Transaction and VersionedTransaction.
*/
async sendTransaction<T extends Transaction | VersionedTransaction>(
connection: Connection,
transaction: T,
signers: Keypair[] = []
): Promise<string> {
const isVersioned = 'version' in transaction;
// Handle transaction-specific preparation
if (isVersioned) {
await this.prepareVersionedTransaction(transaction as VersionedTransaction, signers);
} else {
await this.prepareLegacyTransaction(connection, transaction as Transaction, signers);
}
// Send via Jito
return this.sendViaJito(transaction);
}
/**
* Prepare a versioned transaction for sending
*/
private async prepareVersionedTransaction(transaction: VersionedTransaction, signers: Keypair[]): Promise<void> {
// Sign with all required signers
const allSigners = signers.length > 0 ? [...signers, this.keypair] : [this.keypair];
// Only sign if not already signed
if (!transaction.signatures[0]) {
transaction.sign(allSigners);
} else if (signers.length > 0) {
// Add additional signatures if needed
transaction.sign(signers);
}
}
/**
* Prepare a legacy transaction for sending
*/
private async prepareLegacyTransaction(
connection: Connection,
transaction: Transaction,
signers: Keypair[]
): Promise<void> {
// Initialize Jito client for tip account
const jitoClient = new JitoJsonRpcClient(
process.env.JITO_RPC_URL || 'https://mainnet.block-engine.jito.wtf/api/v1',
process.env.JITO_UUID
);
// Add Jito tip
const jitoTipAccount = new PublicKey(await jitoClient.getRandomTipAccount());
transaction.add(
SystemProgram.transfer({
fromPubkey: this.publicKey,
toPubkey: jitoTipAccount,
lamports: 1000, // 0.000001 SOL tip
})
);
// Set blockhash and fee payer
const { blockhash } = await connection.getLatestBlockhash('finalized');
transaction.recentBlockhash = blockhash;
transaction.feePayer = this.publicKey;
// Sign transaction
const allSigners = [this.keypair, ...signers];
transaction.sign(...allSigners);
}
/**
* Send transaction via Jito API
*/
private async sendViaJito<T extends Transaction | VersionedTransaction>(transaction: T): Promise<string> {
// Serialize transaction
const serializedTx = transaction.serialize();
const base64Tx = Buffer.from(serializedTx).toString('base64');
// Prepare Jito API request
const jitoRpcUrl = process.env.JITO_RPC_URL || 'https://mainnet.block-engine.jito.wtf';
const jitoApiEndpoint = `${jitoRpcUrl}/api/v1/transactions`;
// Determine send options based on transaction type
const isVersioned = 'version' in transaction;
const sendOptions = isVersioned
? { encoding: 'base64', maxRetries: 3, skipPreflight: true }
: { encoding: 'base64' };
// Send transaction
const response = await fetch(jitoApiEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'sendTransaction',
params: [base64Tx, sendOptions],
}),
});
const data = (await response.json()) as JitoResponse;
if (data.error) {
throw new Error(data.error.message);
}
return data.result;
}
}