UNPKG

@saberhq/sail

Version:

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

170 lines 7.66 kB
import { mapN } from "@saberhq/solana-contrib"; import { useSolana } from "@saberhq/use-solana"; import { useCallback } from "react"; import invariant from "tiny-invariant"; import { uniqKeys } from ".."; import { extractErrorMessage, InsufficientSOLError, SailError, SailRefetchAfterTXError, SailTransactionError, SailTransactionSignError, SailUnknownTXFailError, } from "../errors"; const DEBUG_MODE = !!process.env.REACT_APP_LOCAL_PUBKEY || !!process.env.LOCAL_PUBKEY || !!process.env.DEBUG_MODE; /** * Generates a random identifier for a handled transaction. * @returns string */ const genRandomBundleID = () => `bundle-${Math.random()}`; export const useHandleTXsInternal = ({ refetchMany, onBeforeTxSend, onTxSend, onError, txRefetchDelayMs = 1000, waitForConfirmation = false, }) => { const { network } = useSolana(); const handleTXs = useCallback(async (txs, message, options) => { var _a, _b; if (txs.length === 0) { return { success: true, pending: [], }; } if ((_a = options === null || options === void 0 ? void 0 : options.debugMode) !== null && _a !== void 0 ? _a : DEBUG_MODE) { const txTable = await Promise.all(txs.map(async (tx) => { return await tx.simulateTable({ verifySigners: false, ...options }); })); txs.forEach((tx, i) => { const table = txTable[i]; if (network !== "localnet") { console.debug(tx.generateInspectLink(network)); } console.debug(table); }); } const bundleID = genRandomBundleID(); onBeforeTxSend === null || onBeforeTxSend === void 0 ? void 0 : onBeforeTxSend({ bundleID, network, txs, message }); try { const firstTX = txs[0]; invariant(firstTX, "firstTX"); const provider = firstTX.provider; // TODO(igm): when we support other accounts being the payer, // we need to alter this check const [nativeAccount] = await refetchMany([provider.wallet.publicKey]); const nativeBalance = mapN((nativeAccount) => "data" in nativeAccount ? nativeAccount.lamports : null, nativeAccount); if (!nativeBalance) { const error = new InsufficientSOLError(nativeBalance); onError(error); return { success: false, pending: [], errors: [error], }; } let signedTXs; try { signedTXs = await provider.signer.signAll(txs.map((tx) => ({ tx: tx.build(), signers: tx.signers })), options); } catch (e) { const fail = new SailTransactionSignError(e, txs); onError(fail); return { success: false, pending: [], errors: [fail], }; } const errors = []; const maybePending = await Promise.all(signedTXs.map(async (signedTX, i) => { try { return await provider.broadcaster.broadcast(signedTX, options); } catch (e) { const txEnvelope = txs[i]; if (!txEnvelope) { // should be impossible throw new Error(`Unknown TX: ${i} of ${txs.length}`); } const txError = new SailTransactionError(network, e, txEnvelope, message); console.error(`Error sending TX ${i}: ${txError.message}`); console.debug(txError.generateLogMessage()); errors.push(txError); onError(txError); return null; } })); // if any TXs could not send, do not continue. const pending = maybePending.filter((p) => !!p); if (errors.length > 0) { // don't throw anything here because we already threw the errors above return { success: false, pending, errors, }; } // get the unique writable keys for every transaction const writable = uniqKeys(txs.flatMap((tx) => tx.writableKeys)); // refetch everything void (async () => { var _a; const refetchAfterTX = options === null || options === void 0 ? void 0 : options.refetchAfterTX; try { // wait for the tx to be confirmed // it is possible that it never gets await Promise.all(pending.map((p) => p .wait({ commitment: "confirmed", minTimeout: 1000, ...refetchAfterTX, }) .catch((err) => { var _a; throw new Error(`Could not await confirmation of transaction ${p.signature}: ${(_a = extractErrorMessage(err)) !== null && _a !== void 0 ? _a : "unknown"}`); }))); // then fetch, after a delay setTimeout(() => { void refetchMany(writable).catch((e) => { onError(new SailRefetchAfterTXError(e, writable, pending)); }); }, (_a = refetchAfterTX === null || refetchAfterTX === void 0 ? void 0 : refetchAfterTX.refetchDelayMs) !== null && _a !== void 0 ? _a : txRefetchDelayMs); } catch (e) { onError(new SailRefetchAfterTXError(e, writable, pending)); } })(); onTxSend === null || onTxSend === void 0 ? void 0 : onTxSend({ bundleID, network, txs, pending, message }); if (waitForConfirmation) { // await for the tx to be confirmed await Promise.all(pending.map((p) => p.wait())); } return { success: true, pending, }; } catch (e) { // Log the instruction logs console.error("Transaction failed.", e); txs.forEach((tx, i) => { if (txs.length > 1) { console.debug(`TX #${i + 1} of ${txs.length}`); } console.debug(tx.debugStr); if (network !== "localnet") { console.debug(`View on Solana Explorer: ${tx.generateInspectLink(network)}`); } }); const sailError = (_b = SailError.tryInto(e)) !== null && _b !== void 0 ? _b : new SailUnknownTXFailError(e, network, txs); onError(sailError); return { success: false, pending: [], errors: [sailError] }; } }, [ network, onBeforeTxSend, onError, onTxSend, refetchMany, txRefetchDelayMs, waitForConfirmation, ]); const handleTX = useCallback(async (txEnv, message, options) => { var _a; const { success, pending, errors } = await handleTXs([txEnv], message, options); return { success, pending: (_a = pending[0]) !== null && _a !== void 0 ? _a : null, errors }; }, [handleTXs]); return { handleTX, handleTXs }; }; //# sourceMappingURL=useHandleTXs.js.map