Skip to content

Commit

Permalink
Convert errors in @solana/transactions to coded exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
steveluscher committed Feb 27, 2024
1 parent cf80a05 commit 49bb70a
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 39 deletions.
20 changes: 19 additions & 1 deletion packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ 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;
export const SOLANA_ERROR__TRANSACTION_INVOKED_PROGRAMS_CANNOT_PAY_FEES = 99901 as const;
export const SOLANA_ERROR__TRANSACTION_INVOKED_PROGRAMS_MUST_NOT_BE_WRITABLE = 99902 as const;
export const SOLANA_ERROR__TRANSACTION_EXPECTED_BLOCKHASH_LIFETIME = 99903 as const;
export const SOLANA_ERROR__TRANSACTION_EXPECTED_NONCE_LIFETIME = 99904 as const;
export const SOLANA_ERROR__TRANSACTION_VERSION_NUMBER_OUT_OF_RANGE = 99905 as const;
export const SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING = 99906 as const;
export const SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_INDEX_OUT_OF_RANGE = 99907 as const;
export const SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND = 99908 as const;
export const SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_FEE_PAYER_MISSING = 99909 as const;

/**
* A union of every Solana error code
Expand All @@ -36,4 +45,13 @@ export type SolanaErrorCode =
| typeof SOLANA_ERROR__INVALID_KEYPAIR_BYTES
| typeof SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED
| typeof SOLANA_ERROR__NONCE_INVALID
| typeof SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND;
| typeof SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND
| typeof SOLANA_ERROR__TRANSACTION_INVOKED_PROGRAMS_CANNOT_PAY_FEES
| typeof SOLANA_ERROR__TRANSACTION_INVOKED_PROGRAMS_MUST_NOT_BE_WRITABLE
| typeof SOLANA_ERROR__TRANSACTION_EXPECTED_BLOCKHASH_LIFETIME
| typeof SOLANA_ERROR__TRANSACTION_EXPECTED_NONCE_LIFETIME
| typeof SOLANA_ERROR__TRANSACTION_VERSION_NUMBER_OUT_OF_RANGE
| typeof SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING
| typeof SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_INDEX_OUT_OF_RANGE
| typeof SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND
| typeof SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_FEE_PAYER_MISSING;
26 changes: 26 additions & 0 deletions packages/errors/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import {
SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND,
SOLANA_ERROR__NONCE_INVALID,
SOLANA_ERROR__RPC_INTEGER_OVERFLOW,
SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING,
SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_INDEX_OUT_OF_RANGE,
SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND,
SOLANA_ERROR__TRANSACTION_INVOKED_PROGRAMS_CANNOT_PAY_FEES,
SOLANA_ERROR__TRANSACTION_INVOKED_PROGRAMS_MUST_NOT_BE_WRITABLE,
SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES,
SOLANA_ERROR__TRANSACTION_VERSION_NUMBER_OUT_OF_RANGE,
SolanaErrorCode,
} from './codes';

Expand Down Expand Up @@ -45,4 +51,24 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<{
actualNonceValue: string;
expectedNonceValue: string;
};
[SOLANA_ERROR__TRANSACTION_INVOKED_PROGRAMS_CANNOT_PAY_FEES]: {
programAddress: string;
};
[SOLANA_ERROR__TRANSACTION_INVOKED_PROGRAMS_MUST_NOT_BE_WRITABLE]: {
programAddress: string;
};
[SOLANA_ERROR__TRANSACTION_VERSION_NUMBER_OUT_OF_RANGE]: {
actualVersion: number;
};
[SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING]: {
lookupTableAddresses: string[];
};
[SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_INDEX_OUT_OF_RANGE]: {
highestKnownIndex: number;
highestRequestedIndex: number;
lookupTableAddress: string;
};
[SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND]: {
index: number;
};
}>;
28 changes: 28 additions & 0 deletions packages/errors/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,17 @@ import {
SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND,
SOLANA_ERROR__NONCE_INVALID,
SOLANA_ERROR__RPC_INTEGER_OVERFLOW,
SOLANA_ERROR__TRANSACTION_EXPECTED_BLOCKHASH_LIFETIME,
SOLANA_ERROR__TRANSACTION_EXPECTED_NONCE_LIFETIME,
SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING,
SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_INDEX_OUT_OF_RANGE,
SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_FEE_PAYER_MISSING,
SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND,
SOLANA_ERROR__TRANSACTION_INVOKED_PROGRAMS_CANNOT_PAY_FEES,
SOLANA_ERROR__TRANSACTION_INVOKED_PROGRAMS_MUST_NOT_BE_WRITABLE,
SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES,
SOLANA_ERROR__TRANSACTION_SIGNATURE_NOT_COMPUTABLE,
SOLANA_ERROR__TRANSACTION_VERSION_NUMBER_OUT_OF_RANGE,
SolanaErrorCode,
} from './codes';

Expand All @@ -31,8 +40,27 @@ export const SolanaErrorMessages: Readonly<{
'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 ' +
'`Number.MAX_SAFE_INTEGER`.',
[SOLANA_ERROR__TRANSACTION_EXPECTED_BLOCKHASH_LIFETIME]: 'Transaction does not have a blockhash lifetime',
[SOLANA_ERROR__TRANSACTION_EXPECTED_NONCE_LIFETIME]: 'Transaction is not a durable nonce transaction',
[SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING]:
'Contents of these address lookup tables unknown: $lookupTableAddresses',
[SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_INDEX_OUT_OF_RANGE]:
'Lookup of address at index $highestRequestedIndex failed for lookup table ' +
'`$lookupTableAddress`. Highest known index is $highestKnownIndex. The lookup table ' +
'may have been extended since its contents were retrieved',
[SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_FEE_PAYER_MISSING]: 'No fee payer set in CompiledTransaction',
[SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND]:
'Could not find program address at index $index',
[SOLANA_ERROR__TRANSACTION_INVOKED_PROGRAMS_CANNOT_PAY_FEES]:
'This transaction includes an address (`$programAddress`) which is both ' +
'invoked and set as the fee payer. Program addresses may not pay fees',
[SOLANA_ERROR__TRANSACTION_INVOKED_PROGRAMS_MUST_NOT_BE_WRITABLE]:
'This transaction includes an address (`$programAddress`) which is both invoked and ' +
'marked writable. Program addresses may not be writable',
[SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES]: 'Transaction is missing signatures for addresses: $addresses.',
[SOLANA_ERROR__TRANSACTION_SIGNATURE_NOT_COMPUTABLE]:
"Could not determine this transaction's signature. Make sure that the transaction has " +
'been signed by its fee payer.',
[SOLANA_ERROR__TRANSACTION_VERSION_NUMBER_OUT_OF_RANGE]:
'Transaction version must be in the range [0, 127]. `$actualVersion` given',
};
40 changes: 36 additions & 4 deletions packages/transactions/src/__tests__/decompile-transaction-test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Address } from '@solana/addresses';
import {
SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING,
SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_INDEX_OUT_OF_RANGE,
SolanaError,
} from '@solana/errors';
import { AccountRole, IAccountLookupMeta, IAccountMeta, IInstruction } from '@solana/instructions';
import { SignatureBytes } from '@solana/keys';

Expand Down Expand Up @@ -1201,7 +1206,12 @@ describe('decompileTransaction', () => {

const fn = () => decompileTransaction(compiledTransaction);
expect(fn).toThrow(
'Addresses not provided for lookup tables: [9wnrQTq5MKhYfp379pKvpy1PvRyteseQmKv4Bw3uQrUw]',
new SolanaError(
SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING,
{
lookupTableAddresses: ['9wnrQTq5MKhYfp379pKvpy1PvRyteseQmKv4Bw3uQrUw'],
},
),
);
});

Expand Down Expand Up @@ -1241,7 +1251,14 @@ describe('decompileTransaction', () => {
const fn = () =>
decompileTransaction(compiledTransaction, { addressesByLookupTableAddress: lookupTables });
expect(fn).toThrow(
'Cannot look up index 1 in lookup table [9wnrQTq5MKhYfp379pKvpy1PvRyteseQmKv4Bw3uQrUw]. The lookup table may have been extended since the addresses provided were retrieved.',
new SolanaError(
SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_INDEX_OUT_OF_RANGE,
{
highestKnownIndex: 0,
highestRequestedIndex: 1,
lookupTableAddress: '9wnrQTq5MKhYfp379pKvpy1PvRyteseQmKv4Bw3uQrUw',
},
),
);
});

Expand Down Expand Up @@ -1281,7 +1298,14 @@ describe('decompileTransaction', () => {
const fn = () =>
decompileTransaction(compiledTransaction, { addressesByLookupTableAddress: lookupTables });
expect(fn).toThrow(
'Cannot look up index 1 in lookup table [9wnrQTq5MKhYfp379pKvpy1PvRyteseQmKv4Bw3uQrUw]. The lookup table may have been extended since the addresses provided were retrieved.',
new SolanaError(
SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_INDEX_OUT_OF_RANGE,
{
highestKnownIndex: 0,
highestRequestedIndex: 1,
lookupTableAddress: '9wnrQTq5MKhYfp379pKvpy1PvRyteseQmKv4Bw3uQrUw',
},
),
);
});
});
Expand Down Expand Up @@ -1657,7 +1681,15 @@ describe('decompileTransaction', () => {

const fn = () => decompileTransaction(compiledTransaction);
expect(fn).toThrow(
'Addresses not provided for lookup tables: [9wnrQTq5MKhYfp379pKvpy1PvRyteseQmKv4Bw3uQrUw, GS7Rphk6CZLoCGbTcbRaPZzD3k4ZK8XiA5BAj89Fi2Eg]',
new SolanaError(
SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING,
{
lookupTableAddresses: [
'9wnrQTq5MKhYfp379pKvpy1PvRyteseQmKv4Bw3uQrUw',
'GS7Rphk6CZLoCGbTcbRaPZzD3k4ZK8XiA5BAj89Fi2Eg',
],
},
),
);
});
});
Expand Down
34 changes: 16 additions & 18 deletions packages/transactions/src/accounts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Address, getAddressComparator } from '@solana/addresses';
import {
SOLANA_ERROR__TRANSACTION_INVOKED_PROGRAMS_CANNOT_PAY_FEES,
SOLANA_ERROR__TRANSACTION_INVOKED_PROGRAMS_MUST_NOT_BE_WRITABLE,
SolanaError,
} from '@solana/errors';
import {
AccountRole,
IAccountLookupMeta,
Expand Down Expand Up @@ -61,19 +66,13 @@ export function getAddressMapFromInstructions(feePayer: Address, instructions: r
if (isWritableRole(entry.role)) {
switch (entry[TYPE]) {
case AddressMapEntryType.FEE_PAYER:
// TODO: Coded error.
throw new Error(
'This transaction includes an address ' +
`(\`${instruction.programAddress}\`) which is both invoked ` +
'and set as the fee payer. Program addresses may not pay fees.',
);
throw new SolanaError(SOLANA_ERROR__TRANSACTION_INVOKED_PROGRAMS_CANNOT_PAY_FEES, {
programAddress: instruction.programAddress,
});
default:
// TODO: Coded error.
throw new Error(
'This transaction includes an address ' +
`(\`${instruction.programAddress}\`) which is both invoked ` +
'and marked writable. Program addresses may not be writable.',
);
throw new SolanaError(SOLANA_ERROR__TRANSACTION_INVOKED_PROGRAMS_MUST_NOT_BE_WRITABLE, {
programAddress: instruction.programAddress,
});
}
}
if (entry[TYPE] === AddressMapEntryType.STATIC) {
Expand Down Expand Up @@ -141,12 +140,11 @@ export function getAddressMapFromInstructions(feePayer: Address, instructions: r
addressesOfInvokedPrograms.has(account.address)
) {
if (isWritableRole(accountMeta.role)) {
// TODO: Coded error.
throw new Error(
'This transaction includes an address ' +
`(\`${account.address}\`) which is both invoked and ` +
'marked writable. Program addresses may not be ' +
'writable.',
throw new SolanaError(
SOLANA_ERROR__TRANSACTION_INVOKED_PROGRAMS_MUST_NOT_BE_WRITABLE,
{
programAddress: account.address,
},
);
}
if (entry.role !== nextRole) {
Expand Down
4 changes: 2 additions & 2 deletions packages/transactions/src/blockhash.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SOLANA_ERROR__TRANSACTION_EXPECTED_BLOCKHASH_LIFETIME, SolanaError } from '@solana/errors';
import { assertIsBlockhash, type Blockhash } from '@solana/rpc-types';

import { IDurableNonceTransaction } from './durable-nonce';
Expand Down Expand Up @@ -34,8 +35,7 @@ export function assertIsTransactionWithBlockhashLifetime(
transaction: BaseTransaction | (BaseTransaction & ITransactionWithBlockhashLifetime),
): asserts transaction is BaseTransaction & ITransactionWithBlockhashLifetime {
if (!isTransactionWithBlockhashLifetime(transaction)) {
// TODO: Coded error.
throw new Error('Transaction does not have a blockhash lifetime');
throw new SolanaError(SOLANA_ERROR__TRANSACTION_EXPECTED_BLOCKHASH_LIFETIME);
}
}

Expand Down
33 changes: 23 additions & 10 deletions packages/transactions/src/decompile-transaction.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Address, assertIsAddress } from '@solana/addresses';
import {
SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING,
SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_INDEX_OUT_OF_RANGE,
SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_FEE_PAYER_MISSING,
SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND,
SolanaError,
} from '@solana/errors';
import { pipe } from '@solana/functional';
import { AccountRole, IAccountLookupMeta, IAccountMeta, IInstruction } from '@solana/instructions';
import { SignatureBytes } from '@solana/keys';
Expand Down Expand Up @@ -70,9 +77,9 @@ function getAddressLookupMetas(
const compiledAddressTableLookupAddresses = compiledAddressTableLookups.map(l => l.lookupTableAddress);
const missing = compiledAddressTableLookupAddresses.filter(a => addressesByLookupTableAddress[a] === undefined);
if (missing.length > 0) {
const missingAddresses = missing.join(', ');
// TODO: coded error.
throw new Error(`Addresses not provided for lookup tables: [${missingAddresses}]`);
throw new SolanaError(SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING, {
lookupTableAddresses: missing,
});
}

const readOnlyMetas: IAccountLookupMeta[] = [];
Expand All @@ -84,9 +91,13 @@ function getAddressLookupMetas(

const highestIndex = Math.max(...lookup.readableIndices, ...lookup.writableIndices);
if (highestIndex >= addresses.length) {
// TODO coded error
throw new Error(
`Cannot look up index ${highestIndex} in lookup table [${lookup.lookupTableAddress}]. The lookup table may have been extended since the addresses provided were retrieved.`,
throw new SolanaError(
SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_INDEX_OUT_OF_RANGE,
{
highestKnownIndex: addresses.length - 1,
highestRequestedIndex: highestIndex,
lookupTableAddress: lookup.lookupTableAddress,
},
);
}

Expand Down Expand Up @@ -116,8 +127,9 @@ function convertInstruction(
): IInstruction {
const programAddress = accountMetas[instruction.programAddressIndex]?.address;
if (!programAddress) {
// TODO coded error
throw new Error(`Could not find program address at index ${instruction.programAddressIndex}`);
throw new SolanaError(SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND, {
index: instruction.programAddressIndex,
});
}

const accounts = instruction.accountIndices?.map(accountIndex => accountMetas[accountIndex]);
Expand Down Expand Up @@ -196,8 +208,9 @@ export function decompileTransaction(
const { compiledMessage } = compiledTransaction;

const feePayer = compiledMessage.staticAccounts[0];
// TODO: coded error
if (!feePayer) throw new Error('No fee payer set in CompiledTransaction');
if (!feePayer) {
throw new SolanaError(SOLANA_ERROR__TRANSACTION_FAILED_TO_DECOMPILE_FEE_PAYER_MISSING);
}

const accountMetas = getAccountMetas(compiledMessage);
const accountLookupMetas =
Expand Down
4 changes: 2 additions & 2 deletions packages/transactions/src/durable-nonce.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Address } from '@solana/addresses';
import { SOLANA_ERROR__TRANSACTION_EXPECTED_NONCE_LIFETIME, SolanaError } from '@solana/errors';
import {
AccountRole,
IInstruction,
Expand Down Expand Up @@ -65,8 +66,7 @@ export function assertIsDurableNonceTransaction(
transaction: BaseTransaction | (BaseTransaction & IDurableNonceTransaction),
): asserts transaction is BaseTransaction & IDurableNonceTransaction {
if (!isDurableNonceTransaction(transaction)) {
// TODO: Coded error.
throw new Error('Transaction is not a durable nonce transaction');
throw new SolanaError(SOLANA_ERROR__TRANSACTION_EXPECTED_NONCE_LIFETIME);
}
}

Expand Down
6 changes: 4 additions & 2 deletions packages/transactions/src/serializers/transaction-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
VariableSizeDecoder,
VariableSizeEncoder,
} from '@solana/codecs-core';
import { SOLANA_ERROR__TRANSACTION_VERSION_NUMBER_OUT_OF_RANGE, SolanaError } from '@solana/errors';

import { TransactionVersion } from '../types';

Expand All @@ -20,8 +21,9 @@ export function getTransactionVersionEncoder(): VariableSizeEncoder<TransactionV
return offset;
}
if (value < 0 || value > 127) {
// TODO: Coded error.
throw new Error(`Transaction version must be in the range [0, 127]. \`${value}\` given.`);
throw new SolanaError(SOLANA_ERROR__TRANSACTION_VERSION_NUMBER_OUT_OF_RANGE, {
actualVersion: value,
});
}
bytes.set([value | VERSION_FLAG_MASK], offset);
return offset + 1;
Expand Down

0 comments on commit 49bb70a

Please sign in to comment.