/**
* Solana Wallet Implementation
*
* Handles all Solana interactions - balance checks, transfers, etc.
* Uses devnet by default for safe testing.
*/
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
Transaction,
sendAndConfirmTransaction,
ParsedTransactionWithMeta,
} from "@solana/web3.js";
import {
getAssociatedTokenAddress,
createAssociatedTokenAccountInstruction,
createTransferInstruction,
getAccount,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
// Devnet USDC mint (Circle's devnet USDC)
const USDC_MINT = new PublicKey("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU");
const USDC_DECIMALS = 6;
// Devnet RPC
const DEVNET_RPC = "https://api.devnet.solana.com";
export interface BalanceInfo {
sol: number;
usdc: number;
network: string;
address: string;
}
export interface TransactionResult {
signature: string;
recipient: string;
amount: number;
memo?: string;
}
export interface TransactionInfo {
signature: string;
timestamp: number | null | undefined;
status: "success" | "failed";
type: string;
}
export class AgentWallet {
private connection: Connection;
private keypair: Keypair;
private walletPath: string;
constructor() {
this.connection = new Connection(DEVNET_RPC, "confirmed");
this.walletPath = path.join(os.homedir(), ".agent-wallet", "keypair.json");
this.keypair = this.loadOrCreateKeypair();
}
/**
* Load existing keypair or create a new one.
*/
private loadOrCreateKeypair(): Keypair {
const dir = path.dirname(this.walletPath);
// Create directory if it doesn't exist
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// Load existing keypair or create new one
if (fs.existsSync(this.walletPath)) {
try {
const data = JSON.parse(fs.readFileSync(this.walletPath, "utf-8"));
return Keypair.fromSecretKey(Uint8Array.from(data));
} catch {
// If loading fails, create a new one
}
}
// Create new keypair
const keypair = Keypair.generate();
fs.writeFileSync(
this.walletPath,
JSON.stringify(Array.from(keypair.secretKey))
);
console.error(`Created new wallet at ${this.walletPath}`);
return keypair;
}
/**
* Get wallet address.
*/
getAddress(): string {
return this.keypair.publicKey.toBase58();
}
/**
* Get SOL and USDC balances.
*/
async getBalances(): Promise<BalanceInfo> {
const address = this.keypair.publicKey;
// Get SOL balance
const solBalance = await this.connection.getBalance(address);
const sol = solBalance / LAMPORTS_PER_SOL;
// Get USDC balance
let usdc = 0;
try {
const usdcAta = await getAssociatedTokenAddress(USDC_MINT, address);
const tokenAccount = await getAccount(this.connection, usdcAta);
usdc = Number(tokenAccount.amount) / Math.pow(10, USDC_DECIMALS);
} catch {
// Token account doesn't exist yet
usdc = 0;
}
return {
sol: parseFloat(sol.toFixed(4)),
usdc: parseFloat(usdc.toFixed(2)),
network: "devnet",
address: address.toBase58(),
};
}
/**
* Send USDC to a recipient.
*/
async sendUsdc(
recipientAddress: string,
amount: number,
memo?: string
): Promise<TransactionResult> {
const recipient = new PublicKey(recipientAddress);
const sender = this.keypair.publicKey;
// Get ATAs
const senderAta = await getAssociatedTokenAddress(USDC_MINT, sender);
const recipientAta = await getAssociatedTokenAddress(USDC_MINT, recipient);
// Check sender has enough USDC
try {
const senderAccount = await getAccount(this.connection, senderAta);
const balance = Number(senderAccount.amount) / Math.pow(10, USDC_DECIMALS);
if (balance < amount) {
throw new Error(`Insufficient USDC balance. Have: ${balance}, Need: ${amount}`);
}
} catch (error: any) {
if (error.message?.includes("Insufficient")) {
throw error;
}
throw new Error("No USDC token account found. Request devnet USDC first.");
}
// Build transaction
const transaction = new Transaction();
// Check if recipient ATA exists, if not create it
try {
await getAccount(this.connection, recipientAta);
} catch {
// Create recipient ATA
transaction.add(
createAssociatedTokenAccountInstruction(
sender, // payer
recipientAta, // ata address
recipient, // owner
USDC_MINT // mint
)
);
}
// Add transfer instruction
const amountInBaseUnits = Math.floor(amount * Math.pow(10, USDC_DECIMALS));
transaction.add(
createTransferInstruction(
senderAta,
recipientAta,
sender,
amountInBaseUnits
)
);
// Send transaction
const signature = await sendAndConfirmTransaction(
this.connection,
transaction,
[this.keypair]
);
return {
signature,
recipient: recipientAddress,
amount,
memo,
};
}
/**
* Get recent transactions.
*/
async getRecentTransactions(limit: number = 10): Promise<TransactionInfo[]> {
const signatures = await this.connection.getSignaturesForAddress(
this.keypair.publicKey,
{ limit }
);
return signatures.map((sig) => ({
signature: sig.signature,
timestamp: sig.blockTime,
status: sig.err ? "failed" : "success",
type: "transaction",
}));
}
/**
* Request devnet SOL airdrop.
*/
async requestAirdrop(amount: number = 1): Promise<{ success: boolean; amount: number; signature?: string }> {
try {
const signature = await this.connection.requestAirdrop(
this.keypair.publicKey,
amount * LAMPORTS_PER_SOL
);
await this.connection.confirmTransaction(signature);
return {
success: true,
amount,
signature,
};
} catch (error: any) {
// Rate limited or other error
throw new Error(`Airdrop failed: ${error.message}. Try again later or use https://faucet.solana.com`);
}
}
}