UNPKG

@saberhq/sail

Version:

Account caching and batched loading for React-based Solana applications.

294 lines (267 loc) 7.51 kB
import type { Network, PendingTransaction, TransactionEnvelope, } from "@saberhq/solana-contrib"; import type { Commitment, KeyedAccountInfo, PublicKey, TransactionSignature, } from "@solana/web3.js"; import type { ProgramAccountParser } from "../parsers/programAccounts"; import type { TransactionErrorType } from "./categorizeTransactionError"; import { categorizeTransactionError } from "./categorizeTransactionError"; export type SailErrorName = `Sail${ | "UnknownTXFail" | "Transaction" | "InsufficientSOL" | "RefetchAfterTX" | "RefetchSubscriptions" | "TransactionSign" | "CacheRefetch" | "AccountParse" | "ProgramAccountParse" | "AccountLoad" | "SignAndConfirm" | "GetMultipleAccounts"}Error`; export const ERROR_TITLES: { [N in SailErrorName]: string } = { SailUnknownTXFailError: "Transaction failed", SailTransactionError: "Transaction processing failed", SailInsufficientSOLError: "Insufficient SOL balance", SailRefetchAfterTXError: "Error fetching changed accounts", SailRefetchSubscriptionsError: "Error refetching subscribed accounts", SailTransactionSignError: "Error signing transactions", SailCacheRefetchError: "Error refetching from cache", SailAccountParseError: "Error parsing account", SailProgramAccountParseError: "Error parsing program account", SailAccountLoadError: "Error loading account", SailGetMultipleAccountsError: "Error fetching multiple accounts", SailSignAndConfirmError: "Error signing and confirming transactions", }; /** * Extracts the message from an error. * @param errLike Error-like object. * @returns */ export const extractErrorMessage = (errLike: unknown): string | null => { return "message" in (errLike as { message?: string }) ? (errLike as { message?: string }).message ?? null : null; }; /** * Error originating from Sail. */ export class SailError extends Error { _isSailError = true; constructor( /** * Name of the Sail error. */ readonly sailErrorName: SailErrorName, /** * The original error thrown, if applicable. */ readonly originalError: unknown, /** * Underlying error message. */ readonly cause = `${extractErrorMessage(originalError) ?? "unknown"}` ) { super(`${ERROR_TITLES[sailErrorName]}: ${cause}`); this.name = sailErrorName; if (originalError instanceof Error) { this.stack = originalError.stack; } } /** * Title of the error. */ get title(): string { return ERROR_TITLES[this.sailErrorName]; } /** * Returns the value as a {@link SailError}, or null if it isn't. * @param e * @returns */ static tryInto = (e: unknown): SailError | null => { return e instanceof SailError || ("_isSailError" in (e as SailError) && (e as SailError)._isSailError) ? (e as SailError) : null; }; } /** * Error originating from Sail. */ export class SailUnknownTXFailError extends SailError { constructor( originalError: unknown, readonly network: Network, readonly txs: readonly TransactionEnvelope[] ) { super("SailUnknownTXFailError", originalError); } } /** * Error on a Solana transaction */ export class SailTransactionError extends SailError { constructor( readonly network: Network, originalError: unknown, readonly tx: TransactionEnvelope, /** * User message representing the transaction. */ readonly userMessage?: string ) { super("SailTransactionError", originalError); } /** * Tag used for grouping errors together. */ get tag(): TransactionErrorType | null { return categorizeTransactionError(this.message); } /** * Returns true if this error is associated with a simulation. */ get isSimulation(): boolean { return this.message.includes("Transaction simulation failed: "); } /** * Fingerprint used for grouping errors. */ get fingerprint(): string[] { const tag = this.tag; if (tag) { return [this.name, tag]; } return [this.name, ...this.message.split(": ")]; } /** * Generates a debug string representation of the transactions involved in this error. * @param network * @returns */ generateLogMessage(): string { const parts = [this.tx.debugStr]; if (this.network !== "localnet") { parts.push( `Inspect transaction details: ${this.tx.generateInspectLink( this.network )}` ); } return parts.join("\n"); } } /** * Thrown if there is not enough SOL to pay for a transaction. */ export class InsufficientSOLError extends SailError { constructor(readonly currentBalance?: number | null) { super("SailInsufficientSOLError", null, "Insufficient SOL balance"); } } /** * Thrown if there is an error refetching accounts after a transaction. */ export class SailRefetchAfterTXError extends SailError { constructor( originalError: unknown, readonly writable: readonly PublicKey[], readonly txs: readonly PendingTransaction[] ) { super("SailRefetchAfterTXError", originalError); } /** * List of {@link TransactionSignature}s. */ get txSigs(): readonly TransactionSignature[] { return this.txs.map((tx) => tx.signature); } } /** * Thrown if an error occurs when refetching subscriptions. */ export class SailRefetchSubscriptionsError extends SailError { constructor(originalError: unknown) { super("SailRefetchSubscriptionsError", originalError); } } /** * Thrown if transactions could not be signed. */ export class SailTransactionSignError extends SailError { constructor( originalError: unknown, readonly txs: readonly TransactionEnvelope[] ) { super("SailTransactionSignError", originalError); } } /** * Thrown if a cache refetch results in an error. */ export class SailCacheRefetchError extends SailError { constructor( originalError: unknown, readonly keys: readonly (PublicKey | null | undefined)[] ) { super("SailCacheRefetchError", originalError); } } /** * Thrown if there is an error parsing an account. */ export class SailAccountParseError extends SailError { constructor(originalError: unknown, readonly data: KeyedAccountInfo) { super("SailAccountParseError", originalError); } } /** * Thrown if there is an error parsing an account. */ export class SailProgramAccountParseError extends SailError { constructor( originalError: unknown, readonly data: KeyedAccountInfo, readonly parser: ProgramAccountParser<unknown> ) { super("SailProgramAccountParseError", originalError); } } /** * Thrown if an account could not be loaded. */ export class SailAccountLoadError extends SailError { constructor(originalError: unknown, readonly accountId: PublicKey) { super("SailAccountLoadError", originalError); } get userMessage(): string { return `Error loading account ${this.accountId.toString()}`; } } /** * Callback called whenever getMultipleAccounts fails. */ export class SailGetMultipleAccountsError extends SailError { constructor( readonly keys: readonly PublicKey[], readonly commitment: Commitment, originalError: unknown ) { super("SailGetMultipleAccountsError", originalError); } } /** * Callback called whenever getMultipleAccounts fails. */ export class SailSignAndConfirmError extends SailError { constructor(readonly errors: readonly SailError[] | undefined) { super("SailSignAndConfirmError", errors); } }