Skip to content

Commit

Permalink
Convert nonce transaction confirmation exceptions into coded exceptio…
Browse files Browse the repository at this point in the history
…ns (#2161)
  • Loading branch information
steveluscher authored Feb 20, 2024
1 parent 3524f2c commit 94944b6
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 16 deletions.
6 changes: 5 additions & 1 deletion packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
9 changes: 9 additions & 0 deletions packages/errors/src/context.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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;
};
}>;
5 changes: 5 additions & 0 deletions packages/errors/src/messages.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 ' +
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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({
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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',
}),
);
});
});
26 changes: 15 additions & 11 deletions packages/library/src/transaction-confirmation-strategy-nonce.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -24,7 +25,7 @@ export function createNonceInvalidationPromiseFactory(
return async function getNonceInvalidationPromise({
abortSignal: callerAbortSignal,
commitment,
currentNonceValue,
currentNonceValue: expectedNonceValue,
nonceAccountAddress,
}) {
const abortController = new AbortController();
Expand All @@ -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,
});
}
}
})();
Expand All @@ -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 */
Expand Down

0 comments on commit 94944b6

Please sign in to comment.