UNPKG

@solana/transaction-confirmation

Version:

Helpers for confirming Solana transactions

132 lines (128 loc) 5.64 kB
import { getSolanaErrorFromTransactionError } from '@solana/errors'; import { AbortController } from '@solana/event-target-impl'; import type { Signature } from '@solana/keys'; import { safeRace } from '@solana/promises'; import type { GetSignatureStatusesApi, Rpc } from '@solana/rpc'; import type { RpcSubscriptions, SignatureNotificationsApi } from '@solana/rpc-subscriptions'; import { type Commitment, commitmentComparator } from '@solana/rpc-types'; type GetRecentSignatureConfirmationPromiseFn = (config: { abortSignal: AbortSignal; /** * The level of commitment the transaction must have achieved in order for the promise to * resolve. */ commitment: Commitment; /** * A 64 byte Ed25519 signature, encoded as a base-58 string, that uniquely identifies a * transaction by virtue of being the first or only signature in its list of signatures. */ signature: Signature; }) => Promise<void>; type CreateRecentSignatureConfirmationPromiseFactoryConfig<TCluster> = { rpc: Rpc<GetSignatureStatusesApi> & { '~cluster'?: TCluster }; rpcSubscriptions: RpcSubscriptions<SignatureNotificationsApi> & { '~cluster'?: TCluster }; }; /** * Creates a promise that resolves when a recently-landed transaction achieves the target * confirmation commitment, and throws when the transaction fails with an error. * * The status of recently-landed transactions is available in the network's status cache. This * confirmation strategy will only yield a result if the signature is still in the status cache. To * fetch the status of transactions older than those available in the status cache, use the * {@link GetSignatureStatusesApi.getSignatureStatuses} method setting the * `searchTransactionHistory` configuration param to `true`. * * @param config * * @example * ```ts * import { createRecentSignatureConfirmationPromiseFactory } from '@solana/transaction-confirmation'; * * const getRecentSignatureConfirmationPromise = createRecentSignatureConfirmationPromiseFactory({ * rpc, * rpcSubscriptions, * }); * try { * await getRecentSignatureConfirmationPromise({ * commitment, * signature, * }); * console.log(`The transaction with signature \`${signature}\` has achieved a commitment level of \`${commitment}\``); * } catch (e) { * console.error(`The transaction with signature \`${signature}\` failed`, e.cause); * throw e; * } * ``` */ export function createRecentSignatureConfirmationPromiseFactory({ rpc, rpcSubscriptions, }: CreateRecentSignatureConfirmationPromiseFactoryConfig<'devnet'>): GetRecentSignatureConfirmationPromiseFn; export function createRecentSignatureConfirmationPromiseFactory({ rpc, rpcSubscriptions, }: CreateRecentSignatureConfirmationPromiseFactoryConfig<'testnet'>): GetRecentSignatureConfirmationPromiseFn; export function createRecentSignatureConfirmationPromiseFactory({ rpc, rpcSubscriptions, }: CreateRecentSignatureConfirmationPromiseFactoryConfig<'mainnet'>): GetRecentSignatureConfirmationPromiseFn; export function createRecentSignatureConfirmationPromiseFactory< TCluster extends 'devnet' | 'mainnet' | 'testnet' | void = void, >({ rpc, rpcSubscriptions, }: CreateRecentSignatureConfirmationPromiseFactoryConfig<TCluster>): GetRecentSignatureConfirmationPromiseFn { return async function getRecentSignatureConfirmationPromise({ abortSignal: callerAbortSignal, commitment, signature, }) { const abortController = new AbortController(); function handleAbort() { abortController.abort(); } callerAbortSignal.addEventListener('abort', handleAbort, { signal: abortController.signal }); /** * STEP 1: Set up a subscription for status changes to a signature. */ const signatureStatusNotifications = await rpcSubscriptions .signatureNotifications(signature, { commitment }) .subscribe({ abortSignal: abortController.signal }); const signatureDidCommitPromise = (async () => { for await (const signatureStatusNotification of signatureStatusNotifications) { if (signatureStatusNotification.value.err) { throw getSolanaErrorFromTransactionError(signatureStatusNotification.value.err); } else { return; } } })(); /** * STEP 2: Having subscribed for updates, make a one-shot request for the current status. * This will only yield a result if the signature is still in the status cache. */ const signatureStatusLookupPromise = (async () => { const { value: signatureStatusResults } = await rpc .getSignatureStatuses([signature]) .send({ abortSignal: abortController.signal }); const signatureStatus = signatureStatusResults[0]; if (signatureStatus?.err) { throw getSolanaErrorFromTransactionError(signatureStatus.err); } else if ( signatureStatus?.confirmationStatus && commitmentComparator(signatureStatus.confirmationStatus, commitment) >= 0 ) { return; } else { await new Promise(() => { /* never resolve */ }); } })(); try { return await safeRace([signatureDidCommitPromise, signatureStatusLookupPromise]); } finally { abortController.abort(); } }; }