UNPKG

@solana/kit

Version:

Solana Javascript API

173 lines (162 loc) 7.68 kB
import { getSolanaErrorFromTransactionError, isSolanaError, SOLANA_ERROR__INVALID_NONCE } from '@solana/errors'; import { Signature } from '@solana/keys'; import type { GetAccountInfoApi, GetSignatureStatusesApi, Rpc, SendTransactionApi } from '@solana/rpc'; import type { AccountNotificationsApi, RpcSubscriptions, SignatureNotificationsApi } from '@solana/rpc-subscriptions'; import { commitmentComparator } from '@solana/rpc-types'; import { createNonceInvalidationPromiseFactory, createRecentSignatureConfirmationPromiseFactory, waitForDurableNonceTransactionConfirmation, } from '@solana/transaction-confirmation'; import { getSignatureFromTransaction, SendableTransaction, Transaction, TransactionWithDurableNonceLifetime, } from '@solana/transactions'; import { sendAndConfirmDurableNonceTransaction_INTERNAL_ONLY_DO_NOT_EXPORT } from './send-transaction-internal'; type SendAndConfirmDurableNonceTransactionFunction = ( transaction: SendableTransaction & Transaction & TransactionWithDurableNonceLifetime, config: Omit< Parameters<typeof sendAndConfirmDurableNonceTransaction_INTERNAL_ONLY_DO_NOT_EXPORT>[0], 'confirmDurableNonceTransaction' | 'rpc' | 'transaction' >, ) => Promise<void>; type SendAndConfirmDurableNonceTransactionFactoryConfig<TCluster> = { /** An object that supports the {@link GetSignatureStatusesApi} and the {@link SendTransactionApi} of the Solana RPC API */ rpc: Rpc<GetAccountInfoApi & GetSignatureStatusesApi & SendTransactionApi> & { '~cluster'?: TCluster }; /** An object that supports the {@link AccountNotificationsApi} and the {@link SignatureNotificationsApi} of the Solana RPC Subscriptions API */ rpcSubscriptions: RpcSubscriptions<AccountNotificationsApi & SignatureNotificationsApi> & { '~cluster'?: TCluster }; }; /** * Returns a function that you can call to send a nonce-based transaction to the network and to wait * until it has been confirmed. * * @param config * * @example * ```ts * import { * isSolanaError, * sendAndConfirmDurableNonceTransactionFactory, * SOLANA_ERROR__INVALID_NONCE, * SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND, * } from '@solana/kit'; * * const sendAndConfirmNonceTransaction = sendAndConfirmDurableNonceTransactionFactory({ rpc, rpcSubscriptions }); * * try { * await sendAndConfirmNonceTransaction(transaction, { commitment: 'confirmed' }); * } catch (e) { * if (isSolanaError(e, SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND)) { * console.error( * 'The lifetime specified by this transaction refers to a nonce account ' + * `\`${e.context.nonceAccountAddress}\` that does not exist`, * ); * } else if (isSolanaError(e, SOLANA_ERROR__INVALID_NONCE)) { * console.error('This transaction depends on a nonce that is no longer valid'); * } else { * throw e; * } * } * ``` */ export function sendAndConfirmDurableNonceTransactionFactory({ rpc, rpcSubscriptions, }: SendAndConfirmDurableNonceTransactionFactoryConfig<'devnet'>): SendAndConfirmDurableNonceTransactionFunction; export function sendAndConfirmDurableNonceTransactionFactory({ rpc, rpcSubscriptions, }: SendAndConfirmDurableNonceTransactionFactoryConfig<'testnet'>): SendAndConfirmDurableNonceTransactionFunction; export function sendAndConfirmDurableNonceTransactionFactory({ rpc, rpcSubscriptions, }: SendAndConfirmDurableNonceTransactionFactoryConfig<'mainnet'>): SendAndConfirmDurableNonceTransactionFunction; export function sendAndConfirmDurableNonceTransactionFactory< TCluster extends 'devnet' | 'mainnet' | 'testnet' | void = void, >({ rpc, rpcSubscriptions, }: SendAndConfirmDurableNonceTransactionFactoryConfig<TCluster>): SendAndConfirmDurableNonceTransactionFunction { const getNonceInvalidationPromise = createNonceInvalidationPromiseFactory({ rpc, rpcSubscriptions } as Parameters< typeof createNonceInvalidationPromiseFactory >[0]); const getRecentSignatureConfirmationPromise = createRecentSignatureConfirmationPromiseFactory({ rpc, rpcSubscriptions, } as Parameters<typeof createRecentSignatureConfirmationPromiseFactory>[0]); /** * Creates a wrapped version of getNonceInvalidationPromise that handles the race condition * where the nonce account update notification arrives before the signature confirmation. * * When the nonce changes, we check if our transaction actually landed on-chain. * If it did, we don't throw - letting the signature confirmation promise continue. */ function createNonceInvalidationPromiseHandlingRaceCondition( signature: Signature, ): typeof getNonceInvalidationPromise { return async function wrappedGetNonceInvalidationPromise(config) { try { return await getNonceInvalidationPromise(config); } catch (e) { // If nonce became invalid, check if our transaction actually landed if (isSolanaError(e, SOLANA_ERROR__INVALID_NONCE)) { let status; try { const { value: statuses } = await rpc .getSignatureStatuses([signature]) .send({ abortSignal: config.abortSignal }); status = statuses[0]; } catch { // RPC failed - propagate the original nonce error throw e; } if (status === null || status === undefined) { // Transaction doesn't exist - nonce was truly invalid throw e; } // Check if status meets required commitment if ( status.confirmationStatus !== null && commitmentComparator(status.confirmationStatus, config.commitment) >= 0 ) { // Transaction failed on-chain, throw the error from the transaction if (status.err !== null) { throw getSolanaErrorFromTransactionError(status.err); } // Transaction succeeded, resolve the promise successfully return; } // Commitment not met yet - return a never-resolving promise // This lets the signature confirmation promise continue return await new Promise(() => {}); } throw e; } }; } async function confirmDurableNonceTransaction( config: Omit< Parameters<typeof waitForDurableNonceTransactionConfirmation>[0], 'getNonceInvalidationPromise' | 'getRecentSignatureConfirmationPromise' >, ) { const wrappedGetNonceInvalidationPromise = createNonceInvalidationPromiseHandlingRaceCondition( getSignatureFromTransaction(config.transaction), ); await waitForDurableNonceTransactionConfirmation({ ...config, getNonceInvalidationPromise: wrappedGetNonceInvalidationPromise, getRecentSignatureConfirmationPromise, }); } return async function sendAndConfirmDurableNonceTransaction(transaction, config) { await sendAndConfirmDurableNonceTransaction_INTERNAL_ONLY_DO_NOT_EXPORT({ ...config, confirmDurableNonceTransaction, rpc, transaction, }); }; }