@macalinao/grill
Version:
Modern Solana development kit for React applications with automatic account batching, caching, and transaction notifications
249 lines (227 loc) • 7.17 kB
text/typescript
import type {
GetExplorerLinkFunction,
SendTXFunction,
SendTXOptions,
SolanaCluster,
} from "@macalinao/gill-extra";
import type {
Address,
Instruction,
Signature,
SignatureBytes,
TransactionSendingSigner,
} from "@solana/kit";
import type { SolanaClient } from "gill";
import type { TransactionStatusEvent } from "../../types.js";
import {
getSignatureFromBytes,
logTransactionSimulation,
parseTransactionError,
pollConfirmTransaction,
} from "@macalinao/gill-extra";
import {
compressTransactionMessageUsingAddressLookupTables,
getSolanaErrorFromTransactionError,
signAndSendTransactionMessageWithSigners,
} from "@solana/kit";
import { createTransaction, simulateTransactionFactory } from "gill";
export interface CreateSendTXParams {
signer: TransactionSendingSigner | null;
rpc: SolanaClient["rpc"];
refetchAccounts: (addresses: Address[]) => Promise<void>;
onTransactionStatusEvent: (event: TransactionStatusEvent) => void;
getExplorerLink: GetExplorerLinkFunction;
/**
* The RPC URL used for creating transaction inspector URLs.
* This is needed to generate correct inspector URLs for custom RPC endpoints.
*/
rpcUrl?: string;
/**
* The Solana cluster for explorer links.
* Defaults to "mainnet-beta".
*/
cluster?: SolanaCluster;
}
/**
* 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,
rpcUrl,
cluster = "mainnet-beta",
}: CreateSendTXParams): SendTXFunction => {
const simulateTransaction = simulateTransactionFactory({ rpc });
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,
computeUnitLimit: options.computeUnitLimit,
computeUnitPrice: options.computeUnitPrice,
});
// 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;
// preflight
if (!options.skipPreflight) {
const simulationResult = await simulateTransaction(
finalTransactionMessage,
);
if (simulationResult.value.err) {
// Log detailed debugging information to the console
logTransactionSimulation({
title: name,
simulationResult: simulationResult.value,
transactionMessage: finalTransactionMessage,
cluster,
rpcUrl,
});
const logs = simulationResult.value.logs ?? [];
const errorMessage = parseTransactionError(
simulationResult.value.err,
logs,
);
onTransactionStatusEvent({
...baseEvent,
type: "error-simulation-failed",
errorMessage,
});
throw getSolanaErrorFromTransactionError(simulationResult.value.err);
}
}
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;
}
};
};