Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions clients/js/src/generated/errors/index.ts
Original file line number Diff line number Diff line change
@@ -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';
61 changes: 61 additions & 0 deletions clients/js/src/generated/errors/programMetadata.ts
Original file line number Diff line number Diff line change
@@ -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<ProgramMetadataError, string> | 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<ProgramMetadataError, string>)[code];
}

return 'Error message not available in production bundles.';
}

export function isProgramMetadataError<TProgramErrorCode extends ProgramMetadataError>(
error: unknown,
transactionMessage: { instructions: Record<number, { programAddress: Address }> },
code?: TProgramErrorCode,
): error is SolanaError<typeof SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM> &
Readonly<{ context: Readonly<{ code: TProgramErrorCode }> }> {
return isProgramError<TProgramErrorCode>(error, transactionMessage, PROGRAM_METADATA_PROGRAM_ADDRESS, code);
}
1 change: 1 addition & 0 deletions clients/js/src/generated/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

export * from './accounts';
export * from './errors';
export * from './instructions';
export * from './pdas';
export * from './programs';
Expand Down
21 changes: 10 additions & 11 deletions clients/js/test/setData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -20,7 +19,9 @@ import {
getSetAuthorityInstruction,
getSetDataInstruction,
getSetImmutableInstruction,
isProgramMetadataError,
Metadata,
PROGRAM_METADATA_ERROR__IMMUTABLE_METADATA_ACCOUNT,
} from '../src';
import {
createCanonicalMetadata,
Expand Down Expand Up @@ -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));
Copy link
Copy Markdown
Contributor Author

@febo febo May 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lorisleiva Not sure if this is the best way to assert a custom error.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is perfect!

});

test('an immutable non-canonical metadata account cannot be updated', async t => {
Expand Down Expand Up @@ -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 => {
Expand Down
4 changes: 4 additions & 0 deletions clients/rust/src/generated/errors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@
//!
//! <https://github.com/codama-idl/codama>
//!

pub(crate) mod program_metadata;

pub use self::program_metadata::ProgramMetadataError;
34 changes: 34 additions & 0 deletions clients/rust/src/generated/errors/program_metadata.rs
Original file line number Diff line number Diff line change
@@ -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.
//!
//! <https://github.com/codama-idl/codama>
//!

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<ProgramMetadataError> for solana_program_error::ProgramError {
fn from(e: ProgramMetadataError) -> Self {
solana_program_error::ProgramError::Custom(e as u32)
}
}
38 changes: 37 additions & 1 deletion program/idl.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": []
}
12 changes: 12 additions & 0 deletions program/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProgramMetadataError> for ProgramError {
Expand Down
17 changes: 8 additions & 9 deletions program/src/processor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -101,8 +102,7 @@ fn is_program_authority(
}
}
_ => {
// TODO: use custom error (invalid program state)
return Err(ProgramError::InvalidAccountData);
return Err(ProgramMetadataError::InvalidProgramState.into());
}
}
};
Expand All @@ -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)
}
Expand Down
5 changes: 2 additions & 3 deletions program/src/processor/set_immutable.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -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(())
Expand Down
7 changes: 3 additions & 4 deletions program/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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()),
}
}
}
Expand Down
Loading