From 94944b65b9d957ca95653d66dc1f4805f1a36740 Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Tue, 20 Feb 2024 14:59:27 -0800 Subject: [PATCH] Convert nonce transaction confirmation exceptions into coded exceptions (#2161) --- packages/errors/src/codes.ts | 6 +++- packages/errors/src/context.ts | 9 ++++++ packages/errors/src/messages.ts | 5 ++++ ...action-confirmation-strategy-nonce-test.ts | 28 ++++++++++++++++--- ...transaction-confirmation-strategy-nonce.ts | 26 +++++++++-------- 5 files changed, 58 insertions(+), 16 deletions(-) diff --git a/packages/errors/src/codes.ts b/packages/errors/src/codes.ts index 476ec5d13bbd..87ba91481783 100644 --- a/packages/errors/src/codes.ts +++ b/packages/errors/src/codes.ts @@ -11,6 +11,8 @@ export const SOLANA_ERROR__TRANSACTION_SIGNATURE_NOT_COMPUTABLE = 2 as const; export const SOLANA_ERROR__RPC_INTEGER_OVERFLOW = 3 as const; export const SOLANA_ERROR__INVALID_KEYPAIR_BYTES = 4 as const; export const SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED = 5 as const; +export const SOLANA_ERROR__NONCE_INVALID = 6 as const; +export const SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND = 7 as const; /** * A union of every Solana error code @@ -32,4 +34,6 @@ export type SolanaErrorCode = | typeof SOLANA_ERROR__TRANSACTION_SIGNATURE_NOT_COMPUTABLE | typeof SOLANA_ERROR__RPC_INTEGER_OVERFLOW | typeof SOLANA_ERROR__INVALID_KEYPAIR_BYTES - | typeof SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED; + | typeof SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED + | typeof SOLANA_ERROR__NONCE_INVALID + | typeof SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND; diff --git a/packages/errors/src/context.ts b/packages/errors/src/context.ts index 2bd3577c47f0..6cbaef6bee9d 100644 --- a/packages/errors/src/context.ts +++ b/packages/errors/src/context.ts @@ -1,6 +1,8 @@ import { SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED, SOLANA_ERROR__INVALID_KEYPAIR_BYTES, + SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND, + SOLANA_ERROR__NONCE_INVALID, SOLANA_ERROR__RPC_INTEGER_OVERFLOW, SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES, SolanaErrorCode, @@ -36,4 +38,11 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<{ [SOLANA_ERROR__INVALID_KEYPAIR_BYTES]: { byteLength: number; }; + [SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND]: { + nonceAccountAddress: string; + }; + [SOLANA_ERROR__NONCE_INVALID]: { + actualNonceValue: string; + expectedNonceValue: string; + }; }>; diff --git a/packages/errors/src/messages.ts b/packages/errors/src/messages.ts index 1aadab114529..d3c348cadc01 100644 --- a/packages/errors/src/messages.ts +++ b/packages/errors/src/messages.ts @@ -1,6 +1,8 @@ import { SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED, SOLANA_ERROR__INVALID_KEYPAIR_BYTES, + SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND, + SOLANA_ERROR__NONCE_INVALID, SOLANA_ERROR__RPC_INTEGER_OVERFLOW, SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES, SOLANA_ERROR__TRANSACTION_SIGNATURE_NOT_COMPUTABLE, @@ -22,6 +24,9 @@ export const SolanaErrorMessages: Readonly<{ [SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED]: 'The network has progressed past the last block for which this transaction could have been committed.', [SOLANA_ERROR__INVALID_KEYPAIR_BYTES]: 'Key pair bytes must be of length 64, got $byteLength.', + [SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND]: 'No nonce account could be found at address `$nonceAccountAddress`', + [SOLANA_ERROR__NONCE_INVALID]: + 'The nonce `$expectedNonceValue` is no longer valid. It has advanced to `$actualNonceValue`', [SOLANA_ERROR__RPC_INTEGER_OVERFLOW]: 'The $argumentLabel argument to the `$methodName` RPC method$optionalPathLabel was ' + '`$value`. This number is unsafe for use with the Solana JSON-RPC because it exceeds ' + diff --git a/packages/library/src/__tests__/transaction-confirmation-strategy-nonce-test.ts b/packages/library/src/__tests__/transaction-confirmation-strategy-nonce-test.ts index c551c7edac87..a89359fc49e3 100644 --- a/packages/library/src/__tests__/transaction-confirmation-strategy-nonce-test.ts +++ b/packages/library/src/__tests__/transaction-confirmation-strategy-nonce-test.ts @@ -1,5 +1,6 @@ import { Address } from '@solana/addresses'; import { getBase58Encoder, getBase64Decoder } from '@solana/codecs'; +import { SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND, SOLANA_ERROR__NONCE_INVALID, SolanaError } from '@solana/errors'; import { Nonce } from '@solana/transactions'; import { createNonceInvalidationPromiseFactory } from '../transaction-confirmation-strategy-nonce'; @@ -140,6 +141,21 @@ describe('createNonceInvalidationPromiseFactory', () => { await jest.runAllTimersAsync(); await expect(Promise.race([invalidationPromise, 'pending'])).resolves.toBe('pending'); }); + it('fatals when the nonce account can not be found', async () => { + expect.assertions(1); + getAccountInfoMock.mockResolvedValue({ value: null }); + const invalidationPromise = getNonceInvalidationPromise({ + abortSignal: new AbortController().signal, + commitment: 'finalized', + currentNonceValue: '4'.repeat(44) as Nonce, + nonceAccountAddress: '9'.repeat(44) as Address, + }); + await expect(invalidationPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND, { + nonceAccountAddress: '9'.repeat(44), + }), + ); + }); it('fatals when the nonce value returned by the one-shot query is different than the expected one', async () => { expect.assertions(1); getAccountInfoMock.mockResolvedValue({ @@ -154,8 +170,10 @@ describe('createNonceInvalidationPromiseFactory', () => { nonceAccountAddress: '9'.repeat(44) as Address, }); await expect(invalidationPromise).rejects.toThrow( - 'The nonce `44444444444444444444444444444444444444444444` is no longer valid. It has advanced to ' + - '`55555555555555555555555555555555555555555555`.', + new SolanaError(SOLANA_ERROR__NONCE_INVALID, { + actualNonceValue: '55555555555555555555555555555555555555555555', + expectedNonceValue: '44444444444444444444444444444444444444444444', + }), ); }); it('continues to pend when the nonce value returned by the account subscription is the same as the expected one', async () => { @@ -190,8 +208,10 @@ describe('createNonceInvalidationPromiseFactory', () => { nonceAccountAddress: '9'.repeat(44) as Address, }); await expect(invalidationPromise).rejects.toThrow( - 'The nonce `44444444444444444444444444444444444444444444` is no longer valid. It has advanced to ' + - '`55555555555555555555555555555555555555555555`.', + new SolanaError(SOLANA_ERROR__NONCE_INVALID, { + actualNonceValue: '55555555555555555555555555555555555555555555', + expectedNonceValue: '44444444444444444444444444444444444444444444', + }), ); }); }); diff --git a/packages/library/src/transaction-confirmation-strategy-nonce.ts b/packages/library/src/transaction-confirmation-strategy-nonce.ts index 34ebca2eb2a3..9386437fb679 100644 --- a/packages/library/src/transaction-confirmation-strategy-nonce.ts +++ b/packages/library/src/transaction-confirmation-strategy-nonce.ts @@ -1,5 +1,6 @@ import { Address } from '@solana/addresses'; import { getBase58Decoder, getBase64Encoder } from '@solana/codecs'; +import { SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND, SOLANA_ERROR__NONCE_INVALID, SolanaError } from '@solana/errors'; import type { AccountNotificationsApi, GetAccountInfoApi } from '@solana/rpc-core'; import type { Base64EncodedDataResponse, Commitment, Rpc, RpcSubscriptions } from '@solana/rpc-types'; import { Nonce } from '@solana/transactions'; @@ -24,7 +25,7 @@ export function createNonceInvalidationPromiseFactory( return async function getNonceInvalidationPromise({ abortSignal: callerAbortSignal, commitment, - currentNonceValue, + currentNonceValue: expectedNonceValue, nonceAccountAddress, }) { const abortController = new AbortController(); @@ -48,11 +49,11 @@ export function createNonceInvalidationPromiseFactory( const nonceAccountDidAdvancePromise = (async () => { for await (const accountNotification of accountNotifications) { const nonceValue = getNonceFromAccountData(accountNotification.value.data); - if (nonceValue !== currentNonceValue) { - throw new Error( - `The nonce \`${currentNonceValue}\` is no longer valid. It has advanced ` + - `to \`${nonceValue}\`.`, - ); + if (nonceValue !== expectedNonceValue) { + throw new SolanaError(SOLANA_ERROR__NONCE_INVALID, { + actualNonceValue: nonceValue, + expectedNonceValue, + }); } } })(); @@ -69,16 +70,19 @@ export function createNonceInvalidationPromiseFactory( }) .send({ abortSignal: abortController.signal }); if (!nonceAccount) { - throw new Error(`No nonce account could be found at address \`${nonceAccountAddress}\`.`); + throw new SolanaError(SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND, { + nonceAccountAddress, + }); } const nonceValue = // This works because we asked for the exact slice of data representing the nonce // value, and furthermore asked for it in `base58` encoding. nonceAccount.data[0] as unknown as Nonce; - if (nonceValue !== currentNonceValue) { - throw new Error( - `The nonce \`${currentNonceValue}\` is no longer valid. It has advanced to \`${nonceValue}\`.`, - ); + if (nonceValue !== expectedNonceValue) { + throw new SolanaError(SOLANA_ERROR__NONCE_INVALID, { + actualNonceValue: nonceValue, + expectedNonceValue, + }); } else { await new Promise(() => { /* never resolve */