UNPKG

@macalinao/grill

Version:

Modern Solana development kit for React applications with automatic account batching, caching, and transaction notifications

204 lines (185 loc) 6.15 kB
import type { Address, Instruction, Signature, SignatureBytes, TransactionSendingSigner, } from "@solana/kit"; import { compressTransactionMessageUsingAddressLookupTables, signAndSendTransactionMessageWithSigners, } from "@solana/kit"; import type { SolanaClient } from "gill"; import { createTransaction } from "gill"; import type { GetExplorerLinkFunction } from "../../contexts/grill-context.js"; import type { TransactionStatusEvent } from "../../types.js"; import { getSignatureFromBytes } from "../get-signature-from-bytes.js"; import { pollConfirmTransaction } from "../poll-confirm-transaction.js"; import type { SendTXFunction, SendTXOptions } from "../types.js"; export interface CreateSendTXParams { signer: TransactionSendingSigner | null; rpc: SolanaClient["rpc"]; refetchAccounts: (addresses: Address[]) => Promise<void>; onTransactionStatusEvent: (event: TransactionStatusEvent) => void; getExplorerLink: GetExplorerLinkFunction; } /** * Creates a function to send transactions using the modern @solana/kit API * while maintaining compatibility with the wallet adapter. */ export const createSendTX = ({ signer, rpc, refetchAccounts, onTransactionStatusEvent, getExplorerLink, }: CreateSendTXParams): SendTXFunction => { return async ( name: string, ixs: readonly Instruction[], options: SendTXOptions = {}, ): Promise<Signature> => { const txId = Math.random().toString(36).substring(2, 15); const baseEvent = { id: txId, title: name, }; if (!signer) { onTransactionStatusEvent({ ...baseEvent, type: "error-wallet-not-connected", }); throw new Error("Wallet not connected"); } onTransactionStatusEvent({ ...baseEvent, type: "preparing", }); const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); const transactionMessage = createTransaction({ version: 0, feePayer: signer, instructions: [...ixs], latestBlockhash, // the compute budget values are HIGHLY recommend to be set in order to maximize your transaction landing rate computeUnitLimit: options.computeUnitLimit === null ? undefined : (options.computeUnitLimit ?? 1_400_000), computeUnitPrice: options.computeUnitPrice === null ? undefined : (options.computeUnitPrice ?? 100_000n), }); // Apply address lookup tables if provided to compress the transaction const addressLookupTables = options.lookupTables ?? {}; const finalTransactionMessage = Object.keys(addressLookupTables).length > 0 ? compressTransactionMessageUsingAddressLookupTables( transactionMessage, addressLookupTables, ) : transactionMessage; onTransactionStatusEvent({ ...baseEvent, type: "awaiting-wallet-signature", }); // Send transaction using wallet adapter let sigBytes: SignatureBytes; try { sigBytes = await signAndSendTransactionMessageWithSigners( finalTransactionMessage, ); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : "Failed to send transaction"; onTransactionStatusEvent({ ...baseEvent, type: "error-transaction-send-failed", errorMessage, }); throw error; } const sig = getSignatureFromBytes(sigBytes); const sentTxEvent = { ...baseEvent, sig, explorerLink: getExplorerLink({ transaction: sig }), }; onTransactionStatusEvent({ ...sentTxEvent, type: "waiting-for-confirmation", }); try { const result = await pollConfirmTransaction({ signature: sig, lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, rpc, }); onTransactionStatusEvent({ ...sentTxEvent, type: "confirmed", }); // Reload the accounts that were written to const writableAccounts = result.transaction.message.accountKeys .filter((key) => key.writable) .map((k) => k.pubkey); if (writableAccounts.length > 0) { const waitForAccountRefetch = options.waitForAccountRefetch ?? true; if (waitForAccountRefetch) { await refetchAccounts(writableAccounts); } else { // Refetch in background without waiting refetchAccounts(writableAccounts).catch((error: unknown) => { console.warn("Failed to refetch accounts in background:", error); }); } } if (result.meta?.logMessages) { console.log(name, result.meta.logMessages.join("\n")); } // Return the signature as a base58 string return sig; } catch (error: unknown) { // Log error details for debugging console.error(`${name} transaction failed:`, error); // Extract error logs const extractErrorLogs = (err: unknown): string[] => { if ( err && typeof err === "object" && "logs" in err && Array.isArray((err as { logs: unknown }).logs) ) { return (err as { logs: string[] }).logs; } if ( err && typeof err === "object" && "context" in err && typeof (err as { context: unknown }).context === "object" && (err as { context: { logs?: unknown } }).context.logs && Array.isArray((err as { context: { logs: unknown } }).context.logs) ) { return (err as { context: { logs: string[] } }).context.logs; } return []; }; const errorLogs = extractErrorLogs(error); if (errorLogs.length > 0) { console.log("Transaction logs:"); for (const log of errorLogs) { console.log(" ", log); } } const errorMessage = error instanceof Error ? error.message : "Transaction failed."; onTransactionStatusEvent({ ...sentTxEvent, type: "error-transaction-failed", errorMessage, }); throw error; } }; };