@nosana/kit
Version:
Nosana KIT
113 lines (112 loc) • 5.06 kB
JavaScript
import { createSolanaClient, address, getProgramDerivedAddress, getAddressEncoder, createTransaction, signTransactionMessageWithSigners, getExplorerLink, getSignatureFromTransaction } from 'gill';
import { NosanaError, ErrorCodes } from '../errors/NosanaError.js';
export class SolanaUtils {
constructor(sdk) {
this.sdk = sdk;
const rpcEndpoint = this.sdk.config.solana.rpcEndpoint;
if (!rpcEndpoint)
throw new NosanaError('RPC URL is required', ErrorCodes.INVALID_CONFIG);
const { rpc, rpcSubscriptions, sendAndConfirmTransaction } = createSolanaClient({ urlOrMoniker: rpcEndpoint });
this.rpc = rpc;
this.rpcSubscriptions = rpcSubscriptions;
this.sendAndConfirmTransaction = sendAndConfirmTransaction;
}
async pda(seeds, programId) {
const addressEncoder = getAddressEncoder();
const [pda] = await getProgramDerivedAddress({
programAddress: programId,
seeds: seeds.map(seed => typeof seed !== 'string' ? addressEncoder.encode(seed) : seed),
});
return pda;
}
async getBalance(addressStr) {
try {
this.sdk.logger.debug(`Getting balance for address: ${addressStr}`);
const addr = address(addressStr);
const balance = await this.rpc.getBalance(addr).send();
return balance.value;
}
catch (error) {
this.sdk.logger.error(`Failed to get balance: ${error}`);
throw new NosanaError('Failed to get balance', ErrorCodes.RPC_ERROR, error);
}
}
async getLatestBlockhash() {
try {
const { value: blockhash } = await this.rpc.getLatestBlockhash().send();
return blockhash;
}
catch (error) {
this.sdk.logger.error(`Failed to get latest blockhash: ${error}`);
throw new NosanaError('Failed to get latest blockhash', ErrorCodes.RPC_ERROR, error);
}
}
/**
* Type guard to check if the input is a transaction
*/
isTransaction(input) {
return 'instructions' in input && 'version' in input && !('programAddress' in input);
}
/**
* Type guard to check if the input is a signed transaction
*/
isSignedTransaction(input) {
return 'signatures' in input && input.signatures && input.signatures.length > 0;
}
/**
* Create, sign, and send a transaction with proper logging and error handling
* @param instructionsOrTransaction Single instruction, array of instructions, or pre-built transaction
* @returns The transaction signature
*/
async send(instructionsOrTransaction) {
if (!this.sdk.wallet) {
throw new NosanaError('No wallet found', ErrorCodes.NO_WALLET);
}
try {
let signedTransaction;
// Check if it's already a transaction or if we need to create one from instructions
if (this.isTransaction(instructionsOrTransaction)) {
// Check if it's already signed
if (this.isSignedTransaction(instructionsOrTransaction)) {
// Already signed, use it directly
signedTransaction = instructionsOrTransaction;
}
else {
// Not signed yet, sign it
signedTransaction = await signTransactionMessageWithSigners(instructionsOrTransaction);
}
}
else {
// It's instructions, create and sign a transaction
const instructions = Array.isArray(instructionsOrTransaction)
? instructionsOrTransaction
: [instructionsOrTransaction];
const transaction = createTransaction({
instructions,
feePayer: this.sdk.wallet,
latestBlockhash: await this.getLatestBlockhash(),
version: 0,
});
// Sign the transaction with all required signers
signedTransaction = await signTransactionMessageWithSigners(transaction);
}
// Get the transaction signature for logging
const signature = getSignatureFromTransaction(signedTransaction);
// Log the transaction explorer link
const explorerLink = getExplorerLink({
cluster: this.sdk.config.solana.cluster,
transaction: signature
});
this.sdk.logger.info(`Sending transaction: ${explorerLink}`);
// Send and confirm the transaction
await this.sendAndConfirmTransaction(signedTransaction);
this.sdk.logger.info('Transaction confirmed!');
return signature;
}
catch (err) {
const errorMessage = `Failed to send transaction: ${err instanceof Error ? err.message : String(err)}`;
this.sdk.logger.error(errorMessage);
throw new Error(errorMessage);
}
}
}