From 313cb5ffd753734759c2a1e504a8e105acdfcfa3 Mon Sep 17 00:00:00 2001 From: Nagaprasad V R <81755170+Nagaprasadvr@users.noreply.github.com> Date: Tue, 7 Jan 2025 18:09:24 +0530 Subject: [PATCH] Index Token Inscriptions (#222) Program parser for inscriptions for blockbluster. Create and register program parser with program tranformer. Add show_instrcitions flag to the API to view the inscription information. --- blockbuster/src/programs/mod.rs | 3 + .../src/programs/token_inscriptions/mod.rs | 141 ++++++++++++++++++ digital_asset_types/src/dao/full_asset.rs | 3 + .../src/dao/generated/sea_orm_active_enums.rs | 2 + digital_asset_types/src/dao/scopes/asset.rs | 84 ++++++++--- .../src/dapi/assets_by_authority.rs | 2 +- .../src/dapi/assets_by_creator.rs | 2 +- .../src/dapi/assets_by_group.rs | 2 +- .../src/dapi/assets_by_owner.rs | 2 +- digital_asset_types/src/dapi/common/asset.rs | 29 ++++ digital_asset_types/src/dapi/get_asset.rs | 4 +- digital_asset_types/src/dapi/search_assets.rs | 2 +- digital_asset_types/src/rpc/asset.rs | 14 ++ digital_asset_types/src/rpc/options.rs | 2 + ...18N6XrfJHgDbRTaHJR328jN9dixCLQAQhDsTsRzg3v | Bin 0 -> 344 bytes ...kS3kZV4MoGps14tUSp7iVnizGbxcK4bDEhSoF5oYAZ | Bin 0 -> 224 bytes ...o9P7S8FE9NYeAcrtZEpimwQAXJMp8Lrt8p4dMkHkY2 | Bin 0 -> 224 bytes ...rH4z6SmdVzPrt8krAygpLodhdjvNAstP3taj2tysN2 | Bin 0 -> 400 bytes ...ixBLSkuhiGgVbcGhqJar476xzu1bC8wM7yHsc1iXwP | Bin 0 -> 824 bytes .../tests/integration_tests/main.rs | 1 + .../show_inscription_flag_tests.rs | 46 ++++++ ...sset_with_show_inscription_scenario_1.snap | 78 ++++++++++ migration/src/lib.rs | 2 + ...0310_add_token_inscription_enum_variant.rs | 25 ++++ program_transformers/src/lib.rs | 17 ++- .../src/token_inscription/mod.rs | 59 ++++++++ .../accountsdb-plugin-config.json | 13 +- 27 files changed, 498 insertions(+), 35 deletions(-) create mode 100644 blockbuster/src/programs/token_inscriptions/mod.rs create mode 100644 integration_tests/tests/data/accounts/get_asset_with_show_inscription_scenario_1/4Q18N6XrfJHgDbRTaHJR328jN9dixCLQAQhDsTsRzg3v create mode 100644 integration_tests/tests/data/accounts/get_asset_with_show_inscription_scenario_1/9FkS3kZV4MoGps14tUSp7iVnizGbxcK4bDEhSoF5oYAZ create mode 100644 integration_tests/tests/data/accounts/get_asset_with_show_inscription_scenario_1/AKo9P7S8FE9NYeAcrtZEpimwQAXJMp8Lrt8p4dMkHkY2 create mode 100644 integration_tests/tests/data/accounts/get_asset_with_show_inscription_scenario_1/DarH4z6SmdVzPrt8krAygpLodhdjvNAstP3taj2tysN2 create mode 100644 integration_tests/tests/data/accounts/get_asset_with_show_inscription_scenario_1/HMixBLSkuhiGgVbcGhqJar476xzu1bC8wM7yHsc1iXwP create mode 100644 integration_tests/tests/integration_tests/show_inscription_flag_tests.rs create mode 100644 integration_tests/tests/integration_tests/snapshots/integration_tests__show_inscription_flag_tests__get_asset_with_show_inscription_scenario_1.snap create mode 100644 migration/src/m20241119_060310_add_token_inscription_enum_variant.rs create mode 100644 program_transformers/src/token_inscription/mod.rs diff --git a/blockbuster/src/programs/mod.rs b/blockbuster/src/programs/mod.rs index 8da2feed0..474bce179 100644 --- a/blockbuster/src/programs/mod.rs +++ b/blockbuster/src/programs/mod.rs @@ -2,12 +2,14 @@ use bubblegum::BubblegumInstruction; use mpl_core_program::MplCoreAccountState; use token_account::TokenProgramAccount; use token_extensions::TokenExtensionsProgramAccount; +use token_inscriptions::TokenInscriptionAccount; use token_metadata::TokenMetadataAccountState; pub mod bubblegum; pub mod mpl_core_program; pub mod token_account; pub mod token_extensions; +pub mod token_inscriptions; pub mod token_metadata; // Note: `ProgramParseResult` used to contain the following variants that have been deprecated and @@ -30,5 +32,6 @@ pub enum ProgramParseResult<'a> { TokenMetadata(&'a TokenMetadataAccountState), TokenProgramAccount(&'a TokenProgramAccount), TokenExtensionsProgramAccount(&'a TokenExtensionsProgramAccount), + TokenInscriptionAccount(&'a TokenInscriptionAccount), Unknown, } diff --git a/blockbuster/src/programs/token_inscriptions/mod.rs b/blockbuster/src/programs/token_inscriptions/mod.rs new file mode 100644 index 000000000..7e40f5e07 --- /dev/null +++ b/blockbuster/src/programs/token_inscriptions/mod.rs @@ -0,0 +1,141 @@ +use serde::{Deserialize, Serialize}; +use solana_sdk::{pubkey::Pubkey, pubkeys}; + +use crate::{ + error::BlockbusterError, + program_handler::{ParseResult, ProgramParser}, +}; + +use super::ProgramParseResult; + +pubkeys!( + inscription_program_id, + "inscokhJarcjaEs59QbQ7hYjrKz25LEPRfCbP8EmdUp" +); + +pub struct TokenInscriptionParser; + +#[derive(Debug, Serialize, Deserialize)] +pub struct InscriptionData { + pub authority: String, + pub root: String, + pub content: String, + pub encoding: String, + pub inscription_data: String, + pub order: u64, + pub size: u32, + pub validation_hash: Option, +} + +impl InscriptionData { + pub const BASE_SIZE: usize = 121; + pub const INSCRIPTION_ACC_DATA_DISC: [u8; 8] = [232, 120, 205, 47, 153, 239, 229, 224]; + + pub fn try_unpack_data(data: &[u8]) -> Result { + let acc_disc = &data[0..8]; + + if acc_disc != Self::INSCRIPTION_ACC_DATA_DISC { + return Err(BlockbusterError::InvalidAccountType); + } + + if data.len() < Self::BASE_SIZE { + return Err(BlockbusterError::CustomDeserializationError( + "Inscription Data is too short".to_string(), + )); + } + + let authority = Pubkey::try_from(&data[8..40]).unwrap(); + let mint = Pubkey::try_from(&data[40..72]).unwrap(); + let inscription_data = Pubkey::try_from(&data[72..104]).unwrap(); + let order = u64::from_le_bytes(data[104..112].try_into().unwrap()); + let size = u32::from_le_bytes(data[112..116].try_into().unwrap()); + let content_type_len = u32::from_le_bytes(data[116..120].try_into().unwrap()) as usize; + let content = String::from_utf8(data[120..120 + content_type_len].to_vec()).unwrap(); + let encoding_len = u32::from_le_bytes( + data[120 + content_type_len..124 + content_type_len] + .try_into() + .unwrap(), + ) as usize; + + let encoding = String::from_utf8( + data[124 + content_type_len..124 + content_type_len + encoding_len].to_vec(), + ) + .unwrap(); + + let validation_exists = u8::from_le_bytes( + data[124 + content_type_len + encoding_len..124 + content_type_len + encoding_len + 1] + .try_into() + .unwrap(), + ); + + let validation_hash = if validation_exists == 1 { + let validation_hash_len = u32::from_le_bytes( + data[124 + content_type_len + encoding_len + 1 + ..128 + content_type_len + encoding_len + 1] + .try_into() + .unwrap(), + ) as usize; + Some( + String::from_utf8( + data[128 + content_type_len + encoding_len + 1 + ..128 + content_type_len + encoding_len + 1 + validation_hash_len] + .to_vec(), + ) + .unwrap(), + ) + } else { + None + }; + Ok(InscriptionData { + authority: authority.to_string(), + root: mint.to_string(), + content, + encoding, + inscription_data: inscription_data.to_string(), + order, + size, + validation_hash, + }) + } +} + +pub struct TokenInscriptionAccount { + pub data: InscriptionData, +} + +impl ParseResult for TokenInscriptionAccount { + fn result(&self) -> &Self + where + Self: Sized, + { + self + } + fn result_type(&self) -> ProgramParseResult { + ProgramParseResult::TokenInscriptionAccount(self) + } +} + +impl ProgramParser for TokenInscriptionParser { + fn key(&self) -> Pubkey { + inscription_program_id() + } + fn key_match(&self, key: &Pubkey) -> bool { + key == &inscription_program_id() + } + + fn handles_account_updates(&self) -> bool { + true + } + + fn handles_instructions(&self) -> bool { + false + } + + fn handle_account( + &self, + account_data: &[u8], + ) -> Result, BlockbusterError> { + let data = InscriptionData::try_unpack_data(account_data)?; + Ok(Box::new(TokenInscriptionAccount { data })) + } +} diff --git a/digital_asset_types/src/dao/full_asset.rs b/digital_asset_types/src/dao/full_asset.rs index 1d901c2ce..9e5fab427 100644 --- a/digital_asset_types/src/dao/full_asset.rs +++ b/digital_asset_types/src/dao/full_asset.rs @@ -1,5 +1,7 @@ use crate::dao::{asset, asset_authority, asset_creators, asset_data, asset_grouping}; +use super::asset_v1_account_attachments; + #[derive(Clone, Debug, PartialEq)] pub struct FullAsset { pub asset: asset::Model, @@ -7,6 +9,7 @@ pub struct FullAsset { pub authorities: Vec, pub creators: Vec, pub groups: Vec, + pub inscription: Option, } #[derive(Clone, Debug, PartialEq)] pub struct AssetRelated { diff --git a/digital_asset_types/src/dao/generated/sea_orm_active_enums.rs b/digital_asset_types/src/dao/generated/sea_orm_active_enums.rs index e4d0e012d..cf7470c6f 100644 --- a/digital_asset_types/src/dao/generated/sea_orm_active_enums.rs +++ b/digital_asset_types/src/dao/generated/sea_orm_active_enums.rs @@ -66,6 +66,8 @@ pub enum V1AccountAttachments { MasterEditionV1, #[sea_orm(string_value = "master_edition_v2")] MasterEditionV2, + #[sea_orm(string_value = "token_inscription")] + TokenInscription, #[sea_orm(string_value = "unknown")] Unknown, } diff --git a/digital_asset_types/src/dao/scopes/asset.rs b/digital_asset_types/src/dao/scopes/asset.rs index b6530e3dc..ea3fdc8a5 100644 --- a/digital_asset_types/src/dao/scopes/asset.rs +++ b/digital_asset_types/src/dao/scopes/asset.rs @@ -69,7 +69,7 @@ pub async fn get_by_creator( sort_direction: Order, pagination: &Pagination, limit: u64, - show_unverified_collections: bool, + options: &Options, ) -> Result, DbErr> { let mut condition = Condition::all() .add(asset_creators::Column::Creator.eq(creator.clone())) @@ -85,7 +85,7 @@ pub async fn get_by_creator( sort_direction, pagination, limit, - show_unverified_collections, + options, Some(creator), ) .await @@ -121,13 +121,13 @@ pub async fn get_by_grouping( sort_direction: Order, pagination: &Pagination, limit: u64, - show_unverified_collections: bool, + options: &Options, ) -> Result, DbErr> { let mut condition = asset_grouping::Column::GroupKey .eq(group_key) .and(asset_grouping::Column::GroupValue.eq(group_value)); - if !show_unverified_collections { + if !options.show_unverified_collections { condition = condition.and( asset_grouping::Column::Verified .eq(true) @@ -145,7 +145,7 @@ pub async fn get_by_grouping( sort_direction, pagination, limit, - show_unverified_collections, + options, None, ) .await @@ -158,11 +158,12 @@ pub async fn get_assets_by_owner( sort_direction: Order, pagination: &Pagination, limit: u64, - show_unverified_collections: bool, + options: &Options, ) -> Result, DbErr> { let cond = Condition::all() .add(asset::Column::Owner.eq(owner)) .add(asset::Column::Supply.gt(0)); + get_assets_by_condition( conn, cond, @@ -171,7 +172,7 @@ pub async fn get_assets_by_owner( sort_direction, pagination, limit, - show_unverified_collections, + options, ) .await } @@ -181,10 +182,12 @@ pub async fn get_assets( asset_ids: Vec>, pagination: &Pagination, limit: u64, + options: &Options, ) -> Result, DbErr> { let cond = Condition::all() .add(asset::Column::Id.is_in(asset_ids)) .add(asset::Column::Supply.gt(0)); + get_assets_by_condition( conn, cond, @@ -194,7 +197,7 @@ pub async fn get_assets( Order::Asc, pagination, limit, - false, + options, ) .await } @@ -206,7 +209,7 @@ pub async fn get_by_authority( sort_direction: Order, pagination: &Pagination, limit: u64, - show_unverified_collections: bool, + options: &Options, ) -> Result, DbErr> { let cond = Condition::all() .add(asset_authority::Column::Authority.eq(authority)) @@ -219,7 +222,7 @@ pub async fn get_by_authority( sort_direction, pagination, limit, - show_unverified_collections, + options, None, ) .await @@ -234,7 +237,7 @@ async fn get_by_related_condition( sort_direction: Order, pagination: &Pagination, limit: u64, - show_unverified_collections: bool, + options: &Options, required_creator: Option>, ) -> Result, DbErr> where @@ -253,19 +256,19 @@ where let assets = paginate(pagination, limit, stmt, sort_direction, asset::Column::Id) .all(conn) .await?; - get_related_for_assets(conn, assets, show_unverified_collections, required_creator).await + get_related_for_assets(conn, assets, options, required_creator).await } pub async fn get_related_for_assets( conn: &impl ConnectionTrait, assets: Vec, - show_unverified_collections: bool, + options: &Options, required_creator: Option>, ) -> Result, DbErr> { let asset_ids = assets.iter().map(|a| a.id.clone()).collect::>(); let asset_data: Vec = asset_data::Entity::find() - .filter(asset_data::Column::Id.is_in(asset_ids)) + .filter(asset_data::Column::Id.is_in(asset_ids.clone())) .all(conn) .await?; let asset_data_map = asset_data.into_iter().fold(HashMap::new(), |mut acc, ad| { @@ -287,6 +290,7 @@ pub async fn get_related_for_assets( authorities: vec![], creators: vec![], groups: vec![], + inscription: None, }; acc.insert(id, fa); }; @@ -296,7 +300,7 @@ pub async fn get_related_for_assets( // Get all creators for all assets in `assets_map``. let creators = asset_creators::Entity::find() - .filter(asset_creators::Column::AssetId.is_in(ids.clone())) + .filter(asset_creators::Column::AssetId.is_in(ids)) .order_by_asc(asset_creators::Column::AssetId) .order_by_asc(asset_creators::Column::Position) .all(conn) @@ -334,7 +338,7 @@ pub async fn get_related_for_assets( } } - let cond = if show_unverified_collections { + let cond = if options.show_unverified_collections { Condition::all() } else { Condition::any() @@ -351,6 +355,20 @@ pub async fn get_related_for_assets( .order_by_asc(asset_grouping::Column::AssetId) .all(conn) .await?; + + if options.show_inscription { + let attachments = asset_v1_account_attachments::Entity::find() + .filter(asset_v1_account_attachments::Column::AssetId.is_in(asset_ids)) + .all(conn) + .await?; + + for a in attachments.into_iter() { + if let Some(asset) = assets_map.get_mut(&a.id) { + asset.inscription = Some(a); + } + } + } + for g in grouping.into_iter() { if let Some(asset) = assets_map.get_mut(&g.asset_id) { asset.groups.push(g); @@ -369,7 +387,7 @@ pub async fn get_assets_by_condition( sort_direction: Order, pagination: &Pagination, limit: u64, - show_unverified_collections: bool, + options: &Options, ) -> Result, DbErr> { let mut stmt = asset::Entity::find(); for def in joins { @@ -385,8 +403,7 @@ pub async fn get_assets_by_condition( let assets = paginate(pagination, limit, stmt, sort_direction, asset::Column::Id) .all(conn) .await?; - let full_assets = - get_related_for_assets(conn, assets, show_unverified_collections, None).await?; + let full_assets = get_related_for_assets(conn, assets, options, None).await?; Ok(full_assets) } @@ -394,12 +411,20 @@ pub async fn get_by_id( conn: &impl ConnectionTrait, asset_id: Vec, include_no_supply: bool, + options: &Options, ) -> Result { let mut asset_data = asset::Entity::find_by_id(asset_id.clone()).find_also_related(asset_data::Entity); if !include_no_supply { asset_data = asset_data.filter(Condition::all().add(asset::Column::Supply.gt(0))); } + + let inscription = if options.show_inscription { + get_inscription_by_mint(conn, asset_id.clone()).await.ok() + } else { + None + }; + let asset_data: (asset::Model, Option) = asset_data.one(conn).await.and_then(|o| match o { Some((a, d)) => Ok((a, d)), @@ -439,6 +464,7 @@ pub async fn get_by_id( authorities, creators, groups: grouping, + inscription, }) } @@ -711,3 +737,23 @@ pub async fn get_nft_editions( cursor, }) } +pub async fn get_inscription_by_mint( + conn: &impl ConnectionTrait, + mint: Vec, +) -> Result { + asset_v1_account_attachments::Entity::find() + .filter( + asset_v1_account_attachments::Column::Data + .is_not_null() + .and(Expr::cust(&format!( + "data->>'root' = '{}'", + bs58::encode(mint).into_string() + ))), + ) + .one(conn) + .await + .and_then(|o| match o { + Some(t) => Ok(t), + _ => Err(DbErr::RecordNotFound("Inscription Not Found".to_string())), + }) +} diff --git a/digital_asset_types/src/dapi/assets_by_authority.rs b/digital_asset_types/src/dapi/assets_by_authority.rs index 59404f3e0..b52891062 100644 --- a/digital_asset_types/src/dapi/assets_by_authority.rs +++ b/digital_asset_types/src/dapi/assets_by_authority.rs @@ -24,7 +24,7 @@ pub async fn get_assets_by_authority( sort_direction, &pagination, page_options.limit, - options.show_unverified_collections, + options, ) .await?; Ok(build_asset_response( diff --git a/digital_asset_types/src/dapi/assets_by_creator.rs b/digital_asset_types/src/dapi/assets_by_creator.rs index 9ce5de591..3a2b1e53e 100644 --- a/digital_asset_types/src/dapi/assets_by_creator.rs +++ b/digital_asset_types/src/dapi/assets_by_creator.rs @@ -27,7 +27,7 @@ pub async fn get_assets_by_creator( sort_direction, &pagination, page_options.limit, - options.show_unverified_collections, + options, ) .await?; Ok(build_asset_response( diff --git a/digital_asset_types/src/dapi/assets_by_group.rs b/digital_asset_types/src/dapi/assets_by_group.rs index 68784b9f4..36f4ae534 100644 --- a/digital_asset_types/src/dapi/assets_by_group.rs +++ b/digital_asset_types/src/dapi/assets_by_group.rs @@ -27,7 +27,7 @@ pub async fn get_assets_by_group( sort_direction, &pagination, page_options.limit, - options.show_unverified_collections, + options, ) .await?; Ok(build_asset_response( diff --git a/digital_asset_types/src/dapi/assets_by_owner.rs b/digital_asset_types/src/dapi/assets_by_owner.rs index c3c4da3a5..5f342fe9a 100644 --- a/digital_asset_types/src/dapi/assets_by_owner.rs +++ b/digital_asset_types/src/dapi/assets_by_owner.rs @@ -24,7 +24,7 @@ pub async fn get_assets_by_owner( sort_direction, &pagination, page_options.limit, - options.show_unverified_collections, + options, ) .await?; Ok(build_asset_response( diff --git a/digital_asset_types/src/dapi/common/asset.rs b/digital_asset_types/src/dapi/common/asset.rs index 6caa1f059..72d58ef04 100644 --- a/digital_asset_types/src/dapi/common/asset.rs +++ b/digital_asset_types/src/dapi/common/asset.rs @@ -8,11 +8,13 @@ use crate::rpc::options::Options; use crate::rpc::response::TokenAccountList; use crate::rpc::response::TransactionSignatureList; use crate::rpc::response::{AssetList, DasError}; +use crate::rpc::TokenInscriptionInfo; use crate::rpc::{ Asset as RpcAsset, Authority, Compression, Content, Creator, File, Group, Interface, MetadataMap, MplCoreInfo, Ownership, Royalty, Scope, Supply, TokenAccount as RpcTokenAccount, Uses, }; +use blockbuster::programs::token_inscriptions::InscriptionData; use jsonpath_lib::JsonPathError; use log::warn; use mime_guess::Mime; @@ -348,6 +350,7 @@ pub fn asset_to_rpc(asset: FullAsset, options: &Options) -> Result Result None, }; + let inscription = if options.show_inscription { + inscription + .and_then(|i| { + i.data.map(|d| -> Result { + let deserialized_data: InscriptionData = + serde_json::from_value(d).map_err(|e| { + DbErr::Custom(format!("Failed to deserialize inscription data: {}", e)) + })?; + Ok(TokenInscriptionInfo { + authority: deserialized_data.authority, + root: deserialized_data.root, + content: deserialized_data.content, + encoding: deserialized_data.encoding, + inscription_data: deserialized_data.inscription_data, + order: deserialized_data.order, + size: deserialized_data.size, + validation_hash: deserialized_data.validation_hash, + }) + }) + }) + .and_then(|i| i.ok()) + } else { + None + }; + Ok(RpcAsset { interface: interface.clone(), id: bs58::encode(asset.id).into_string(), @@ -447,6 +475,7 @@ pub fn asset_to_rpc(asset: FullAsset, options: &Options) -> Result, options: &Options, ) -> Result { - let asset = scopes::asset::get_by_id(db, id, false).await?; + let asset = scopes::asset::get_by_id(db, id, false, options).await?; asset_to_rpc(asset, options) } @@ -22,7 +22,7 @@ pub async fn get_assets( options: &Options, ) -> Result, DbErr> { let pagination = Pagination::Page { page: 1 }; - let assets = scopes::asset::get_assets(db, ids, &pagination, limit).await?; + let assets = scopes::asset::get_assets(db, ids, &pagination, limit, options).await?; let asset_list = build_asset_response(assets, limit, &pagination, options); let asset_map = asset_list .items diff --git a/digital_asset_types/src/dapi/search_assets.rs b/digital_asset_types/src/dapi/search_assets.rs index a7ee65509..85a0f190d 100644 --- a/digital_asset_types/src/dapi/search_assets.rs +++ b/digital_asset_types/src/dapi/search_assets.rs @@ -23,7 +23,7 @@ pub async fn search_assets( sort_direction, &pagination, page_options.limit, - options.show_unverified_collections, + options, ) .await?; Ok(build_asset_response( diff --git a/digital_asset_types/src/rpc/asset.rs b/digital_asset_types/src/rpc/asset.rs index f8f7aab98..5c333cf71 100644 --- a/digital_asset_types/src/rpc/asset.rs +++ b/digital_asset_types/src/rpc/asset.rs @@ -379,6 +379,18 @@ pub struct MplCoreInfo { pub plugins_json_version: Option, } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct TokenInscriptionInfo { + pub authority: String, + pub root: String, + pub inscription_data: String, + pub content: String, + pub encoding: String, + pub order: u64, + pub size: u32, + pub validation_hash: Option, +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, Default)] pub struct Asset { pub interface: Interface, @@ -406,6 +418,8 @@ pub struct Asset { #[serde(skip_serializing_if = "Option::is_none")] pub mint_extensions: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub inscription: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub plugins: Option, #[serde(skip_serializing_if = "Option::is_none")] pub unknown_plugins: Option, diff --git a/digital_asset_types/src/rpc/options.rs b/digital_asset_types/src/rpc/options.rs index 219f27c6e..af3520fc3 100644 --- a/digital_asset_types/src/rpc/options.rs +++ b/digital_asset_types/src/rpc/options.rs @@ -8,4 +8,6 @@ pub struct Options { pub show_unverified_collections: bool, #[serde(default)] pub show_zero_balance: bool, + #[serde(default)] + pub show_inscription: bool, } diff --git a/integration_tests/tests/data/accounts/get_asset_with_show_inscription_scenario_1/4Q18N6XrfJHgDbRTaHJR328jN9dixCLQAQhDsTsRzg3v b/integration_tests/tests/data/accounts/get_asset_with_show_inscription_scenario_1/4Q18N6XrfJHgDbRTaHJR328jN9dixCLQAQhDsTsRzg3v new file mode 100644 index 0000000000000000000000000000000000000000..7443f1f4604775e965ce30b1ced2063442ed3dfa GIT binary patch literal 344 zcmY#jfB*@G3fHs`ZBd z_a&V))_lV7%$IA+x;lINxstcfw=EKp`|)Uw%85S@@AI#ju=}2++D?(TATt?e8?%6@ z|4<;=%PR!po4u+Gn#{<+a02cT!Mf!$a)D5HX!++-1Y~Mn&|1` z7*cU7IYEN8S1Iyxc1Sv5 za#iQRQ{h!A!A=KVHnN^L*wnW&i_uqEa=Pjii9W%T7N?ZivZ5VA9YnbqR2Ej|?4RCt S1L!ygPgg&ebxsLQ3=9DBh;%6c literal 0 HcmV?d00001 diff --git a/integration_tests/tests/data/accounts/get_asset_with_show_inscription_scenario_1/9FkS3kZV4MoGps14tUSp7iVnizGbxcK4bDEhSoF5oYAZ b/integration_tests/tests/data/accounts/get_asset_with_show_inscription_scenario_1/9FkS3kZV4MoGps14tUSp7iVnizGbxcK4bDEhSoF5oYAZ new file mode 100644 index 0000000000000000000000000000000000000000..1478546cdd8b73455762623c7528974d1d668c99 GIT binary patch literal 224 zcmY#jfB*@G3LaA z|0L^IeGS$&+P{tM?ze~6Qx{IYdHSK-x!09zWVW=vjgK~;Xw~zzweCN|N|2cezG6U< zf#E+C9PH*50`Z+Tmj_K|WMBvavVj!CUgzbO3X+rJS2 zOe^}xu=-I4mowYlZx64hE}VSx^h39EuPfKcY-xQPA8kI-s^@EK-G7FaATtwu#egIO z!+$7XYUdRK^H=5vO=e_Z2m*3|6a%C3^OlL(EsG>e3psWFX&g%OQe5{)sP38R=8C`S Ood1-GEWj(^>Bq| z@|>o^ViDCD*P^+$tgExPpDTI$eA^-sxgU?_sGRuo@IL>V3A^uEs_hhc3o_H-u?CQ2 zVE7LOsXe?xApZCL6+x3385kIW_(jE8{h9BdJ^+d11651*SbtvO^;qNEns+9<9b4CX zZC`a%?Vn`*s;|M?M*FuJl~0gmWXQ{3!fhhqy*;^FCdF*kdc*(wl1>_HK4Ey~dz1}g z`Y9k~3dEVYiRr2Od8sA(MVX}qS(Qck#yQ1h*-7y^nZ+fESs4adDaA#_S-E+Y8KuP) zWmS2V$%W-0Gm;XEQ_V~m9e_;pv_t~~6GIaV<0MO?G}A-_6B7#qGxOBs)HKVa)Z|1X mLrbG1L*t~hR134Tq!d$QpmHOVR11@|c+$zkEL!@0 z-}H=#HIJ&6?6Lm5!t1fdw>9rfc00DN_u9VdsM~Qve!NP@0!*WMXLu zG87x&0tyCK<|gImfMibqaYji=L9vy-eqvF1YGPTcUS4X6exX-NR%(Dngj-sqr@L8| zX@QrnaZqtokdbS0Qo5&4M0|dUZjw=&tFu8m$e2L^6qp%7fk`M38B-V-8UH|oeNZ@j Hs5t@vd6|Gg literal 0 HcmV?d00001 diff --git a/integration_tests/tests/integration_tests/main.rs b/integration_tests/tests/integration_tests/main.rs index 62eafb887..01142197a 100644 --- a/integration_tests/tests/integration_tests/main.rs +++ b/integration_tests/tests/integration_tests/main.rs @@ -6,5 +6,6 @@ mod general_scenario_tests; mod mpl_core_tests; mod nft_editions_tests; mod regular_nft_tests; +mod show_inscription_flag_tests; mod test_show_zero_balance_filter; mod token_accounts_tests; diff --git a/integration_tests/tests/integration_tests/show_inscription_flag_tests.rs b/integration_tests/tests/integration_tests/show_inscription_flag_tests.rs new file mode 100644 index 000000000..7c5dffdff --- /dev/null +++ b/integration_tests/tests/integration_tests/show_inscription_flag_tests.rs @@ -0,0 +1,46 @@ +use function_name::named; + +use das_api::api::{self, ApiContract}; + +use itertools::Itertools; + +use serial_test::serial; + +use super::common::*; + +#[tokio::test] +#[serial] +#[named] +async fn test_get_asset_with_show_inscription_scenario_1() { + let name = trim_test_name(function_name!()); + let setup = TestSetup::new_with_options( + name.clone(), + TestSetupOptions { + network: Some(Network::Mainnet), + }, + ) + .await; + + let seeds: Vec = seed_accounts([ + "9FkS3kZV4MoGps14tUSp7iVnizGbxcK4bDEhSoF5oYAZ", + "HMixBLSkuhiGgVbcGhqJar476xzu1bC8wM7yHsc1iXwP", + "DarH4z6SmdVzPrt8krAygpLodhdjvNAstP3taj2tysN2", + ]); + + apply_migrations_and_delete_data(setup.db.clone()).await; + index_seed_events(&setup, seeds.iter().collect_vec()).await; + + let request = r#" + { + "id": "9FkS3kZV4MoGps14tUSp7iVnizGbxcK4bDEhSoF5oYAZ", + "displayOptions": { + "showInscription": true + } + } + "#; + + let request: api::GetAsset = serde_json::from_str(request).unwrap(); + let response = setup.das_api.get_asset(request).await.unwrap(); + + insta::assert_json_snapshot!(name, response); +} diff --git a/integration_tests/tests/integration_tests/snapshots/integration_tests__show_inscription_flag_tests__get_asset_with_show_inscription_scenario_1.snap b/integration_tests/tests/integration_tests/snapshots/integration_tests__show_inscription_flag_tests__get_asset_with_show_inscription_scenario_1.snap new file mode 100644 index 000000000..62373341d --- /dev/null +++ b/integration_tests/tests/integration_tests/snapshots/integration_tests__show_inscription_flag_tests__get_asset_with_show_inscription_scenario_1.snap @@ -0,0 +1,78 @@ +--- +source: integration_tests/tests/integration_tests/show_inscription_flag_tests.rs +expression: response +snapshot_kind: text +--- +{ + "interface": "V1_NFT", + "id": "9FkS3kZV4MoGps14tUSp7iVnizGbxcK4bDEhSoF5oYAZ", + "content": { + "$schema": "https://schema.metaplex.com/nft1.0.json", + "json_uri": "https://arweave.net/qJdjeP8XFfYIG6z5pJ-3RsZR2EcbgILX_ot-b2fEC0g", + "files": [], + "metadata": { + "name": "punk2491", + "symbol": "Symbol", + "token_standard": "NonFungible" + }, + "links": {} + }, + "authorities": [ + { + "address": "BVk6Bvxa9v6Y32o7KGPhYV4CU9pmG2K7nAYc7mDejsGM", + "scopes": [ + "full" + ] + } + ], + "compression": { + "eligible": false, + "compressed": false, + "data_hash": "", + "creator_hash": "", + "asset_hash": "", + "tree": "", + "seq": 0, + "leaf_id": 0 + }, + "grouping": [], + "royalty": { + "royalty_model": "creators", + "target": null, + "percent": 0.08, + "basis_points": 800, + "primary_sale_happened": false, + "locked": false + }, + "creators": [ + { + "address": "BVk6Bvxa9v6Y32o7KGPhYV4CU9pmG2K7nAYc7mDejsGM", + "share": 100, + "verified": true + } + ], + "ownership": { + "frozen": false, + "delegated": false, + "delegate": null, + "ownership_model": "single", + "owner": "" + }, + "supply": { + "print_max_supply": 0, + "print_current_supply": 0, + "edition_nonce": 252 + }, + "mutable": true, + "burnt": false, + "inscription": { + "authority": "11111111111111111111111111111111", + "root": "9FkS3kZV4MoGps14tUSp7iVnizGbxcK4bDEhSoF5oYAZ", + "inscription_data": "4Q18N6XrfJHgDbRTaHJR328jN9dixCLQAQhDsTsRzg3v", + "content": "image/net/riupjyro3lsvkb_listajh0jdsrsjmnyhusxvznycqw", + "encoding": "base64", + "order": 1733, + "size": 202, + "validation_hash": "7fa0041483b92f5a0448067ecef9beca2192b13bfe86fbd53a0024e84fcea652" + } +} diff --git a/migration/src/lib.rs b/migration/src/lib.rs index d2d5c9051..211501cd3 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -44,6 +44,7 @@ mod m20240319_120101_add_mpl_core_enum_vals; mod m20240320_120101_add_mpl_core_info_items; mod m20240520_120101_add_mpl_core_external_plugins_columns; mod m20240718_161232_change_supply_columns_to_numeric; +mod m20241119_060310_add_token_inscription_enum_variant; pub mod model; @@ -97,6 +98,7 @@ impl MigratorTrait for Migrator { Box::new(m20240320_120101_add_mpl_core_info_items::Migration), Box::new(m20240520_120101_add_mpl_core_external_plugins_columns::Migration), Box::new(m20240718_161232_change_supply_columns_to_numeric::Migration), + Box::new(m20241119_060310_add_token_inscription_enum_variant::Migration), ] } } diff --git a/migration/src/m20241119_060310_add_token_inscription_enum_variant.rs b/migration/src/m20241119_060310_add_token_inscription_enum_variant.rs new file mode 100644 index 000000000..1bdd6f8c3 --- /dev/null +++ b/migration/src/m20241119_060310_add_token_inscription_enum_variant.rs @@ -0,0 +1,25 @@ +use sea_orm::{ConnectionTrait, DatabaseBackend, Statement}; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .get_connection() + .execute(Statement::from_string( + DatabaseBackend::Postgres, + "ALTER TYPE v1_account_attachments ADD VALUE IF NOT EXISTS 'token_inscription';" + .to_string(), + )) + .await?; + + Ok(()) + } + + async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> { + Ok(()) + } +} diff --git a/program_transformers/src/lib.rs b/program_transformers/src/lib.rs index 32d3aa02f..99a10277b 100644 --- a/program_transformers/src/lib.rs +++ b/program_transformers/src/lib.rs @@ -4,6 +4,7 @@ use { error::{ProgramTransformerError, ProgramTransformerResult}, mpl_core_program::handle_mpl_core_account, token::handle_token_program_account, + token_inscription::handle_token_inscription_program_update, token_metadata::handle_token_metadata_account, }, blockbuster::{ @@ -12,7 +13,8 @@ use { programs::{ bubblegum::BubblegumParser, mpl_core_program::MplCoreParser, token_account::TokenAccountParser, token_extensions::Token2022AccountParser, - token_metadata::TokenMetadataParser, ProgramParseResult, + token_inscriptions::TokenInscriptionParser, token_metadata::TokenMetadataParser, + ProgramParseResult, }, }, futures::future::BoxFuture, @@ -36,6 +38,7 @@ pub mod error; mod mpl_core_program; mod token; mod token_extensions; +mod token_inscription; mod token_metadata; #[derive(Debug, Clone, PartialEq, Eq)] @@ -91,17 +94,19 @@ pub struct ProgramTransformer { impl ProgramTransformer { pub fn new(pool: PgPool, download_metadata_notifier: DownloadMetadataNotifier) -> Self { - let mut parsers: HashMap> = HashMap::with_capacity(5); + let mut parsers: HashMap> = HashMap::with_capacity(6); let bgum = BubblegumParser {}; let token_metadata = TokenMetadataParser {}; let token = TokenAccountParser {}; let mpl_core = MplCoreParser {}; let token_extensions = Token2022AccountParser {}; + let token_inscription = TokenInscriptionParser {}; parsers.insert(bgum.key(), Box::new(bgum)); parsers.insert(token_metadata.key(), Box::new(token_metadata)); parsers.insert(token.key(), Box::new(token)); parsers.insert(mpl_core.key(), Box::new(mpl_core)); parsers.insert(token_extensions.key(), Box::new(token_extensions)); + parsers.insert(token_inscription.key(), Box::new(token_inscription)); let hs = parsers.iter().fold(HashSet::new(), |mut acc, (k, _)| { acc.insert(*k); acc @@ -253,6 +258,14 @@ impl ProgramTransformer { ) .await } + ProgramParseResult::TokenInscriptionAccount(parsing_result) => { + handle_token_inscription_program_update( + account_info, + parsing_result, + &self.storage, + ) + .await + } _ => Err(ProgramTransformerError::NotImplemented), }?; } diff --git a/program_transformers/src/token_inscription/mod.rs b/program_transformers/src/token_inscription/mod.rs new file mode 100644 index 000000000..957275fc8 --- /dev/null +++ b/program_transformers/src/token_inscription/mod.rs @@ -0,0 +1,59 @@ +use std::str::FromStr; + +use crate::AccountInfo; +use blockbuster::programs::token_inscriptions::TokenInscriptionAccount; +use digital_asset_types::dao::asset_v1_account_attachments; +use digital_asset_types::dao::sea_orm_active_enums::V1AccountAttachments; +use sea_orm::sea_query::OnConflict; +use sea_orm::{ + ActiveValue, ConnectionTrait, DatabaseConnection, DbBackend, EntityTrait, QueryTrait, +}; +use solana_sdk::pubkey::Pubkey; + +use crate::error::{ProgramTransformerError, ProgramTransformerResult}; + +pub async fn handle_token_inscription_program_update<'a, 'b>( + account_info: &AccountInfo, + parsing_result: &'a TokenInscriptionAccount, + db: &'b DatabaseConnection, +) -> ProgramTransformerResult<()> { + let account_key = account_info.pubkey.to_bytes().to_vec(); + + let TokenInscriptionAccount { data } = parsing_result; + + let ser = serde_json::to_value(data) + .map_err(|e| ProgramTransformerError::SerializatonError(e.to_string()))?; + + let asset_id = Pubkey::from_str(&data.root) + .map_err(|e| ProgramTransformerError::ParsingError(e.to_string()))? + .to_bytes() + .to_vec(); + + let model = asset_v1_account_attachments::ActiveModel { + id: ActiveValue::Set(account_key), + asset_id: ActiveValue::Set(Some(asset_id)), + data: ActiveValue::Set(Some(ser)), + slot_updated: ActiveValue::Set(account_info.slot as i64), + initialized: ActiveValue::Set(true), + attachment_type: ActiveValue::Set(V1AccountAttachments::TokenInscription), + }; + + let mut query = asset_v1_account_attachments::Entity::insert(model) + .on_conflict( + OnConflict::columns([asset_v1_account_attachments::Column::Id]) + .update_columns([ + asset_v1_account_attachments::Column::Data, + asset_v1_account_attachments::Column::SlotUpdated, + ]) + .to_owned(), + ) + .build(DbBackend::Postgres); + + query.sql = format!( + "{} WHERE excluded.slot_updated > asset_v1_account_attachments.slot_updated", + query.sql + ); + db.execute(query).await?; + + Ok(()) +} diff --git a/solana-test-validator-geyser-config/accountsdb-plugin-config.json b/solana-test-validator-geyser-config/accountsdb-plugin-config.json index 3925d085a..34a6adce8 100644 --- a/solana-test-validator-geyser-config/accountsdb-plugin-config.json +++ b/solana-test-validator-geyser-config/accountsdb-plugin-config.json @@ -4,19 +4,18 @@ "metrics_port": 8125, "metrics_uri": "graphite", "env": "dev", - "accounts_selector" : { - "owners" : [ + "accounts_selector": { + "owners": [ "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", "BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY", - "CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d" + "CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d", + "inscokhJarcjaEs59QbQ7hYjrKz25LEPRfCbP8EmdUp" ] }, - "transaction_selector" : { - "mentions" : [ - "BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY" - ] + "transaction_selector": { + "mentions": ["BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY"] } }