@solana/transaction-confirmation
Version:
Helpers for confirming Solana transactions
132 lines (128 loc) • 5.64 kB
text/typescript
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();
}
};
}