diff --git a/.changeset/happy-areas-spend.md b/.changeset/happy-areas-spend.md new file mode 100644 index 0000000..133cd0a --- /dev/null +++ b/.changeset/happy-areas-spend.md @@ -0,0 +1,6 @@ +--- +"@macalinao/gill-extra": patch +"@macalinao/grill": patch +--- + +Add parseTransactionError, new error type diff --git a/.changeset/silent-suits-return.md b/.changeset/silent-suits-return.md new file mode 100644 index 0000000..d2d748f --- /dev/null +++ b/.changeset/silent-suits-return.md @@ -0,0 +1,6 @@ +--- +"@macalinao/gill-extra": minor +"@macalinao/grill": minor +--- + +Change compute unit stuff to be not included by default diff --git a/packages/gill-extra/src/index.ts b/packages/gill-extra/src/index.ts index 6422270..e37fc51 100644 --- a/packages/gill-extra/src/index.ts +++ b/packages/gill-extra/src/index.ts @@ -13,4 +13,5 @@ export * from "./get-solscan-explorer-link.js"; export * from "./ixs/index.js"; export * from "./poll-confirm-transaction.js"; export * from "./transaction.js"; +export * from "./transaction-error.js"; export * from "./types.js"; diff --git a/packages/gill-extra/src/transaction-error.ts b/packages/gill-extra/src/transaction-error.ts new file mode 100644 index 0000000..e7d19dd --- /dev/null +++ b/packages/gill-extra/src/transaction-error.ts @@ -0,0 +1,22 @@ +import type { TransactionError } from "@solana/kit"; +import { getSolanaErrorFromTransactionError } from "@solana/kit"; + +export const parseTransactionError = ( + err: TransactionError, + logs: string[] | null, +): string => { + // First, try to extract Anchor error from logs + const anchorError = [...(logs ?? [])] + .reverse() + .find((log) => log.includes("AnchorError")); + + if (anchorError) { + const errorMessageStart = anchorError.indexOf("Error Message: "); + if (errorMessageStart !== -1) { + return anchorError.slice(errorMessageStart + 15).trim(); + } + } + + const solanaError = getSolanaErrorFromTransactionError(err); + return solanaError.message; +}; diff --git a/packages/gill-extra/src/types.ts b/packages/gill-extra/src/types.ts index 6e8f552..acf02d4 100644 --- a/packages/gill-extra/src/types.ts +++ b/packages/gill-extra/src/types.ts @@ -4,21 +4,17 @@ import type { Instruction, Signature, } from "@solana/kit"; +import type { CreateTransactionInput } from "gill"; -export interface SendTXOptions { - lookupTables?: AddressesByLookupTableAddress; - /** - * Compute unit limit for the transaction. - * Set to null to omit compute unit limit instruction. - * Defaults to 1,400,000 if not specified. - */ - computeUnitLimit?: number | null; +export interface SendTXOptions + extends Pick< + CreateTransactionInput<0>, + "computeUnitLimit" | "computeUnitPrice" + > { /** - * Compute unit price for the transaction in microlamports. - * Set to null to omit compute unit price instruction. - * Defaults to 100,000 if not specified. + * Address lookup tables (optional) */ - computeUnitPrice?: bigint | null; + lookupTables?: AddressesByLookupTableAddress; /** * Whether to wait for account refetch after transaction confirmation. * When true (default), the function will wait for all writable accounts @@ -28,6 +24,10 @@ export interface SendTXOptions { * @default true */ waitForAccountRefetch?: boolean; + /** + * If true, skips the pre-flight simulation. + */ + skipPreflight?: boolean; } export type SendTXFunction = ( diff --git a/packages/grill/src/providers/grill-provider.tsx b/packages/grill/src/providers/grill-provider.tsx index 71c766e..d6d4378 100644 --- a/packages/grill/src/providers/grill-provider.tsx +++ b/packages/grill/src/providers/grill-provider.tsx @@ -207,6 +207,27 @@ export const GrillProvider: FC = ({ toastIds.current.delete(txId); break; } + case "error-simulation-failed": { + console.error("Simulation failed", event); + const description = `Simulation failed: ${event.errorMessage}`; + if (existingToastId) { + // Update existing toast to error + toast.error(event.title, { + id: existingToastId, + description, + duration: errorToastDuration, + }); + } else { + // Create new error toast if somehow we don't have one + toast.error(event.title, { + description, + duration: errorToastDuration, + }); + } + // Clean up toast ID after error + toastIds.current.delete(txId); + break; + } } }, [ diff --git a/packages/grill/src/types.ts b/packages/grill/src/types.ts index b7f072a..4bb7ad7 100644 --- a/packages/grill/src/types.ts +++ b/packages/grill/src/types.ts @@ -35,6 +35,10 @@ export type TransactionStatusEvent = { sig: Signature; explorerLink: string; } + | { + type: "error-simulation-failed"; + errorMessage: string; + } ); export type TransactionStatusEventCallback = ( diff --git a/packages/grill/src/utils/internal/create-send-tx.ts b/packages/grill/src/utils/internal/create-send-tx.ts index a198d54..155ae4e 100644 --- a/packages/grill/src/utils/internal/create-send-tx.ts +++ b/packages/grill/src/utils/internal/create-send-tx.ts @@ -14,13 +14,15 @@ import type { SolanaClient } from "gill"; import type { TransactionStatusEvent } from "../../types.js"; import { getSignatureFromBytes, + parseTransactionError, pollConfirmTransaction, } from "@macalinao/gill-extra"; import { compressTransactionMessageUsingAddressLookupTables, + getSolanaErrorFromTransactionError, signAndSendTransactionMessageWithSigners, } from "@solana/kit"; -import { createTransaction } from "gill"; +import { createTransaction, simulateTransactionFactory } from "gill"; export interface CreateSendTXParams { signer: TransactionSendingSigner | null; @@ -41,6 +43,7 @@ export const createSendTX = ({ onTransactionStatusEvent, getExplorerLink, }: CreateSendTXParams): SendTXFunction => { + const simulateTransaction = simulateTransactionFactory({ rpc }); return async ( name: string, ixs: readonly Instruction[], @@ -70,15 +73,8 @@ export const createSendTX = ({ 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), + computeUnitLimit: options.computeUnitLimit, + computeUnitPrice: options.computeUnitPrice, }); // Apply address lookup tables if provided to compress the transaction @@ -91,6 +87,24 @@ export const createSendTX = ({ ) : transactionMessage; + // preflight + if (!options.skipPreflight) { + const simulationResult = await simulateTransaction( + finalTransactionMessage, + ); + if (simulationResult.value.err) { + onTransactionStatusEvent({ + ...baseEvent, + type: "error-simulation-failed", + errorMessage: parseTransactionError( + simulationResult.value.err, + simulationResult.value.logs, + ), + }); + throw getSolanaErrorFromTransactionError(simulationResult.value.err); + } + } + onTransactionStatusEvent({ ...baseEvent, type: "awaiting-wallet-signature",