From af8957e584bd1f647b1bb73f0c206c8176bea6f9 Mon Sep 17 00:00:00 2001 From: febo Date: Sat, 9 May 2026 15:16:12 +0100 Subject: [PATCH 1/3] Add custom errors --- program/src/error.rs | 12 ++++++++++++ program/src/processor/mod.rs | 17 ++++++++--------- program/src/processor/set_immutable.rs | 5 ++--- program/src/state/mod.rs | 7 +++---- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/program/src/error.rs b/program/src/error.rs index 363daef..96b08b1 100644 --- a/program/src/error.rs +++ b/program/src/error.rs @@ -4,6 +4,18 @@ use pinocchio::program_error::ProgramError; pub enum ProgramMetadataError { /// 0 - The program account is not executable. NotExecutableAccount, + + /// 1 - The program state is invalid. + InvalidProgramState, + + /// 2 - The program data account is invalid. + InvalidProgramDataAccount, + + /// 3 - The metadata account is immutable. + ImmutableMetadataAccount, + + /// 4 - The account data length is invalid. + InvalidDataLength, } impl From for ProgramError { diff --git a/program/src/processor/mod.rs b/program/src/processor/mod.rs index 24b59cd..3a983da 100644 --- a/program/src/processor/mod.rs +++ b/program/src/processor/mod.rs @@ -4,7 +4,10 @@ use pinocchio::{ pubkey::{Pubkey, PUBKEY_BYTES}, }; -use crate::state::{header::Header, Account, AccountDiscriminator}; +use crate::{ + error::ProgramMetadataError, + state::{header::Header, Account, AccountDiscriminator}, +}; pub mod allocate; pub mod close; @@ -71,16 +74,14 @@ fn is_program_authority( .map_err(|_| ProgramError::InvalidAccountData)? } _ => { - // TODO: use custom error (invalid program state) - return Err(ProgramError::InvalidAccountData); + return Err(ProgramMetadataError::InvalidProgramState.into()); } } }; // Program <-> Program Data check. if expected_program_data != *program_data.key() { - // TODO: use custom error (invalid program data account) - return Err(ProgramError::InvalidAccountData); + return Err(ProgramMetadataError::InvalidProgramDataAccount.into()); } // Program Data checks. @@ -101,8 +102,7 @@ fn is_program_authority( } } _ => { - // TODO: use custom error (invalid program state) - return Err(ProgramError::InvalidAccountData); + return Err(ProgramMetadataError::InvalidProgramState.into()); } } }; @@ -124,8 +124,7 @@ fn validate_metadata(metadata: &AccountInfo) -> Result<&Header, ProgramError> { return Err(ProgramError::UninitializedAccount); } if !header.mutable() { - // TODO: use custom error (immutable metadata account) - return Err(ProgramError::InvalidAccountData); + return Err(ProgramMetadataError::ImmutableMetadataAccount.into()); } Ok(header) } diff --git a/program/src/processor/set_immutable.rs b/program/src/processor/set_immutable.rs index 42e8523..35b639c 100644 --- a/program/src/processor/set_immutable.rs +++ b/program/src/processor/set_immutable.rs @@ -1,6 +1,6 @@ use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; -use crate::state::header::Header; +use crate::{error::ProgramMetadataError, state::header::Header}; use super::{validate_authority, validate_metadata}; @@ -40,8 +40,7 @@ pub fn set_immutable(accounts: &[AccountInfo]) -> ProgramResult { if header.mutable() { header.mutable = 0; } else { - // TODO: use custom error (metadata already immutable) - return Err(ProgramError::InvalidAccountData); + return Err(ProgramMetadataError::ImmutableMetadataAccount.into()); } Ok(()) diff --git a/program/src/state/mod.rs b/program/src/state/mod.rs index 559b27b..55851d5 100644 --- a/program/src/state/mod.rs +++ b/program/src/state/mod.rs @@ -11,6 +11,8 @@ use pinocchio::{ use data::{Data, ExternalData}; use header::Header; +use crate::error::ProgramMetadataError; + /// The length of the seed used to derive the metadata account address. pub const SEED_LEN: usize = 16; @@ -176,10 +178,7 @@ impl DataSource { match (self, length) { (DataSource::Direct | DataSource::Url, l) if l > 0 => Ok(()), (DataSource::External, ExternalData::LEN) => Ok(()), - _ => { - // TODO: use custom error (invalid data length) - Err(ProgramError::InvalidAccountData) - } + _ => Err(ProgramMetadataError::InvalidDataLength.into()), } } } From 4840fd681c2a049eaa3a25073008931c32054ac2 Mon Sep 17 00:00:00 2001 From: febo Date: Mon, 11 May 2026 13:13:13 +0100 Subject: [PATCH 2/3] Update client generation --- clients/js/src/generated/errors/index.ts | 9 +++ .../src/generated/errors/programMetadata.ts | 61 +++++++++++++++++++ clients/js/src/generated/index.ts | 1 + clients/rust/src/generated/errors/mod.rs | 4 ++ .../src/generated/errors/program_metadata.rs | 34 +++++++++++ program/idl.json | 38 +++++++++++- 6 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 clients/js/src/generated/errors/index.ts create mode 100644 clients/js/src/generated/errors/programMetadata.ts create mode 100644 clients/rust/src/generated/errors/program_metadata.rs diff --git a/clients/js/src/generated/errors/index.ts b/clients/js/src/generated/errors/index.ts new file mode 100644 index 0000000..37bb6ba --- /dev/null +++ b/clients/js/src/generated/errors/index.ts @@ -0,0 +1,9 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +export * from './programMetadata'; diff --git a/clients/js/src/generated/errors/programMetadata.ts b/clients/js/src/generated/errors/programMetadata.ts new file mode 100644 index 0000000..8a02b1b --- /dev/null +++ b/clients/js/src/generated/errors/programMetadata.ts @@ -0,0 +1,61 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + isProgramError, + type Address, + type SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM, + type SolanaError, +} from '@solana/kit'; +import { PROGRAM_METADATA_PROGRAM_ADDRESS } from '../programs'; + +/** NotExecutableAccount: The program account is not executable */ +export const PROGRAM_METADATA_ERROR__NOT_EXECUTABLE_ACCOUNT = 0x0; // 0 +/** InvalidProgramState: The program state is invalid */ +export const PROGRAM_METADATA_ERROR__INVALID_PROGRAM_STATE = 0x1; // 1 +/** InvalidProgramDataAccount: The program data account is invalid */ +export const PROGRAM_METADATA_ERROR__INVALID_PROGRAM_DATA_ACCOUNT = 0x2; // 2 +/** ImmutableMetadataAccount: The metadata account is immutable */ +export const PROGRAM_METADATA_ERROR__IMMUTABLE_METADATA_ACCOUNT = 0x3; // 3 +/** InvalidDataLength: The account data length is invalid */ +export const PROGRAM_METADATA_ERROR__INVALID_DATA_LENGTH = 0x4; // 4 + +export type ProgramMetadataError = + | typeof PROGRAM_METADATA_ERROR__IMMUTABLE_METADATA_ACCOUNT + | typeof PROGRAM_METADATA_ERROR__INVALID_DATA_LENGTH + | typeof PROGRAM_METADATA_ERROR__INVALID_PROGRAM_DATA_ACCOUNT + | typeof PROGRAM_METADATA_ERROR__INVALID_PROGRAM_STATE + | typeof PROGRAM_METADATA_ERROR__NOT_EXECUTABLE_ACCOUNT; + +let programMetadataErrorMessages: Record | undefined; +if (process.env.NODE_ENV !== 'production') { + programMetadataErrorMessages = { + [PROGRAM_METADATA_ERROR__IMMUTABLE_METADATA_ACCOUNT]: `The metadata account is immutable`, + [PROGRAM_METADATA_ERROR__INVALID_DATA_LENGTH]: `The account data length is invalid`, + [PROGRAM_METADATA_ERROR__INVALID_PROGRAM_DATA_ACCOUNT]: `The program data account is invalid`, + [PROGRAM_METADATA_ERROR__INVALID_PROGRAM_STATE]: `The program state is invalid`, + [PROGRAM_METADATA_ERROR__NOT_EXECUTABLE_ACCOUNT]: `The program account is not executable`, + }; +} + +export function getProgramMetadataErrorMessage(code: ProgramMetadataError): string { + if (process.env.NODE_ENV !== 'production') { + return (programMetadataErrorMessages as Record)[code]; + } + + return 'Error message not available in production bundles.'; +} + +export function isProgramMetadataError( + error: unknown, + transactionMessage: { instructions: Record }, + code?: TProgramErrorCode, +): error is SolanaError & + Readonly<{ context: Readonly<{ code: TProgramErrorCode }> }> { + return isProgramError(error, transactionMessage, PROGRAM_METADATA_PROGRAM_ADDRESS, code); +} diff --git a/clients/js/src/generated/index.ts b/clients/js/src/generated/index.ts index 923de15..157d6f1 100644 --- a/clients/js/src/generated/index.ts +++ b/clients/js/src/generated/index.ts @@ -7,6 +7,7 @@ */ export * from './accounts'; +export * from './errors'; export * from './instructions'; export * from './pdas'; export * from './programs'; diff --git a/clients/rust/src/generated/errors/mod.rs b/clients/rust/src/generated/errors/mod.rs index 6172ba6..1dc3c94 100644 --- a/clients/rust/src/generated/errors/mod.rs +++ b/clients/rust/src/generated/errors/mod.rs @@ -4,3 +4,7 @@ //! //! //! + +pub(crate) mod program_metadata; + +pub use self::program_metadata::ProgramMetadataError; diff --git a/clients/rust/src/generated/errors/program_metadata.rs b/clients/rust/src/generated/errors/program_metadata.rs new file mode 100644 index 0000000..95ab724 --- /dev/null +++ b/clients/rust/src/generated/errors/program_metadata.rs @@ -0,0 +1,34 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +use num_derive::FromPrimitive; +use thiserror::Error; + +#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] +pub enum ProgramMetadataError { + /// 0 - The program account is not executable + #[error("The program account is not executable")] + NotExecutableAccount = 0x0, + /// 1 - The program state is invalid + #[error("The program state is invalid")] + InvalidProgramState = 0x1, + /// 2 - The program data account is invalid + #[error("The program data account is invalid")] + InvalidProgramDataAccount = 0x2, + /// 3 - The metadata account is immutable + #[error("The metadata account is immutable")] + ImmutableMetadataAccount = 0x3, + /// 4 - The account data length is invalid + #[error("The account data length is invalid")] + InvalidDataLength = 0x4, +} + +impl From for solana_program_error::ProgramError { + fn from(e: ProgramMetadataError) -> Self { + solana_program_error::ProgramError::Custom(e as u32) + } +} diff --git a/program/idl.json b/program/idl.json index 31a938e..de3fe5d 100644 --- a/program/idl.json +++ b/program/idl.json @@ -1249,7 +1249,43 @@ } } ], - "errors": [] + "errors": [ + { + "kind": "errorNode", + "name": "notExecutableAccount", + "code": 0, + "message": "The program account is not executable", + "docs": ["NotExecutableAccount: The program account is not executable"] + }, + { + "kind": "errorNode", + "name": "invalidProgramState", + "code": 1, + "message": "The program state is invalid", + "docs": ["InvalidProgramState: The program state is invalid"] + }, + { + "kind": "errorNode", + "name": "invalidProgramDataAccount", + "code": 2, + "message": "The program data account is invalid", + "docs": ["InvalidProgramDataAccount: The program data account is invalid"] + }, + { + "kind": "errorNode", + "name": "immutableMetadataAccount", + "code": 3, + "message": "The metadata account is immutable", + "docs": ["ImmutableMetadataAccount: The metadata account is immutable"] + }, + { + "kind": "errorNode", + "name": "invalidDataLength", + "code": 4, + "message": "The account data length is invalid", + "docs": ["InvalidDataLength: The account data length is invalid"] + } + ] }, "additionalPrograms": [] } From fd44144d149011c03f0c6ad08eb0e4e96dba79bb Mon Sep 17 00:00:00 2001 From: febo Date: Mon, 11 May 2026 13:53:18 +0100 Subject: [PATCH 3/3] Update error assert --- clients/js/test/setData.test.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/clients/js/test/setData.test.ts b/clients/js/test/setData.test.ts index b5b6b55..2e09103 100644 --- a/clients/js/test/setData.test.ts +++ b/clients/js/test/setData.test.ts @@ -6,7 +6,6 @@ import { getUtf8Encoder, isSolanaError, pipe, - SOLANA_ERROR__INSTRUCTION_ERROR__INVALID_ACCOUNT_DATA, SOLANA_ERROR__INSTRUCTION_ERROR__INVALID_REALLOC, } from '@solana/kit'; import test from 'ava'; @@ -20,7 +19,9 @@ import { getSetAuthorityInstruction, getSetDataInstruction, getSetImmutableInstruction, + isProgramMetadataError, Metadata, + PROGRAM_METADATA_ERROR__IMMUTABLE_METADATA_ACCOUNT, } from '../src'; import { createCanonicalMetadata, @@ -450,15 +451,14 @@ test('an immutable canonical metadata account cannot be updated', async t => { dataSource: DataSource.Direct, data: getUtf8Encoder().encode('New data'), }); - const promise = pipe( - await createDefaultTransaction(client, authority), - tx => appendTransactionMessageInstructions([setImmutableIx, setDataIx], tx), - tx => signAndSendTransaction(client, tx), + const transactionMessage = pipe(await createDefaultTransaction(client, authority), tx => + appendTransactionMessageInstructions([setImmutableIx, setDataIx], tx), ); + const promise = signAndSendTransaction(client, transactionMessage); // Then we expect the transaction to fail. const error = await t.throwsAsync(promise); - t.true(isSolanaError(error.cause, SOLANA_ERROR__INSTRUCTION_ERROR__INVALID_ACCOUNT_DATA)); + t.true(isProgramMetadataError(error.cause, transactionMessage, PROGRAM_METADATA_ERROR__IMMUTABLE_METADATA_ACCOUNT)); }); test('an immutable non-canonical metadata account cannot be updated', async t => { @@ -493,15 +493,14 @@ test('an immutable non-canonical metadata account cannot be updated', async t => dataSource: DataSource.Direct, data: getUtf8Encoder().encode('New data'), }); - const promise = pipe( - await createDefaultTransaction(client, authority), - tx => appendTransactionMessageInstructions([setImmutableIx, setDataIx], tx), - tx => signAndSendTransaction(client, tx), + const transactionMessage = pipe(await createDefaultTransaction(client, authority), tx => + appendTransactionMessageInstructions([setImmutableIx, setDataIx], tx), ); + const promise = signAndSendTransaction(client, transactionMessage); // Then we expect the transaction to fail. const error = await t.throwsAsync(promise); - t.true(isSolanaError(error.cause, SOLANA_ERROR__INSTRUCTION_ERROR__INVALID_ACCOUNT_DATA)); + t.true(isProgramMetadataError(error.cause, transactionMessage, PROGRAM_METADATA_ERROR__IMMUTABLE_METADATA_ACCOUNT)); }); test('The metadata account needs to be extended for data changes that add more than 1KB', async t => {