@saberhq/sail
Version:
Account caching and batched loading for React-based Solana applications.
170 lines • 7.66 kB
JavaScript
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