v0.4.8-pre.003
This commit is contained in:
@@ -1,8 +1,16 @@
|
||||
<!-- file: kb-lib/CHANGELOG.md -->
|
||||
<!-- version: 16 -->
|
||||
<!-- version: 17 -->
|
||||
|
||||
# CHANGELOG — kb-lib
|
||||
|
||||
## 0.4.8-pre.003 — modèles Solana Program Metadata
|
||||
|
||||
- ajoute les constantes publiques des layouts fixes `Buffer`, `Metadata`, seed et référence externe ;
|
||||
- ajoute les enums wire fermées, les cinq erreurs custom officielles et les conversions fail-closed ;
|
||||
- ajoute les modèles bornés du seed, des données directes, URL et externes sans résolution off-chain ;
|
||||
- ajoute les sérialisations wire sûres, les représentations JSON stables et les tests de façade publique ;
|
||||
- conserve le décodeur Solana Program Metadata au statut réservé jusqu’aux phases comptes et instructions.
|
||||
|
||||
## 0.4.7-pre.016 — clôture documentaire Metaplex
|
||||
|
||||
- généralise la documentation d’usage de la surface Metaplex ;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-lib/README.md -->
|
||||
<!-- version: 19 -->
|
||||
<!-- version: 20 -->
|
||||
|
||||
# kb-lib
|
||||
|
||||
@@ -7,7 +7,15 @@
|
||||
|
||||
## Périmètre
|
||||
|
||||
La crate couvre actuellement Solana Core, SPL Memo, SPL Token classique, SPL Associated Token Account, Token-2022, le registre SPL ElGamal et Metaplex Token Metadata.
|
||||
La crate couvre actuellement Solana Core, SPL Memo, SPL Token classique, SPL Associated Token Account, Token-2022, le registre SPL ElGamal et Metaplex Token Metadata. Elle expose également les modèles wire bornés de Solana Program Metadata ; son décodeur reste réservé tant que les comptes et instructions ne sont pas implémentés.
|
||||
|
||||
Pour Solana Program Metadata, la surface publique fondatrice comprend :
|
||||
|
||||
- les tailles fixes officielles des headers, seeds et références externes ;
|
||||
- les enums fermées de discriminateur, encodage, compression, format et source ;
|
||||
- le seed brut de 16 octets avec helpers UTF-8 bornés ;
|
||||
- les données directes, URL et références vers un autre compte sans résolution off-chain ;
|
||||
- les cinq erreurs custom officielles et les erreurs de conversion fail-closed.
|
||||
|
||||
Pour Metaplex Token Metadata, elle fournit :
|
||||
|
||||
@@ -40,6 +48,7 @@ Pour Metaplex Token Metadata, elle fournit :
|
||||
- contrats `MtApi*` et matérialisateurs concrets ;
|
||||
- contrats `ExApi*`, politiques de sécurité et exécuteurs `Ex*Executor` ;
|
||||
- `ExMetadataMetaplexTokenMetadataExecutor`, `ExMetaplexTokenMetadataExecutionIntent` et `ExMetaplexTokenMetadataOperation` ;
|
||||
- modèles `DcMetadataSpm*` et constantes `DC_METADATA_SPM_*` pour le contrat Solana Program Metadata ;
|
||||
- modèles canoniques `Md*` réexportés par la façade.
|
||||
|
||||
Les exemples d’appel et invariants sont documentés dans [USAGE.md](USAGE.md).
|
||||
@@ -54,6 +63,8 @@ Les exemples d’appel et invariants sont documentés dans [USAGE.md](USAGE.md).
|
||||
|
||||
La surface fonctionnelle Metaplex de la crate est achevée. Les campagnes Devnet restantes relèvent de `kb-pipeline-demo-scenarios` et de `kb-app-demo-desktop`, non d’un manque de builder dans `kb-lib`.
|
||||
|
||||
Pour Solana Program Metadata, seuls les modèles wire sont actuellement actifs. Le décodeur de protocole reste volontairement réservé jusqu’à l’implémentation bornée des comptes et instructions.
|
||||
|
||||
Le registre ElGamal n’est pas déclaré validé réellement sur réseau.
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-lib/USAGE.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# Utilisation de kb-lib
|
||||
|
||||
@@ -82,6 +82,34 @@ println!("parsed registry state: {registry:#?}");
|
||||
|
||||
Les parseurs vérifient les bornes et formats qu’ils annoncent. Ils ne reconstruisent pas un état historique depuis un RPC courant.
|
||||
|
||||
## Modèles Solana Program Metadata
|
||||
|
||||
Les modèles `DcMetadataSpm*` représentent uniquement le contrat on-chain du programme `ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S`. Ils ne téléchargent jamais les URL et ne lisent pas automatiquement les comptes référencés par `External`.
|
||||
|
||||
```rust
|
||||
let seed_result = kb_lib::DcMetadataSpmSeed::from_utf8("idl");
|
||||
let seed = match seed_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let external = kb_lib::DcMetadataSpmExternalData::from_zeroable_length(
|
||||
solana_pubkey::Pubkey::new_from_array([7_u8; 32]),
|
||||
128,
|
||||
4096,
|
||||
);
|
||||
let data = kb_lib::DcMetadataSpmData::External(external);
|
||||
let wire_bytes = match data.to_wire_bytes() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
assert_eq!(seed.as_bytes().len(), kb_lib::DC_METADATA_SPM_SEED_BYTES);
|
||||
assert_eq!(wire_bytes.len(), kb_lib::DC_METADATA_SPM_EXTERNAL_DATA_BYTES);
|
||||
```
|
||||
|
||||
Le champ `length` externe suit le contrat zeroable : la valeur wire `0` devient `None` et signifie que la référence porte sur tout le compte à partir de `offset`. Les contenus `Direct` et `Url` doivent être non vides, et toute longueur doit rester représentable par le `u32` du header.
|
||||
|
||||
Le décodeur de protocole `DcMetadataSolanaProgramMetadataDecoder` est encore réservé. Les modèles servent déjà de contrat public stable aux futurs décodeurs de comptes, décodeurs d’instructions, matérialisateurs et builders.
|
||||
|
||||
## Comptes Metaplex Token Metadata
|
||||
|
||||
Les fonctions `decoder_metadata_metaplex_token_metadata_decode_*_account` décodent les familles de comptes publiques prises en charge. Le consommateur doit fournir les données et le contexte attendus par la signature exacte de la fonction concernée. Les parseurs conservent les variantes historiques et refusent les longueurs, propriétaires ou suffixes incompatibles.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/decoder.rs
|
||||
// version: 12
|
||||
// version: 13
|
||||
|
||||
//! Consolidated decoder modules.
|
||||
|
||||
@@ -332,6 +332,14 @@ pub(crate) use self::metadata::DC_METADATA_MTM_VERIFY_COLLECTION_DISCRIMINATOR;
|
||||
pub(crate) use self::metadata::DC_METADATA_MTM_VERIFY_DISCRIMINATOR;
|
||||
/// Verify Sized Collection Item discriminator.
|
||||
pub(crate) use self::metadata::DC_METADATA_MTM_VERIFY_SIZED_COLLECTION_ITEM_DISCRIMINATOR;
|
||||
/// Exact fixed byte length of one Buffer account header.
|
||||
pub use self::metadata::DC_METADATA_SPM_BUFFER_HEADER_BYTES;
|
||||
/// Exact byte length of one external-account data reference.
|
||||
pub use self::metadata::DC_METADATA_SPM_EXTERNAL_DATA_BYTES;
|
||||
/// Exact fixed byte length of one Metadata account header.
|
||||
pub use self::metadata::DC_METADATA_SPM_METADATA_HEADER_BYTES;
|
||||
/// Exact byte length of one Solana Program Metadata seed.
|
||||
pub use self::metadata::DC_METADATA_SPM_SEED_BYTES;
|
||||
/// Exact decoder for the Metaplex Token Metadata program.
|
||||
pub use self::metadata::DcMetadataMetaplexTokenMetadataDecoder;
|
||||
/// Canonical projection shared by Collection and Use Authority records.
|
||||
@@ -384,6 +392,26 @@ pub use self::metadata::DcMetadataMtmTokenRecordAccountSnapshot;
|
||||
pub use self::metadata::DcMetadataSolanaProgramMetadataDecoder;
|
||||
/// Reserved `kb-lib.decoder.metadata.spl_name_service` decoder.
|
||||
pub use self::metadata::DcMetadataSplNameServiceDecoder;
|
||||
/// Solana Program Metadata account discriminator.
|
||||
pub use self::metadata::DcMetadataSpmAccountDiscriminator;
|
||||
/// Compression declared for Solana Program Metadata content.
|
||||
pub use self::metadata::DcMetadataSpmCompression;
|
||||
/// Typed Solana Program Metadata content without off-chain resolution.
|
||||
pub use self::metadata::DcMetadataSpmData;
|
||||
/// On-chain source used for Solana Program Metadata content.
|
||||
pub use self::metadata::DcMetadataSpmDataSource;
|
||||
/// Encoding declared for Solana Program Metadata content.
|
||||
pub use self::metadata::DcMetadataSpmEncoding;
|
||||
/// Fixed external-account data reference.
|
||||
pub use self::metadata::DcMetadataSpmExternalData;
|
||||
/// Format declared for Solana Program Metadata content.
|
||||
pub use self::metadata::DcMetadataSpmFormat;
|
||||
/// Bounded Solana Program Metadata model conversion error.
|
||||
pub use self::metadata::DcMetadataSpmModelError;
|
||||
/// Official Solana Program Metadata custom error.
|
||||
pub use self::metadata::DcMetadataSpmProgramError;
|
||||
/// Exact fixed-size Solana Program Metadata seed.
|
||||
pub use self::metadata::DcMetadataSpmSeed;
|
||||
/// Canonical tracing target for this crate.
|
||||
pub(crate) use self::metadata::TRACING_TARGET_DECODER_METADATA_METAPLEX_TOKEN_METADATA;
|
||||
/// Crate-root access to `decode` from `instruction`.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/decoder/metadata.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
//! `metadata` decoder family.
|
||||
|
||||
@@ -77,8 +77,36 @@ pub use self::metaplex_token_metadata::decoder_metadata_metaplex_token_metadata_
|
||||
pub use self::metaplex_token_metadata::decoder_metadata_metaplex_token_metadata_decode_token_record_account;
|
||||
/// Decodes one Use Authority Record account.
|
||||
pub use self::metaplex_token_metadata::decoder_metadata_metaplex_token_metadata_decode_use_authority_record_account;
|
||||
/// Exact fixed byte length of one Buffer account header.
|
||||
pub use self::solana_program_metadata::DC_METADATA_SPM_BUFFER_HEADER_BYTES;
|
||||
/// Exact byte length of one external-account data reference.
|
||||
pub use self::solana_program_metadata::DC_METADATA_SPM_EXTERNAL_DATA_BYTES;
|
||||
/// Exact fixed byte length of one Metadata account header.
|
||||
pub use self::solana_program_metadata::DC_METADATA_SPM_METADATA_HEADER_BYTES;
|
||||
/// Exact byte length of one Solana Program Metadata seed.
|
||||
pub use self::solana_program_metadata::DC_METADATA_SPM_SEED_BYTES;
|
||||
/// Reserved `kb-lib.decoder.metadata.solana_program_metadata` decoder.
|
||||
pub use self::solana_program_metadata::DcMetadataSolanaProgramMetadataDecoder;
|
||||
/// Solana Program Metadata account discriminator.
|
||||
pub use self::solana_program_metadata::DcMetadataSpmAccountDiscriminator;
|
||||
/// Compression declared for Solana Program Metadata content.
|
||||
pub use self::solana_program_metadata::DcMetadataSpmCompression;
|
||||
/// Typed Solana Program Metadata content without off-chain resolution.
|
||||
pub use self::solana_program_metadata::DcMetadataSpmData;
|
||||
/// On-chain source used for Solana Program Metadata content.
|
||||
pub use self::solana_program_metadata::DcMetadataSpmDataSource;
|
||||
/// Encoding declared for Solana Program Metadata content.
|
||||
pub use self::solana_program_metadata::DcMetadataSpmEncoding;
|
||||
/// Fixed external-account data reference.
|
||||
pub use self::solana_program_metadata::DcMetadataSpmExternalData;
|
||||
/// Format declared for Solana Program Metadata content.
|
||||
pub use self::solana_program_metadata::DcMetadataSpmFormat;
|
||||
/// Bounded Solana Program Metadata model conversion error.
|
||||
pub use self::solana_program_metadata::DcMetadataSpmModelError;
|
||||
/// Official Solana Program Metadata custom error.
|
||||
pub use self::solana_program_metadata::DcMetadataSpmProgramError;
|
||||
/// Exact fixed-size Solana Program Metadata seed.
|
||||
pub use self::solana_program_metadata::DcMetadataSpmSeed;
|
||||
/// Reserved `kb-lib.decoder.metadata.spl_name_service` decoder.
|
||||
pub use self::spl_name_service::DcMetadataSplNameServiceDecoder;
|
||||
|
||||
|
||||
@@ -1,7 +1,39 @@
|
||||
// file: kb-lib/src/decoder/metadata/solana_program_metadata.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Reserved protocol decoder for `metadata_solana_program_metadata`.
|
||||
//! Public Solana Program Metadata models and reserved protocol decoder.
|
||||
|
||||
mod constants;
|
||||
mod model;
|
||||
|
||||
/// Exact fixed byte length of one Buffer account header.
|
||||
pub use self::constants::DC_METADATA_SPM_BUFFER_HEADER_BYTES;
|
||||
/// Exact byte length of one external-account data reference.
|
||||
pub use self::constants::DC_METADATA_SPM_EXTERNAL_DATA_BYTES;
|
||||
/// Exact fixed byte length of one Metadata account header.
|
||||
pub use self::constants::DC_METADATA_SPM_METADATA_HEADER_BYTES;
|
||||
/// Exact byte length of one Solana Program Metadata seed.
|
||||
pub use self::constants::DC_METADATA_SPM_SEED_BYTES;
|
||||
/// Solana Program Metadata account discriminator.
|
||||
pub use self::model::DcMetadataSpmAccountDiscriminator;
|
||||
/// Compression declared for metadata content.
|
||||
pub use self::model::DcMetadataSpmCompression;
|
||||
/// Typed on-chain metadata content without off-chain resolution.
|
||||
pub use self::model::DcMetadataSpmData;
|
||||
/// On-chain source used for metadata content.
|
||||
pub use self::model::DcMetadataSpmDataSource;
|
||||
/// Encoding declared for metadata content.
|
||||
pub use self::model::DcMetadataSpmEncoding;
|
||||
/// Fixed external-account data reference.
|
||||
pub use self::model::DcMetadataSpmExternalData;
|
||||
/// Format declared for metadata content.
|
||||
pub use self::model::DcMetadataSpmFormat;
|
||||
/// Bounded model conversion error.
|
||||
pub use self::model::DcMetadataSpmModelError;
|
||||
/// Official Solana Program Metadata custom error.
|
||||
pub use self::model::DcMetadataSpmProgramError;
|
||||
/// Exact fixed-size metadata seed.
|
||||
pub use self::model::DcMetadataSpmSeed;
|
||||
|
||||
/// Reserved decoder for the `metadata_solana_program_metadata` program surface.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// file: kb-lib/src/decoder/metadata/solana_program_metadata/constants.rs
|
||||
// version: 1
|
||||
|
||||
//! Bounded Solana Program Metadata wire constants.
|
||||
|
||||
/// Exact byte length of one Solana Program Metadata seed.
|
||||
pub const DC_METADATA_SPM_SEED_BYTES: usize = 16;
|
||||
/// Exact fixed byte length of one Buffer account header.
|
||||
pub const DC_METADATA_SPM_BUFFER_HEADER_BYTES: usize = 96;
|
||||
/// Exact fixed byte length of one Metadata account header.
|
||||
pub const DC_METADATA_SPM_METADATA_HEADER_BYTES: usize = 96;
|
||||
/// Exact byte length of one external-account data reference.
|
||||
pub const DC_METADATA_SPM_EXTERNAL_DATA_BYTES: usize = 40;
|
||||
740
kb-lib/src/decoder/metadata/solana_program_metadata/model.rs
Normal file
740
kb-lib/src/decoder/metadata/solana_program_metadata/model.rs
Normal file
@@ -0,0 +1,740 @@
|
||||
// file: kb-lib/src/decoder/metadata/solana_program_metadata/model.rs
|
||||
// version: 1
|
||||
|
||||
//! Public bounded models for the Solana Program Metadata wire contract.
|
||||
|
||||
/// Solana Program Metadata account discriminator.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[repr(u8)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DcMetadataSpmAccountDiscriminator {
|
||||
/// Uninitialized account data.
|
||||
Empty = 0,
|
||||
/// Writable staging buffer.
|
||||
Buffer = 1,
|
||||
/// Initialized program metadata account.
|
||||
Metadata = 2,
|
||||
}
|
||||
|
||||
impl DcMetadataSpmAccountDiscriminator {
|
||||
/// Returns the exact wire discriminant.
|
||||
pub const fn wire_value(self) -> u8 {
|
||||
return self as u8;
|
||||
}
|
||||
|
||||
/// Resolves an exact wire discriminant.
|
||||
pub fn from_wire_value(value: u8) -> std::result::Result<Self, crate::DcMetadataSpmModelError> {
|
||||
match value {
|
||||
0 => return std::result::Result::Ok(Self::Empty),
|
||||
1 => return std::result::Result::Ok(Self::Buffer),
|
||||
2 => return std::result::Result::Ok(Self::Metadata),
|
||||
_ => {
|
||||
return std::result::Result::Err(
|
||||
crate::DcMetadataSpmModelError::UnknownAccountDiscriminator { value },
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encoding declared for metadata content.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[repr(u8)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DcMetadataSpmEncoding {
|
||||
/// No content encoding is declared.
|
||||
None = 0,
|
||||
/// UTF-8 content encoding.
|
||||
Utf8 = 1,
|
||||
/// Base58 content encoding.
|
||||
Base58 = 2,
|
||||
/// Base64 content encoding.
|
||||
Base64 = 3,
|
||||
}
|
||||
|
||||
impl DcMetadataSpmEncoding {
|
||||
/// Returns the exact wire discriminant.
|
||||
pub const fn wire_value(self) -> u8 {
|
||||
return self as u8;
|
||||
}
|
||||
|
||||
/// Resolves an exact wire discriminant.
|
||||
pub fn from_wire_value(value: u8) -> std::result::Result<Self, crate::DcMetadataSpmModelError> {
|
||||
match value {
|
||||
0 => return std::result::Result::Ok(Self::None),
|
||||
1 => return std::result::Result::Ok(Self::Utf8),
|
||||
2 => return std::result::Result::Ok(Self::Base58),
|
||||
3 => return std::result::Result::Ok(Self::Base64),
|
||||
_ => {
|
||||
return std::result::Result::Err(crate::DcMetadataSpmModelError::UnknownEncoding {
|
||||
value,
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compression declared for metadata content.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[repr(u8)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DcMetadataSpmCompression {
|
||||
/// No content compression is declared.
|
||||
None = 0,
|
||||
/// Gzip content compression.
|
||||
Gzip = 1,
|
||||
/// Zlib content compression.
|
||||
Zlib = 2,
|
||||
}
|
||||
|
||||
impl DcMetadataSpmCompression {
|
||||
/// Returns the exact wire discriminant.
|
||||
pub const fn wire_value(self) -> u8 {
|
||||
return self as u8;
|
||||
}
|
||||
|
||||
/// Resolves an exact wire discriminant.
|
||||
pub fn from_wire_value(value: u8) -> std::result::Result<Self, crate::DcMetadataSpmModelError> {
|
||||
match value {
|
||||
0 => return std::result::Result::Ok(Self::None),
|
||||
1 => return std::result::Result::Ok(Self::Gzip),
|
||||
2 => return std::result::Result::Ok(Self::Zlib),
|
||||
_ => {
|
||||
return std::result::Result::Err(
|
||||
crate::DcMetadataSpmModelError::UnknownCompression { value },
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Format declared for metadata content.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[repr(u8)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DcMetadataSpmFormat {
|
||||
/// No structured content format is declared.
|
||||
None = 0,
|
||||
/// JSON content format.
|
||||
Json = 1,
|
||||
/// YAML content format.
|
||||
Yaml = 2,
|
||||
/// TOML content format.
|
||||
Toml = 3,
|
||||
}
|
||||
|
||||
impl DcMetadataSpmFormat {
|
||||
/// Returns the exact wire discriminant.
|
||||
pub const fn wire_value(self) -> u8 {
|
||||
return self as u8;
|
||||
}
|
||||
|
||||
/// Resolves an exact wire discriminant.
|
||||
pub fn from_wire_value(value: u8) -> std::result::Result<Self, crate::DcMetadataSpmModelError> {
|
||||
match value {
|
||||
0 => return std::result::Result::Ok(Self::None),
|
||||
1 => return std::result::Result::Ok(Self::Json),
|
||||
2 => return std::result::Result::Ok(Self::Yaml),
|
||||
3 => return std::result::Result::Ok(Self::Toml),
|
||||
_ => {
|
||||
return std::result::Result::Err(crate::DcMetadataSpmModelError::UnknownFormat {
|
||||
value,
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// On-chain source used for metadata content.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[repr(u8)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DcMetadataSpmDataSource {
|
||||
/// Content bytes are stored directly after the account header.
|
||||
Direct = 0,
|
||||
/// A UTF-8 URL is stored after the account header.
|
||||
Url = 1,
|
||||
/// A fixed external-account reference is stored after the account header.
|
||||
External = 2,
|
||||
}
|
||||
|
||||
impl DcMetadataSpmDataSource {
|
||||
/// Returns the exact wire discriminant.
|
||||
pub const fn wire_value(self) -> u8 {
|
||||
return self as u8;
|
||||
}
|
||||
|
||||
/// Resolves an exact wire discriminant.
|
||||
pub fn from_wire_value(value: u8) -> std::result::Result<Self, crate::DcMetadataSpmModelError> {
|
||||
match value {
|
||||
0 => return std::result::Result::Ok(Self::Direct),
|
||||
1 => return std::result::Result::Ok(Self::Url),
|
||||
2 => return std::result::Result::Ok(Self::External),
|
||||
_ => {
|
||||
return std::result::Result::Err(
|
||||
crate::DcMetadataSpmModelError::UnknownDataSource { value },
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Official custom error returned by the Solana Program Metadata program.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[repr(u32)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DcMetadataSpmProgramError {
|
||||
/// The referenced program account is not executable.
|
||||
NotExecutableAccount = 0,
|
||||
/// The referenced upgradeable-program state is invalid.
|
||||
InvalidProgramState = 1,
|
||||
/// The referenced program-data account is invalid.
|
||||
InvalidProgramDataAccount = 2,
|
||||
/// The metadata account is immutable.
|
||||
ImmutableMetadataAccount = 3,
|
||||
/// The metadata data length is invalid for its source.
|
||||
InvalidDataLength = 4,
|
||||
}
|
||||
|
||||
impl DcMetadataSpmProgramError {
|
||||
/// Returns the exact custom program error code.
|
||||
pub const fn code(self) -> u32 {
|
||||
return self as u32;
|
||||
}
|
||||
|
||||
/// Resolves an exact custom program error code.
|
||||
pub fn from_code(value: u32) -> std::result::Result<Self, crate::DcMetadataSpmModelError> {
|
||||
match value {
|
||||
0 => return std::result::Result::Ok(Self::NotExecutableAccount),
|
||||
1 => return std::result::Result::Ok(Self::InvalidProgramState),
|
||||
2 => return std::result::Result::Ok(Self::InvalidProgramDataAccount),
|
||||
3 => return std::result::Result::Ok(Self::ImmutableMetadataAccount),
|
||||
4 => return std::result::Result::Ok(Self::InvalidDataLength),
|
||||
_ => {
|
||||
return std::result::Result::Err(
|
||||
crate::DcMetadataSpmModelError::UnknownProgramError { value },
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Exact fixed-size seed carried by PDA derivations and account headers.
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize,
|
||||
)]
|
||||
#[serde(transparent)]
|
||||
pub struct DcMetadataSpmSeed([u8; crate::DC_METADATA_SPM_SEED_BYTES]);
|
||||
|
||||
impl DcMetadataSpmSeed {
|
||||
/// Creates a seed from its exact fixed-size wire representation.
|
||||
pub const fn from_bytes(bytes: [u8; crate::DC_METADATA_SPM_SEED_BYTES]) -> Self {
|
||||
return Self(bytes);
|
||||
}
|
||||
|
||||
/// Creates a zero-padded fixed-size seed from one UTF-8 value.
|
||||
pub fn from_utf8(value: &str) -> std::result::Result<Self, crate::DcMetadataSpmModelError> {
|
||||
let source = value.as_bytes();
|
||||
if source.len() > crate::DC_METADATA_SPM_SEED_BYTES {
|
||||
return std::result::Result::Err(crate::DcMetadataSpmModelError::SeedUtf8TooLong {
|
||||
actual_bytes: source.len(),
|
||||
max_bytes: crate::DC_METADATA_SPM_SEED_BYTES,
|
||||
});
|
||||
}
|
||||
let mut bytes = [0_u8; crate::DC_METADATA_SPM_SEED_BYTES];
|
||||
bytes[..source.len()].copy_from_slice(source);
|
||||
return std::result::Result::Ok(Self(bytes));
|
||||
}
|
||||
|
||||
/// Parses an exact fixed-size wire slice.
|
||||
pub fn from_wire_bytes(
|
||||
bytes: &[u8],
|
||||
) -> std::result::Result<Self, crate::DcMetadataSpmModelError> {
|
||||
if bytes.len() != crate::DC_METADATA_SPM_SEED_BYTES {
|
||||
return std::result::Result::Err(crate::DcMetadataSpmModelError::InvalidSeedLength {
|
||||
actual_bytes: bytes.len(),
|
||||
expected_bytes: crate::DC_METADATA_SPM_SEED_BYTES,
|
||||
});
|
||||
}
|
||||
let mut seed = [0_u8; crate::DC_METADATA_SPM_SEED_BYTES];
|
||||
seed.copy_from_slice(bytes);
|
||||
return std::result::Result::Ok(Self(seed));
|
||||
}
|
||||
|
||||
/// Returns the exact fixed-size wire representation.
|
||||
pub const fn as_bytes(&self) -> &[u8; crate::DC_METADATA_SPM_SEED_BYTES] {
|
||||
return &self.0;
|
||||
}
|
||||
|
||||
/// Returns the UTF-8 value after removing only terminal zero padding.
|
||||
pub fn trimmed_utf8(&self) -> std::result::Result<&str, crate::DcMetadataSpmModelError> {
|
||||
let mut length = self.0.len();
|
||||
while length > 0 && self.0[length - 1] == 0 {
|
||||
length -= 1;
|
||||
}
|
||||
match std::str::from_utf8(&self.0[..length]) {
|
||||
std::result::Result::Ok(value) => return std::result::Result::Ok(value),
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::DcMetadataSpmModelError::InvalidSeedUtf8);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fixed external-account data reference used by the `External` data source.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct DcMetadataSpmExternalData {
|
||||
/// Account that stores the referenced data.
|
||||
pub address: solana_pubkey::Pubkey,
|
||||
/// Byte offset inside the external account.
|
||||
pub offset: u32,
|
||||
/// Optional non-zero byte length; `None` is encoded as zero and means the whole account.
|
||||
pub length: std::option::Option<std::num::NonZeroU32>,
|
||||
}
|
||||
|
||||
impl DcMetadataSpmExternalData {
|
||||
/// Creates an exact external-account reference.
|
||||
pub const fn new(
|
||||
address: solana_pubkey::Pubkey,
|
||||
offset: u32,
|
||||
length: std::option::Option<std::num::NonZeroU32>,
|
||||
) -> Self {
|
||||
return Self { address, offset, length };
|
||||
}
|
||||
|
||||
/// Creates an external-account reference from a zeroable optional length.
|
||||
pub fn from_zeroable_length(address: solana_pubkey::Pubkey, offset: u32, length: u32) -> Self {
|
||||
return Self::new(address, offset, std::num::NonZeroU32::new(length));
|
||||
}
|
||||
|
||||
/// Serializes the exact 40-byte little-endian wire representation.
|
||||
pub fn to_wire_bytes(&self) -> [u8; crate::DC_METADATA_SPM_EXTERNAL_DATA_BYTES] {
|
||||
let mut bytes = [0_u8; crate::DC_METADATA_SPM_EXTERNAL_DATA_BYTES];
|
||||
bytes[..32].copy_from_slice(&self.address.to_bytes());
|
||||
bytes[32..36].copy_from_slice(&self.offset.to_le_bytes());
|
||||
let length = match self.length {
|
||||
std::option::Option::Some(value) => value.get(),
|
||||
std::option::Option::None => 0,
|
||||
};
|
||||
bytes[36..40].copy_from_slice(&length.to_le_bytes());
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// Parses the exact 40-byte little-endian wire representation.
|
||||
pub fn from_wire_bytes(
|
||||
bytes: &[u8],
|
||||
) -> std::result::Result<Self, crate::DcMetadataSpmModelError> {
|
||||
if bytes.len() != crate::DC_METADATA_SPM_EXTERNAL_DATA_BYTES {
|
||||
return std::result::Result::Err(
|
||||
crate::DcMetadataSpmModelError::InvalidExternalDataLength {
|
||||
actual_bytes: bytes.len(),
|
||||
expected_bytes: crate::DC_METADATA_SPM_EXTERNAL_DATA_BYTES,
|
||||
},
|
||||
);
|
||||
}
|
||||
let mut address = [0_u8; 32];
|
||||
address.copy_from_slice(&bytes[..32]);
|
||||
let mut offset = [0_u8; 4];
|
||||
offset.copy_from_slice(&bytes[32..36]);
|
||||
let mut length = [0_u8; 4];
|
||||
length.copy_from_slice(&bytes[36..40]);
|
||||
return std::result::Result::Ok(Self::from_zeroable_length(
|
||||
solana_pubkey::Pubkey::new_from_array(address),
|
||||
u32::from_le_bytes(offset),
|
||||
u32::from_le_bytes(length),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed on-chain metadata content without off-chain resolution.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(tag = "source", content = "value", rename_all = "snake_case")]
|
||||
pub enum DcMetadataSpmData {
|
||||
/// Raw content stored directly in the metadata account.
|
||||
Direct(std::vec::Vec<u8>),
|
||||
/// UTF-8 URL stored directly in the metadata account.
|
||||
Url(String),
|
||||
/// Fixed reference to data stored in another account.
|
||||
External(crate::DcMetadataSpmExternalData),
|
||||
}
|
||||
|
||||
impl DcMetadataSpmData {
|
||||
/// Returns the source discriminator associated with this content.
|
||||
pub const fn source(&self) -> crate::DcMetadataSpmDataSource {
|
||||
match self {
|
||||
Self::Direct(_) => return crate::DcMetadataSpmDataSource::Direct,
|
||||
Self::Url(_) => return crate::DcMetadataSpmDataSource::Url,
|
||||
Self::External(_) => return crate::DcMetadataSpmDataSource::External,
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates source-specific length requirements and the `u32` header bound.
|
||||
pub fn validate(&self) -> std::result::Result<(), crate::DcMetadataSpmModelError> {
|
||||
let actual_bytes = self.wire_byte_len();
|
||||
match self {
|
||||
Self::Direct(_) | Self::Url(_) if actual_bytes == 0 => {
|
||||
return std::result::Result::Err(crate::DcMetadataSpmModelError::EmptyData {
|
||||
source: self.source(),
|
||||
});
|
||||
},
|
||||
Self::Direct(_) | Self::Url(_) | Self::External(_) => {},
|
||||
}
|
||||
if u32::try_from(actual_bytes).is_err() {
|
||||
return std::result::Result::Err(
|
||||
crate::DcMetadataSpmModelError::DataLengthExceedsU32 { actual_bytes },
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
/// Returns the exact number of bytes represented on wire.
|
||||
pub fn wire_byte_len(&self) -> usize {
|
||||
match self {
|
||||
Self::Direct(value) => return value.len(),
|
||||
Self::Url(value) => return value.len(),
|
||||
Self::External(_) => return crate::DC_METADATA_SPM_EXTERNAL_DATA_BYTES,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the exact little-endian header length after validation.
|
||||
pub fn wire_length_u32(&self) -> std::result::Result<u32, crate::DcMetadataSpmModelError> {
|
||||
match self.validate() {
|
||||
std::result::Result::Ok(()) => {},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let actual_bytes = self.wire_byte_len();
|
||||
match u32::try_from(actual_bytes) {
|
||||
std::result::Result::Ok(value) => return std::result::Result::Ok(value),
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(
|
||||
crate::DcMetadataSpmModelError::DataLengthExceedsU32 { actual_bytes },
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes the exact content bytes without fetching URL or external-account data.
|
||||
pub fn to_wire_bytes(
|
||||
&self,
|
||||
) -> std::result::Result<std::vec::Vec<u8>, crate::DcMetadataSpmModelError> {
|
||||
match self.validate() {
|
||||
std::result::Result::Ok(()) => {},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
match self {
|
||||
Self::Direct(value) => return std::result::Result::Ok(value.clone()),
|
||||
Self::Url(value) => {
|
||||
return std::result::Result::Ok(value.as_bytes().to_vec());
|
||||
},
|
||||
Self::External(value) => {
|
||||
return std::result::Result::Ok(value.to_wire_bytes().to_vec());
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bounded error for Solana Program Metadata public model conversions.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum DcMetadataSpmModelError {
|
||||
/// An account discriminator is outside the published closed enum.
|
||||
UnknownAccountDiscriminator {
|
||||
/// Rejected wire value.
|
||||
value: u8,
|
||||
},
|
||||
/// An encoding discriminator is outside the published closed enum.
|
||||
UnknownEncoding {
|
||||
/// Rejected wire value.
|
||||
value: u8,
|
||||
},
|
||||
/// A compression discriminator is outside the published closed enum.
|
||||
UnknownCompression {
|
||||
/// Rejected wire value.
|
||||
value: u8,
|
||||
},
|
||||
/// A format discriminator is outside the published closed enum.
|
||||
UnknownFormat {
|
||||
/// Rejected wire value.
|
||||
value: u8,
|
||||
},
|
||||
/// A data-source discriminator is outside the published closed enum.
|
||||
UnknownDataSource {
|
||||
/// Rejected wire value.
|
||||
value: u8,
|
||||
},
|
||||
/// A custom program error code is outside the published closed enum.
|
||||
UnknownProgramError {
|
||||
/// Rejected custom error code.
|
||||
value: u32,
|
||||
},
|
||||
/// One UTF-8 seed exceeds the exact fixed-size wire bound.
|
||||
SeedUtf8TooLong {
|
||||
/// Actual UTF-8 byte length.
|
||||
actual_bytes: usize,
|
||||
/// Maximum accepted UTF-8 byte length.
|
||||
max_bytes: usize,
|
||||
},
|
||||
/// One seed slice does not have the exact fixed-size wire length.
|
||||
InvalidSeedLength {
|
||||
/// Actual slice length.
|
||||
actual_bytes: usize,
|
||||
/// Required fixed length.
|
||||
expected_bytes: usize,
|
||||
},
|
||||
/// The non-padding seed bytes are not valid UTF-8.
|
||||
InvalidSeedUtf8,
|
||||
/// One external-data slice does not have the exact fixed-size wire length.
|
||||
InvalidExternalDataLength {
|
||||
/// Actual slice length.
|
||||
actual_bytes: usize,
|
||||
/// Required fixed length.
|
||||
expected_bytes: usize,
|
||||
},
|
||||
/// Direct or URL content is empty.
|
||||
EmptyData {
|
||||
/// Source whose data is empty.
|
||||
source: crate::DcMetadataSpmDataSource,
|
||||
},
|
||||
/// Content cannot be represented by the `u32` length stored in the account header.
|
||||
DataLengthExceedsU32 {
|
||||
/// Actual content byte length.
|
||||
actual_bytes: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DcMetadataSpmModelError {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::UnknownAccountDiscriminator { value } => {
|
||||
return write!(formatter, "unknown account discriminator {value}");
|
||||
},
|
||||
Self::UnknownEncoding { value } => {
|
||||
return write!(formatter, "unknown encoding discriminator {value}");
|
||||
},
|
||||
Self::UnknownCompression { value } => {
|
||||
return write!(formatter, "unknown compression discriminator {value}");
|
||||
},
|
||||
Self::UnknownFormat { value } => {
|
||||
return write!(formatter, "unknown format discriminator {value}");
|
||||
},
|
||||
Self::UnknownDataSource { value } => {
|
||||
return write!(formatter, "unknown data-source discriminator {value}");
|
||||
},
|
||||
Self::UnknownProgramError { value } => {
|
||||
return write!(formatter, "unknown program metadata error code {value}");
|
||||
},
|
||||
Self::SeedUtf8TooLong { actual_bytes, max_bytes } => {
|
||||
return write!(
|
||||
formatter,
|
||||
"seed UTF-8 length {actual_bytes} exceeds {max_bytes} bytes",
|
||||
);
|
||||
},
|
||||
Self::InvalidSeedLength { actual_bytes, expected_bytes } => {
|
||||
return write!(
|
||||
formatter,
|
||||
"seed length {actual_bytes} does not equal {expected_bytes} bytes",
|
||||
);
|
||||
},
|
||||
Self::InvalidSeedUtf8 => {
|
||||
return formatter.write_str("seed contains invalid UTF-8 before terminal padding");
|
||||
},
|
||||
Self::InvalidExternalDataLength { actual_bytes, expected_bytes } => {
|
||||
return write!(
|
||||
formatter,
|
||||
"external data length {actual_bytes} does not equal {expected_bytes} bytes",
|
||||
);
|
||||
},
|
||||
Self::EmptyData { source } => {
|
||||
return write!(formatter, "metadata data source {source:?} cannot be empty");
|
||||
},
|
||||
Self::DataLengthExceedsU32 { actual_bytes } => {
|
||||
return write!(
|
||||
formatter,
|
||||
"metadata data length {actual_bytes} exceeds the u32 wire bound",
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DcMetadataSpmModelError {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn published_wire_values_are_exact_and_closed() {
|
||||
assert_eq!(crate::DcMetadataSpmAccountDiscriminator::Empty.wire_value(), 0);
|
||||
assert_eq!(crate::DcMetadataSpmAccountDiscriminator::Buffer.wire_value(), 1);
|
||||
assert_eq!(crate::DcMetadataSpmAccountDiscriminator::Metadata.wire_value(), 2);
|
||||
assert_eq!(crate::DcMetadataSpmEncoding::None.wire_value(), 0);
|
||||
assert_eq!(crate::DcMetadataSpmEncoding::Utf8.wire_value(), 1);
|
||||
assert_eq!(crate::DcMetadataSpmEncoding::Base58.wire_value(), 2);
|
||||
assert_eq!(crate::DcMetadataSpmEncoding::Base64.wire_value(), 3);
|
||||
assert_eq!(crate::DcMetadataSpmCompression::None.wire_value(), 0);
|
||||
assert_eq!(crate::DcMetadataSpmCompression::Gzip.wire_value(), 1);
|
||||
assert_eq!(crate::DcMetadataSpmCompression::Zlib.wire_value(), 2);
|
||||
assert_eq!(crate::DcMetadataSpmFormat::None.wire_value(), 0);
|
||||
assert_eq!(crate::DcMetadataSpmFormat::Json.wire_value(), 1);
|
||||
assert_eq!(crate::DcMetadataSpmFormat::Yaml.wire_value(), 2);
|
||||
assert_eq!(crate::DcMetadataSpmFormat::Toml.wire_value(), 3);
|
||||
assert_eq!(crate::DcMetadataSpmDataSource::Direct.wire_value(), 0);
|
||||
assert_eq!(crate::DcMetadataSpmDataSource::Url.wire_value(), 1);
|
||||
assert_eq!(crate::DcMetadataSpmDataSource::External.wire_value(), 2);
|
||||
assert_eq!(crate::DcMetadataSpmProgramError::NotExecutableAccount.code(), 0);
|
||||
assert_eq!(crate::DcMetadataSpmProgramError::InvalidProgramState.code(), 1);
|
||||
assert_eq!(crate::DcMetadataSpmProgramError::InvalidProgramDataAccount.code(), 2);
|
||||
assert_eq!(crate::DcMetadataSpmProgramError::ImmutableMetadataAccount.code(), 3);
|
||||
assert_eq!(crate::DcMetadataSpmProgramError::InvalidDataLength.code(), 4);
|
||||
assert!(crate::DcMetadataSpmAccountDiscriminator::from_wire_value(3).is_err());
|
||||
assert!(crate::DcMetadataSpmEncoding::from_wire_value(4).is_err());
|
||||
assert!(crate::DcMetadataSpmCompression::from_wire_value(3).is_err());
|
||||
assert!(crate::DcMetadataSpmFormat::from_wire_value(4).is_err());
|
||||
assert!(crate::DcMetadataSpmDataSource::from_wire_value(3).is_err());
|
||||
assert!(crate::DcMetadataSpmProgramError::from_code(5).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_preserves_exact_bytes_and_applies_the_utf8_byte_bound() {
|
||||
let seed_result = crate::DcMetadataSpmSeed::from_utf8("sécurité");
|
||||
assert!(seed_result.is_ok());
|
||||
let seed;
|
||||
if let std::result::Result::Ok(value) = seed_result {
|
||||
seed = value;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
assert_eq!(seed.as_bytes().len(), crate::DC_METADATA_SPM_SEED_BYTES);
|
||||
assert_eq!(seed.trimmed_utf8(), std::result::Result::Ok("sécurité"));
|
||||
assert!(crate::DcMetadataSpmSeed::from_utf8("éééééééé").is_ok());
|
||||
assert_eq!(
|
||||
crate::DcMetadataSpmSeed::from_utf8("ééééééééé"),
|
||||
std::result::Result::Err(crate::DcMetadataSpmModelError::SeedUtf8TooLong {
|
||||
actual_bytes: 18,
|
||||
max_bytes: crate::DC_METADATA_SPM_SEED_BYTES,
|
||||
}),
|
||||
);
|
||||
assert!(crate::DcMetadataSpmSeed::from_wire_bytes(&[0_u8; 15]).is_err());
|
||||
let invalid = crate::DcMetadataSpmSeed::from_bytes([
|
||||
0xff, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8,
|
||||
0_u8, 0_u8,
|
||||
]);
|
||||
assert_eq!(
|
||||
invalid.trimmed_utf8(),
|
||||
std::result::Result::Err(crate::DcMetadataSpmModelError::InvalidSeedUtf8),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_data_round_trips_the_exact_zeroable_layout() {
|
||||
let address = solana_pubkey::Pubkey::new_from_array([7_u8; 32]);
|
||||
let length_option = std::num::NonZeroU32::new(9);
|
||||
assert!(length_option.is_some());
|
||||
let length;
|
||||
if let std::option::Option::Some(value) = length_option {
|
||||
length = value;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
let model = crate::DcMetadataSpmExternalData::new(
|
||||
address,
|
||||
0x1122_3344,
|
||||
std::option::Option::Some(length),
|
||||
);
|
||||
let bytes = model.to_wire_bytes();
|
||||
assert_eq!(bytes.len(), crate::DC_METADATA_SPM_EXTERNAL_DATA_BYTES);
|
||||
assert_eq!(&bytes[..32], &[7_u8; 32]);
|
||||
assert_eq!(&bytes[32..36], &0x1122_3344_u32.to_le_bytes());
|
||||
assert_eq!(&bytes[36..40], &9_u32.to_le_bytes());
|
||||
assert_eq!(
|
||||
crate::DcMetadataSpmExternalData::from_wire_bytes(&bytes),
|
||||
std::result::Result::Ok(model),
|
||||
);
|
||||
let zeroable = crate::DcMetadataSpmExternalData::from_zeroable_length(address, 3, 0);
|
||||
assert_eq!(zeroable.length, std::option::Option::None);
|
||||
assert_eq!(&zeroable.to_wire_bytes()[36..40], &0_u32.to_le_bytes());
|
||||
assert!(crate::DcMetadataSpmExternalData::from_wire_bytes(&bytes[..39]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_data_keeps_url_and_external_content_on_chain_only() {
|
||||
let direct = crate::DcMetadataSpmData::Direct(vec![1_u8, 2_u8, 3_u8]);
|
||||
assert_eq!(direct.source(), crate::DcMetadataSpmDataSource::Direct);
|
||||
assert_eq!(direct.wire_length_u32(), std::result::Result::Ok(3));
|
||||
assert_eq!(direct.to_wire_bytes(), std::result::Result::Ok(vec![1_u8, 2_u8, 3_u8]));
|
||||
let url = crate::DcMetadataSpmData::Url("https://example.invalid/idl.json".to_string());
|
||||
assert_eq!(url.source(), crate::DcMetadataSpmDataSource::Url);
|
||||
assert_eq!(
|
||||
url.to_wire_bytes(),
|
||||
std::result::Result::Ok(b"https://example.invalid/idl.json".to_vec()),
|
||||
);
|
||||
let external = crate::DcMetadataSpmData::External(
|
||||
crate::DcMetadataSpmExternalData::from_zeroable_length(
|
||||
solana_pubkey::Pubkey::new_from_array([8_u8; 32]),
|
||||
12,
|
||||
20,
|
||||
),
|
||||
);
|
||||
assert_eq!(external.source(), crate::DcMetadataSpmDataSource::External);
|
||||
assert_eq!(external.wire_length_u32(), std::result::Result::Ok(40),);
|
||||
assert_eq!(
|
||||
external.to_wire_bytes().map(|value| return value.len()),
|
||||
std::result::Result::Ok(crate::DC_METADATA_SPM_EXTERNAL_DATA_BYTES),
|
||||
);
|
||||
assert!(crate::DcMetadataSpmData::Direct(std::vec::Vec::new()).validate().is_err());
|
||||
assert!(crate::DcMetadataSpmData::Url(String::new()).validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_json_representation_is_stable_and_typed() {
|
||||
let encoding_result = serde_json::to_value(crate::DcMetadataSpmEncoding::Base64);
|
||||
assert!(encoding_result.is_ok());
|
||||
let encoding;
|
||||
if let std::result::Result::Ok(value) = encoding_result {
|
||||
encoding = value;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
assert_eq!(encoding, serde_json::json!("base64"));
|
||||
let url_result = serde_json::to_value(crate::DcMetadataSpmData::Url(
|
||||
"https://example.invalid/metadata.json".to_string(),
|
||||
));
|
||||
assert!(url_result.is_ok());
|
||||
let url;
|
||||
if let std::result::Result::Ok(value) = url_result {
|
||||
url = value;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
url,
|
||||
serde_json::json!({
|
||||
"source": "url",
|
||||
"value": "https://example.invalid/metadata.json",
|
||||
}),
|
||||
);
|
||||
let seed_result = crate::DcMetadataSpmSeed::from_utf8("idl");
|
||||
assert!(seed_result.is_ok());
|
||||
let seed;
|
||||
if let std::result::Result::Ok(value) = seed_result {
|
||||
seed = value;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
let json_result = serde_json::to_value(seed);
|
||||
assert!(json_result.is_ok());
|
||||
let json;
|
||||
if let std::result::Result::Ok(value) = json_result {
|
||||
json = value;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
assert_eq!(json.as_array().map(std::vec::Vec::len), std::option::Option::Some(16));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn published_fixed_sizes_match_the_audited_contract() {
|
||||
assert_eq!(crate::DC_METADATA_SPM_SEED_BYTES, 16);
|
||||
assert_eq!(crate::DC_METADATA_SPM_BUFFER_HEADER_BYTES, 96);
|
||||
assert_eq!(crate::DC_METADATA_SPM_METADATA_HEADER_BYTES, 96);
|
||||
assert_eq!(crate::DC_METADATA_SPM_EXTERNAL_DATA_BYTES, 40);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/lib.rs
|
||||
// version: 29
|
||||
// version: 30
|
||||
|
||||
//! Consolidated decoder, executor, materializer and shared model library.
|
||||
#![warn(missing_docs)]
|
||||
@@ -15,6 +15,14 @@ mod model;
|
||||
pub use self::decoder::DC_API_CORE_INSTRUCTION_INPUT_CONTRACT_VERSION;
|
||||
/// Exhaustive published Metaplex account-key inventory.
|
||||
pub use self::decoder::DC_METADATA_MTM_METADATA_ACCOUNT_LAYOUT_AUDIT;
|
||||
/// Exposes the exact fixed byte length of one Buffer account header.
|
||||
pub use self::decoder::DC_METADATA_SPM_BUFFER_HEADER_BYTES;
|
||||
/// Exposes the exact byte length of one external-account data reference.
|
||||
pub use self::decoder::DC_METADATA_SPM_EXTERNAL_DATA_BYTES;
|
||||
/// Exposes the exact fixed byte length of one Metadata account header.
|
||||
pub use self::decoder::DC_METADATA_SPM_METADATA_HEADER_BYTES;
|
||||
/// Exposes the exact byte length of one Solana Program Metadata seed.
|
||||
pub use self::decoder::DC_METADATA_SPM_SEED_BYTES;
|
||||
/// Exact byte length of one SPL ElGamal registry account.
|
||||
pub use self::decoder::DC_SPL_ELGAMAL_REGISTRY_ACCOUNT_LEN;
|
||||
/// Reserved `kb-lib.decoder.adapter.saber_decimal_wrapper` decoder.
|
||||
@@ -226,6 +234,26 @@ pub use self::decoder::DcMetadataMtmTokenRecordAccountSnapshot;
|
||||
pub use self::decoder::DcMetadataSolanaProgramMetadataDecoder;
|
||||
/// Reserved `kb-lib.decoder.metadata.spl_name_service` decoder.
|
||||
pub use self::decoder::DcMetadataSplNameServiceDecoder;
|
||||
/// Solana Program Metadata account discriminator.
|
||||
pub use self::decoder::DcMetadataSpmAccountDiscriminator;
|
||||
/// Compression declared for Solana Program Metadata content.
|
||||
pub use self::decoder::DcMetadataSpmCompression;
|
||||
/// Typed Solana Program Metadata content without off-chain resolution.
|
||||
pub use self::decoder::DcMetadataSpmData;
|
||||
/// On-chain source used for Solana Program Metadata content.
|
||||
pub use self::decoder::DcMetadataSpmDataSource;
|
||||
/// Encoding declared for Solana Program Metadata content.
|
||||
pub use self::decoder::DcMetadataSpmEncoding;
|
||||
/// Fixed external-account data reference.
|
||||
pub use self::decoder::DcMetadataSpmExternalData;
|
||||
/// Format declared for Solana Program Metadata content.
|
||||
pub use self::decoder::DcMetadataSpmFormat;
|
||||
/// Bounded Solana Program Metadata model conversion error.
|
||||
pub use self::decoder::DcMetadataSpmModelError;
|
||||
/// Official Solana Program Metadata custom error.
|
||||
pub use self::decoder::DcMetadataSpmProgramError;
|
||||
/// Exact fixed-size Solana Program Metadata seed.
|
||||
pub use self::decoder::DcMetadataSpmSeed;
|
||||
/// Reserved `kb-lib.decoder.nft.metaplex_bubblegum` decoder.
|
||||
pub use self::decoder::DcNftMetaplexBubblegumDecoder;
|
||||
/// Exposes the reserved `DcNftTensorCnftDecoder` implementation.
|
||||
|
||||
30
kb-lib/tests/external_solana_program_metadata_model_api.rs
Normal file
30
kb-lib/tests/external_solana_program_metadata_model_api.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
// file: kb-lib/tests/external_solana_program_metadata_model_api.rs
|
||||
// version: 1
|
||||
|
||||
//! Downstream-style compilation contract for Solana Program Metadata models.
|
||||
|
||||
#[test]
|
||||
fn external_consumers_use_only_crate_root_model_exports() {
|
||||
let seed_result = kb_lib::DcMetadataSpmSeed::from_utf8("idl");
|
||||
assert!(seed_result.is_ok());
|
||||
let seed;
|
||||
if let std::result::Result::Ok(value) = seed_result {
|
||||
seed = value;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
assert_eq!(seed.trimmed_utf8(), std::result::Result::Ok("idl"));
|
||||
assert_eq!(seed.as_bytes().len(), kb_lib::DC_METADATA_SPM_SEED_BYTES);
|
||||
let external = kb_lib::DcMetadataSpmExternalData::from_zeroable_length(
|
||||
solana_pubkey::Pubkey::new_from_array([9_u8; 32]),
|
||||
4,
|
||||
8,
|
||||
);
|
||||
let data = kb_lib::DcMetadataSpmData::External(external);
|
||||
assert_eq!(data.source(), kb_lib::DcMetadataSpmDataSource::External);
|
||||
assert_eq!(data.wire_length_u32(), std::result::Result::Ok(40),);
|
||||
assert_eq!(kb_lib::DcMetadataSpmProgramError::InvalidDataLength.code(), 4);
|
||||
assert_eq!(kb_lib::DC_METADATA_SPM_BUFFER_HEADER_BYTES, 96);
|
||||
assert_eq!(kb_lib::DC_METADATA_SPM_METADATA_HEADER_BYTES, 96);
|
||||
assert_eq!(kb_lib::DC_METADATA_SPM_EXTERNAL_DATA_BYTES, 40);
|
||||
}
|
||||
Reference in New Issue
Block a user