From 8d922cadb7829f6591bbab62cb39177ddb69bda3 Mon Sep 17 00:00:00 2001 From: Nagaprasadvr Date: Thu, 14 Nov 2024 12:12:49 +0530 Subject: [PATCH] Query NFT Editions Associate editions to the nft mint of the master edition. Add new method to API get_nft_editions to find editions for the mint. --- Cargo.lock | 1 + .../src/programs/token_extensions/mod.rs | 1 - das_api/src/api/api_impl.rs | 35 ++++- das_api/src/api/mod.rs | 22 +++- das_api/src/builder.rs | 10 +- digital_asset_types/Cargo.toml | 1 + digital_asset_types/src/dao/scopes/asset.rs | 124 +++++++++++++++++- digital_asset_types/src/rpc/response.rs | 29 ++++ ...9QuYLpiMu4ZQmhdEHmgATdgiHkDeJfvZi84BfkYcez | Bin 0 -> 824 bytes ...HfqzJYABeGfiG1apwiEYt6TvfGQiL1pdwEjvTKsyiZ | Bin 0 -> 424 bytes ...mY7qCaq7WbrR7RZdHWCNS9FrFRPwRqU84wzWfmqLDz | Bin 0 -> 824 bytes ...QecKKYSHxez7fFjJkUvkz42TLmkoXzhyZxEf2pw8pz | Bin 0 -> 224 bytes ...xgzXKEsJmUyF5pBb3djn9cJFA26zh2SQHvd9EYijZV | Bin 0 -> 384 bytes ...2Qb8kLctbchQsMnhZs5DjY32To2QtPuXNwWvk4NosL | Bin 0 -> 224 bytes ...vFDcBWf6aDncd1TBzx2ou1rgLFYaMBdbYLBa9oTAEw | Bin 0 -> 224 bytes ...iWoA4jqHFkodPJgtbRYRcYtiXbsVytnxnEao3QT2gg | Bin 0 -> 384 bytes .../tests/integration_tests/main.rs | 1 + .../integration_tests/nft_editions_tests.rs | 50 +++++++ ...token_extensions_get_asset_scenario_1.snap | 2 - ...token_extensions_get_asset_scenario_2.snap | 2 - ...token_extensions_get_asset_scenario_3.snap | 2 - ..._nft_editions_tests__get_nft_editions.snap | 24 ++++ .../src/token_metadata/master_edition.rs | 73 +++++++---- .../src/token_metadata/mod.rs | 29 +++- .../src/token_metadata/v1_asset.rs | 35 +++++ 25 files changed, 401 insertions(+), 40 deletions(-) create mode 100644 integration_tests/tests/data/accounts/get_nft_editions/4V9QuYLpiMu4ZQmhdEHmgATdgiHkDeJfvZi84BfkYcez create mode 100644 integration_tests/tests/data/accounts/get_nft_editions/8SHfqzJYABeGfiG1apwiEYt6TvfGQiL1pdwEjvTKsyiZ create mode 100644 integration_tests/tests/data/accounts/get_nft_editions/9ZmY7qCaq7WbrR7RZdHWCNS9FrFRPwRqU84wzWfmqLDz create mode 100644 integration_tests/tests/data/accounts/get_nft_editions/9yQecKKYSHxez7fFjJkUvkz42TLmkoXzhyZxEf2pw8pz create mode 100644 integration_tests/tests/data/accounts/get_nft_editions/AoxgzXKEsJmUyF5pBb3djn9cJFA26zh2SQHvd9EYijZV create mode 100644 integration_tests/tests/data/accounts/get_nft_editions/Ey2Qb8kLctbchQsMnhZs5DjY32To2QtPuXNwWvk4NosL create mode 100644 integration_tests/tests/data/accounts/get_nft_editions/GJvFDcBWf6aDncd1TBzx2ou1rgLFYaMBdbYLBa9oTAEw create mode 100644 integration_tests/tests/data/accounts/get_nft_editions/giWoA4jqHFkodPJgtbRYRcYtiXbsVytnxnEao3QT2gg create mode 100644 integration_tests/tests/integration_tests/nft_editions_tests.rs create mode 100644 integration_tests/tests/integration_tests/snapshots/integration_tests__nft_editions_tests__get_nft_editions.snap diff --git a/Cargo.lock b/Cargo.lock index 34458c8ad..2e8cba4d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1946,6 +1946,7 @@ dependencies = [ "jsonpath_lib", "log", "mime_guess", + "mpl-token-metadata", "num-derive 0.3.3", "num-traits", "schemars", diff --git a/blockbuster/src/programs/token_extensions/mod.rs b/blockbuster/src/programs/token_extensions/mod.rs index faf12b719..6272f96b5 100644 --- a/blockbuster/src/programs/token_extensions/mod.rs +++ b/blockbuster/src/programs/token_extensions/mod.rs @@ -66,7 +66,6 @@ impl MintAccountExtensions { pub fn is_some(&self) -> bool { self.default_account_state.is_some() || self.confidential_transfer_mint.is_some() - || self.confidential_transfer_account.is_some() || self.confidential_transfer_fee_config.is_some() || self.interest_bearing_config.is_some() || self.transfer_fee_config.is_some() diff --git a/das_api/src/api/api_impl.rs b/das_api/src/api/api_impl.rs index 4f25ed653..9aac04b2c 100644 --- a/das_api/src/api/api_impl.rs +++ b/das_api/src/api/api_impl.rs @@ -1,15 +1,15 @@ use digital_asset_types::{ dao::{ - scopes::asset::get_grouping, + scopes::asset::{get_grouping, get_nft_editions}, sea_orm_active_enums::{ OwnerType, RoyaltyTargetType, SpecificationAssetClass, SpecificationVersions, }, Cursor, PageOptions, SearchAssetsQuery, }, dapi::{ - get_asset, get_asset_proofs, get_asset_signatures, get_assets, get_assets_by_authority, - get_assets_by_creator, get_assets_by_group, get_assets_by_owner, get_proof_for_asset, - get_token_accounts, search_assets, + common::create_pagination, get_asset, get_asset_proofs, get_asset_signatures, get_assets, + get_assets_by_authority, get_assets_by_creator, get_assets_by_group, get_assets_by_owner, + get_proof_for_asset, get_token_accounts, search_assets, }, rpc::{ filter::{AssetSortBy, SearchConditionType}, @@ -501,6 +501,7 @@ impl ApiContract for DasApi { .await .map_err(Into::into) } + async fn get_grouping( self: &DasApi, payload: GetGrouping, @@ -545,4 +546,30 @@ impl ApiContract for DasApi { .await .map_err(Into::into) } + + async fn get_nft_editions( + self: &DasApi, + payload: GetNftEditions, + ) -> Result { + let GetNftEditions { + mint_address, + page, + limit, + before, + after, + cursor, + } = payload; + + let page_options = self.validate_pagination(limit, page, &before, &after, &cursor, None)?; + let mint_address = validate_pubkey(mint_address.clone())?; + let pagination = create_pagination(&page_options)?; + get_nft_editions( + &self.db_connection, + mint_address, + &pagination, + page_options.limit, + ) + .await + .map_err(Into::into) + } } diff --git a/das_api/src/api/mod.rs b/das_api/src/api/mod.rs index ab9da0f9b..b98fe2cbe 100644 --- a/das_api/src/api/mod.rs +++ b/das_api/src/api/mod.rs @@ -2,7 +2,9 @@ use crate::error::DasApiError; use async_trait::async_trait; use digital_asset_types::rpc::filter::{AssetSortDirection, SearchConditionType}; use digital_asset_types::rpc::options::Options; -use digital_asset_types::rpc::response::{AssetList, TokenAccountList, TransactionSignatureList}; +use digital_asset_types::rpc::response::{ + AssetList, NftEditions, TokenAccountList, TransactionSignatureList, +}; use digital_asset_types::rpc::{filter::AssetSorting, response::GetGroupingResponse}; use digital_asset_types::rpc::{Asset, AssetProof, Interface, OwnershipModel, RoyaltyModel}; use open_rpc_derive::{document_rpc, rpc}; @@ -147,6 +149,18 @@ pub struct GetGrouping { pub group_value: String, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +pub struct GetNftEditions { + pub mint_address: String, + pub page: Option, + pub limit: Option, + pub before: Option, + pub after: Option, + #[serde(default)] + pub cursor: Option, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)] #[serde(deny_unknown_fields, rename_all = "camelCase")] pub struct GetAssetSignatures { @@ -276,4 +290,10 @@ pub trait ApiContract: Send + Sync + 'static { &self, payload: GetTokenAccounts, ) -> Result; + #[rpc( + name = "getNftEditions", + params = "named", + summary = "Get all printable editions for a master edition NFT mint" + )] + async fn get_nft_editions(&self, payload: GetNftEditions) -> Result; } diff --git a/das_api/src/builder.rs b/das_api/src/builder.rs index df5e3bc6c..d048dfc5a 100644 --- a/das_api/src/builder.rs +++ b/das_api/src/builder.rs @@ -128,9 +128,17 @@ impl RpcApiBuilder { .map_err(Into::into) }, )?; - module.register_alias("getTokenAccounts", "get_token_accounts")?; + module.register_async_method("get_nft_editions", |rpc_params, rpc_context| async move { + let payload = rpc_params.parse::()?; + rpc_context + .get_nft_editions(payload) + .await + .map_err(Into::into) + })?; + module.register_alias("getNftEditions", "get_nft_editions")?; + Ok(module) } } diff --git a/digital_asset_types/Cargo.toml b/digital_asset_types/Cargo.toml index 449f6e78d..53d7bc0d5 100644 --- a/digital_asset_types/Cargo.toml +++ b/digital_asset_types/Cargo.toml @@ -29,6 +29,7 @@ spl-concurrent-merkle-tree = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["macros"] } url = { workspace = true } +mpl-token-metadata = { workspace = true } [features] default = ["json_types", "sql_types"] diff --git a/digital_asset_types/src/dao/scopes/asset.rs b/digital_asset_types/src/dao/scopes/asset.rs index b3c4e0e63..b6530e3dc 100644 --- a/digital_asset_types/src/dao/scopes/asset.rs +++ b/digital_asset_types/src/dao/scopes/asset.rs @@ -1,15 +1,24 @@ use crate::{ dao::{ asset::{self}, - asset_authority, asset_creators, asset_data, asset_grouping, cl_audits_v2, + asset_authority, asset_creators, asset_data, asset_grouping, asset_v1_account_attachments, + cl_audits_v2, extensions::{self, instruction::PascalCase}, - sea_orm_active_enums::Instruction, + sea_orm_active_enums::{Instruction, V1AccountAttachments}, token_accounts, Cursor, FullAsset, GroupingSize, Pagination, }, - rpc::{filter::AssetSortDirection, options::Options}, + rpc::{ + filter::AssetSortDirection, + options::Options, + response::{NftEdition, NftEditions}, + }, }; use indexmap::IndexMap; -use sea_orm::{entity::*, query::*, ConnectionTrait, DbErr, Order}; +use mpl_token_metadata::accounts::{Edition, MasterEdition}; +use sea_orm::{entity::*, query::*, sea_query::Expr, ConnectionTrait, DbErr, Order}; +use serde::de::DeserializeOwned; +use serde_json::Value; +use solana_sdk::pubkey::Pubkey; use std::collections::HashMap; pub fn paginate( @@ -595,3 +604,110 @@ pub async fn get_token_accounts( Ok(token_accounts) } + +pub fn get_edition_data_from_json(data: Value) -> Result { + serde_json::from_value(data).map_err(|e| DbErr::Custom(e.to_string())) +} + +pub fn attachment_to_nft_edition( + attachment: asset_v1_account_attachments::Model, +) -> Result { + let data: Edition = attachment + .data + .clone() + .ok_or(DbErr::RecordNotFound("Edition data not found".to_string())) + .map(get_edition_data_from_json)??; + + Ok(NftEdition { + mint_address: attachment + .asset_id + .clone() + .map(|id| bs58::encode(id).into_string()) + .unwrap_or("".to_string()), + edition_number: data.edition, + edition_address: bs58::encode(attachment.id.clone()).into_string(), + }) +} + +pub async fn get_nft_editions( + conn: &impl ConnectionTrait, + mint_address: Pubkey, + pagination: &Pagination, + limit: u64, +) -> Result { + let master_edition_pubkey = MasterEdition::find_pda(&mint_address).0; + + // to fetch nft editions associated with a mint we need to fetch the master edition first + let master_edition = + asset_v1_account_attachments::Entity::find_by_id(master_edition_pubkey.to_bytes().to_vec()) + .one(conn) + .await? + .ok_or(DbErr::RecordNotFound( + "Master Edition not found".to_string(), + ))?; + + let master_edition_data: MasterEdition = master_edition + .data + .clone() + .ok_or(DbErr::RecordNotFound( + "Master Edition data not found".to_string(), + )) + .map(get_edition_data_from_json)??; + + let mut stmt = asset_v1_account_attachments::Entity::find(); + + stmt = stmt.filter( + asset_v1_account_attachments::Column::AttachmentType + .eq(V1AccountAttachments::Edition) + // The data field is a JSON field that contains the edition data. + .and(asset_v1_account_attachments::Column::Data.is_not_null()) + // The parent field is a string field that contains the master edition pubkey ( mapping edition to master edition ) + .and(Expr::cust(&format!( + "data->>'parent' = '{}'", + master_edition_pubkey + ))), + ); + + let nft_editions = paginate( + pagination, + limit, + stmt, + Order::Asc, + asset_v1_account_attachments::Column::Id, + ) + .all(conn) + .await? + .into_iter() + .map(attachment_to_nft_edition) + .collect::, _>>()?; + + let (page, before, after, cursor) = match pagination { + Pagination::Keyset { before, after } => { + let bef = before.clone().and_then(|x| String::from_utf8(x).ok()); + let aft = after.clone().and_then(|x| String::from_utf8(x).ok()); + (None, bef, aft, None) + } + Pagination::Page { page } => (Some(*page as u32), None, None, None), + Pagination::Cursor(_) => { + if let Some(last_asset) = nft_editions.last() { + let cursor_str = bs58::encode(last_asset.edition_address.clone()).into_string(); + (None, None, None, Some(cursor_str)) + } else { + (None, None, None, None) + } + } + }; + + Ok(NftEditions { + total: nft_editions.len() as u32, + master_edition_address: master_edition_pubkey.to_string(), + supply: master_edition_data.supply, + max_supply: master_edition_data.max_supply, + editions: nft_editions, + limit: limit as u32, + page, + before, + after, + cursor, + }) +} diff --git a/digital_asset_types/src/rpc/response.rs b/digital_asset_types/src/rpc/response.rs index 55277e5d4..641f39640 100644 --- a/digital_asset_types/src/rpc/response.rs +++ b/digital_asset_types/src/rpc/response.rs @@ -67,3 +67,32 @@ pub struct TokenAccountList { pub cursor: Option, pub errors: Vec, } + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default, JsonSchema)] +#[serde(default)] + +pub struct NftEdition { + pub mint_address: String, + pub edition_address: String, + pub edition_number: u64, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default, JsonSchema)] +#[serde(default)] +pub struct NftEditions { + pub total: u32, + pub limit: u32, + pub master_edition_address: String, + pub supply: u64, + pub max_supply: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub editions: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub page: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub before: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub after: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cursor: Option, +} diff --git a/integration_tests/tests/data/accounts/get_nft_editions/4V9QuYLpiMu4ZQmhdEHmgATdgiHkDeJfvZi84BfkYcez b/integration_tests/tests/data/accounts/get_nft_editions/4V9QuYLpiMu4ZQmhdEHmgATdgiHkDeJfvZi84BfkYcez new file mode 100644 index 0000000000000000000000000000000000000000..558488282bb1232cbef1991e25eb372f8473ce9f GIT binary patch literal 824 zcmY#jfB*@G90nE!4+a$=H-NzfNJap00)qsQG(K_lWXjskoNd)X_I{Zze`gf+PCoH% z@fSt|aOz@1>11JugYT z_mSK!Q@-GfH$WJyZDv2)&3att&^>xigF>fF_FPIrYfk`M38G&KK_?MA+Ae}n2 G?FImpjDPq5 literal 0 HcmV?d00001 diff --git a/integration_tests/tests/data/accounts/get_nft_editions/8SHfqzJYABeGfiG1apwiEYt6TvfGQiL1pdwEjvTKsyiZ b/integration_tests/tests/data/accounts/get_nft_editions/8SHfqzJYABeGfiG1apwiEYt6TvfGQiL1pdwEjvTKsyiZ new file mode 100644 index 0000000000000000000000000000000000000000..10eae717ab2f9945cb62f4d2f931cc1b942ef8f3 GIT binary patch literal 424 zcmY#jfB*@G33~?>c+riUHLezOgX*(c4xf$Kky9+S;O|z?Z5Q`2|Il>P0GYWZ6i6~K z{D%Ttt)GG*{)e?&^d~cdSPTrynHU&Y_!DxbG_F>dc;G%0L#o8PlBlb^mp0Dyyd?47 zM{>7JRpR5a#EUx(tW9UOh(F|)+}kSf>A(Nt3!k|%18+~0sI;G>05m8#GpDpDGcivg zAhjqnKLunq3gANFodEJON=gcft@QO1i^@|I%To37QcLs;OHzzN(^9;%lXHvHb-i5O z^L*l?y!?v%i-QgFlZ}c>;){YptHMfsU9(Zl8%WLzW=2q85(-2{V3;ueV`Lskrw(nq E0r3NTfdBvi literal 0 HcmV?d00001 diff --git a/integration_tests/tests/data/accounts/get_nft_editions/9yQecKKYSHxez7fFjJkUvkz42TLmkoXzhyZxEf2pw8pz b/integration_tests/tests/data/accounts/get_nft_editions/9yQecKKYSHxez7fFjJkUvkz42TLmkoXzhyZxEf2pw8pz new file mode 100644 index 0000000000000000000000000000000000000000..75d190842d3f68f6177bc5db184233e9750c912a GIT binary patch literal 224 zcmY#jfB*@G30V6T085jUQ3Q-vV literal 0 HcmV?d00001 diff --git a/integration_tests/tests/data/accounts/get_nft_editions/AoxgzXKEsJmUyF5pBb3djn9cJFA26zh2SQHvd9EYijZV b/integration_tests/tests/data/accounts/get_nft_editions/AoxgzXKEsJmUyF5pBb3djn9cJFA26zh2SQHvd9EYijZV new file mode 100644 index 0000000000000000000000000000000000000000..31abb9fb66ffa18d1f0277f89d9b97146391fd2b GIT binary patch literal 384 zcmY#jfB*@G3vkVkY}&(Jkh<~l#Tr+Oxk2^WEQilV?8qsWTJU$P=eCP`n15)xfy^{8PzRC> z4F93PPV1*2i2rrn7X8VL3=AKEY{tAA-T1}tt9nnz+SKH*cxR_w`f>KSR{h%Aq(z-) Ma?E!D)eScQ0QjpyO#lD@ literal 0 HcmV?d00001 diff --git a/integration_tests/tests/data/accounts/get_nft_editions/Ey2Qb8kLctbchQsMnhZs5DjY32To2QtPuXNwWvk4NosL b/integration_tests/tests/data/accounts/get_nft_editions/Ey2Qb8kLctbchQsMnhZs5DjY32To2QtPuXNwWvk4NosL new file mode 100644 index 0000000000000000000000000000000000000000..cf8f217c81e16d808c5fa972c134551cf80544ce GIT binary patch literal 224 zcmY#jfB*@G3`7X8VL3=Ba)HjrY-tI>^L{JyI9bgWHH4vTko+NB?7k89Phtxa0g Sc_znv7f1~lFcPDhfdK%>SyNR2 literal 0 HcmV?d00001 diff --git a/integration_tests/tests/data/accounts/get_nft_editions/GJvFDcBWf6aDncd1TBzx2ou1rgLFYaMBdbYLBa9oTAEw b/integration_tests/tests/data/accounts/get_nft_editions/GJvFDcBWf6aDncd1TBzx2ou1rgLFYaMBdbYLBa9oTAEw new file mode 100644 index 0000000000000000000000000000000000000000..80ea336fbe2765a4003cf9286d1359b196ba6b4e GIT binary patch literal 224 zcmY#jfB*@G3C6`Khy0RzTLnJ-_g{SB zGgoHd?P(H~_H)?oetUR5b>ZZjryshVdtJFkW=rea_-ONqRy|)^>;5yW1euxOD+VMP z82&?noz_o55P#yjE&7ug85n|qY#_xj@xVRZ7eDqlPh7KV_QjGo& T?LMy9vb+~&A literal 0 HcmV?d00001 diff --git a/integration_tests/tests/data/accounts/get_nft_editions/giWoA4jqHFkodPJgtbRYRcYtiXbsVytnxnEao3QT2gg b/integration_tests/tests/data/accounts/get_nft_editions/giWoA4jqHFkodPJgtbRYRcYtiXbsVytnxnEao3QT2gg new file mode 100644 index 0000000000000000000000000000000000000000..a63f177f011d22c824c8e4997f9296abebb840b6 GIT binary patch literal 384 zcmY#jfB*@G3vvn_(rH@{pYrcQ-whe-O)Yyu zD&O-3EqXFP=^=MP>c+OCE6Q = seed_accounts([ + "Ey2Qb8kLctbchQsMnhZs5DjY32To2QtPuXNwWvk4NosL", + "9ZmY7qCaq7WbrR7RZdHWCNS9FrFRPwRqU84wzWfmqLDz", + "8SHfqzJYABeGfiG1apwiEYt6TvfGQiL1pdwEjvTKsyiZ", + "GJvFDcBWf6aDncd1TBzx2ou1rgLFYaMBdbYLBa9oTAEw", + "9ZmY7qCaq7WbrR7RZdHWCNS9FrFRPwRqU84wzWfmqLDz", + "AoxgzXKEsJmUyF5pBb3djn9cJFA26zh2SQHvd9EYijZV", + "9yQecKKYSHxez7fFjJkUvkz42TLmkoXzhyZxEf2pw8pz", + "4V9QuYLpiMu4ZQmhdEHmgATdgiHkDeJfvZi84BfkYcez", + "giWoA4jqHFkodPJgtbRYRcYtiXbsVytnxnEao3QT2gg", + ]); + + apply_migrations_and_delete_data(setup.db.clone()).await; + index_seed_events(&setup, seeds.iter().collect_vec()).await; + + let request = r#" + { + "mintAddress": "Ey2Qb8kLctbchQsMnhZs5DjY32To2QtPuXNwWvk4NosL", + "limit":10 + } + "#; + + let request: api::GetNftEditions = serde_json::from_str(request).unwrap(); + let response = setup.das_api.get_nft_editions(request).await.unwrap(); + + insta::assert_json_snapshot!(name, response); +} diff --git a/integration_tests/tests/integration_tests/snapshots/integration_tests__fungibles_and_token_extensions_tests__token_extensions_get_asset_scenario_1.snap b/integration_tests/tests/integration_tests/snapshots/integration_tests__fungibles_and_token_extensions_tests__token_extensions_get_asset_scenario_1.snap index edaf78071..52b0da861 100644 --- a/integration_tests/tests/integration_tests/snapshots/integration_tests__fungibles_and_token_extensions_tests__token_extensions_get_asset_scenario_1.snap +++ b/integration_tests/tests/integration_tests/snapshots/integration_tests__fungibles_and_token_extensions_tests__token_extensions_get_asset_scenario_1.snap @@ -1,7 +1,6 @@ --- source: integration_tests/tests/integration_tests/fungibles_and_token_extensions_tests.rs expression: response -snapshot_kind: text --- { "interface": "FungibleToken", @@ -56,7 +55,6 @@ snapshot_kind: text "additional_metadata": [] }, "metadata_pointer": { - "authority": "Em34oqDQYQZ9b6ycPHD28K47mttrRsdNu1S1pgK6NtPL", "metadata_address": "BPU5vrAHafRuVeK33CgfdwTKSsmC4p6t3aqyav3cFF7Y" } } diff --git a/integration_tests/tests/integration_tests/snapshots/integration_tests__fungibles_and_token_extensions_tests__token_extensions_get_asset_scenario_2.snap b/integration_tests/tests/integration_tests/snapshots/integration_tests__fungibles_and_token_extensions_tests__token_extensions_get_asset_scenario_2.snap index 3ac4fdcb9..31d33d4d2 100644 --- a/integration_tests/tests/integration_tests/snapshots/integration_tests__fungibles_and_token_extensions_tests__token_extensions_get_asset_scenario_2.snap +++ b/integration_tests/tests/integration_tests/snapshots/integration_tests__fungibles_and_token_extensions_tests__token_extensions_get_asset_scenario_2.snap @@ -1,7 +1,6 @@ --- source: integration_tests/tests/integration_tests/fungibles_and_token_extensions_tests.rs expression: response -snapshot_kind: text --- { "interface": "FungibleToken", @@ -60,7 +59,6 @@ snapshot_kind: text "program_id": null }, "metadata_pointer": { - "authority": "2apBGMsS6ti9RyF5TwQTDswXBWskiJP2LD4cUEDqYJjk", "metadata_address": "HVbpJAQGNpkgBaYBZQBR1t7yFdvaYVp2vCQQfKKEN4tM" }, "permanent_delegate": { diff --git a/integration_tests/tests/integration_tests/snapshots/integration_tests__fungibles_and_token_extensions_tests__token_extensions_get_asset_scenario_3.snap b/integration_tests/tests/integration_tests/snapshots/integration_tests__fungibles_and_token_extensions_tests__token_extensions_get_asset_scenario_3.snap index 3484f3ce6..75c59b24b 100644 --- a/integration_tests/tests/integration_tests/snapshots/integration_tests__fungibles_and_token_extensions_tests__token_extensions_get_asset_scenario_3.snap +++ b/integration_tests/tests/integration_tests/snapshots/integration_tests__fungibles_and_token_extensions_tests__token_extensions_get_asset_scenario_3.snap @@ -1,7 +1,6 @@ --- source: integration_tests/tests/integration_tests/fungibles_and_token_extensions_tests.rs expression: response -snapshot_kind: text --- { "interface": "FungibleToken", @@ -60,7 +59,6 @@ snapshot_kind: text "program_id": null }, "metadata_pointer": { - "authority": "9nEfZqzTP3dfVWmzQy54TzsZqSQqDFVW4PhXdG9vYCVD", "metadata_address": "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo" }, "permanent_delegate": { diff --git a/integration_tests/tests/integration_tests/snapshots/integration_tests__nft_editions_tests__get_nft_editions.snap b/integration_tests/tests/integration_tests/snapshots/integration_tests__nft_editions_tests__get_nft_editions.snap new file mode 100644 index 000000000..d67779b90 --- /dev/null +++ b/integration_tests/tests/integration_tests/snapshots/integration_tests__nft_editions_tests__get_nft_editions.snap @@ -0,0 +1,24 @@ +--- +source: integration_tests/tests/integration_tests/nft_editions_tests.rs +expression: response +snapshot_kind: text +--- +{ + "total": 2, + "limit": 10, + "master_edition_address": "8SHfqzJYABeGfiG1apwiEYt6TvfGQiL1pdwEjvTKsyiZ", + "supply": 60, + "max_supply": 69, + "editions": [ + { + "mint_address": "GJvFDcBWf6aDncd1TBzx2ou1rgLFYaMBdbYLBa9oTAEw", + "edition_address": "AoxgzXKEsJmUyF5pBb3djn9cJFA26zh2SQHvd9EYijZV", + "edition_number": 1 + }, + { + "mint_address": "9yQecKKYSHxez7fFjJkUvkz42TLmkoXzhyZxEf2pw8pz", + "edition_address": "giWoA4jqHFkodPJgtbRYRcYtiXbsVytnxnEao3QT2gg", + "edition_number": 2 + } + ] +} diff --git a/program_transformers/src/token_metadata/master_edition.rs b/program_transformers/src/token_metadata/master_edition.rs index 791368af6..73eeb1fcc 100644 --- a/program_transformers/src/token_metadata/master_edition.rs +++ b/program_transformers/src/token_metadata/master_edition.rs @@ -1,17 +1,15 @@ use { crate::error::{ProgramTransformerError, ProgramTransformerResult}, blockbuster::token_metadata::{ - accounts::{DeprecatedMasterEditionV1, MasterEdition}, + accounts::{DeprecatedMasterEditionV1, Edition, MasterEdition}, types::Key, }, digital_asset_types::dao::{ - asset, asset_v1_account_attachments, extensions, - sea_orm_active_enums::{SpecificationAssetClass, V1AccountAttachments}, + asset_v1_account_attachments, sea_orm_active_enums::V1AccountAttachments, }, sea_orm::{ - entity::{ActiveModelTrait, ActiveValue, EntityTrait, RelationTrait}, - prelude::*, - query::{JoinType, QuerySelect, QueryTrait}, + entity::{ActiveValue, EntityTrait}, + query::QueryTrait, sea_query::query::OnConflict, ConnectionTrait, DatabaseTransaction, DbBackend, }, @@ -65,15 +63,7 @@ pub async fn save_master_edition( txn: &DatabaseTransaction, ) -> ProgramTransformerResult<()> { let id_bytes = id.to_bytes().to_vec(); - let master_edition: Option<(asset_v1_account_attachments::Model, Option)> = - asset_v1_account_attachments::Entity::find_by_id(id.to_bytes().to_vec()) - .find_also_related(asset::Entity) - .join( - JoinType::InnerJoin, - extensions::asset::Relation::AssetData.def(), - ) - .one(txn) - .await?; + let ser = serde_json::to_value(me_data) .map_err(|e| ProgramTransformerError::SerializatonError(e.to_string()))?; @@ -85,14 +75,47 @@ pub async fn save_master_edition( ..Default::default() }; - if let Some((_me, Some(asset))) = master_edition { - let mut updatable: asset::ActiveModel = asset.into(); - updatable.supply = ActiveValue::Set(Decimal::from(1)); - updatable.specification_asset_class = ActiveValue::Set(Some(SpecificationAssetClass::Nft)); - updatable.update(txn).await?; - } + 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::AttachmentType, + 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 + ); + + txn.execute(query).await?; + Ok(()) +} - let query = asset_v1_account_attachments::Entity::insert(model) +pub async fn save_edition( + id: Pubkey, + slot: u64, + e_data: &Edition, + txn: &DatabaseTransaction, +) -> ProgramTransformerResult<()> { + let id_bytes = id.to_bytes().to_vec(); + + let ser = serde_json::to_value(e_data) + .map_err(|e| ProgramTransformerError::SerializatonError(e.to_string()))?; + + let model = asset_v1_account_attachments::ActiveModel { + id: ActiveValue::Set(id_bytes), + attachment_type: ActiveValue::Set(V1AccountAttachments::Edition), + data: ActiveValue::Set(Some(ser)), + slot_updated: ActiveValue::Set(slot as i64), + ..Default::default() + }; + + let mut query = asset_v1_account_attachments::Entity::insert(model) .on_conflict( OnConflict::columns([asset_v1_account_attachments::Column::Id]) .update_columns([ @@ -103,6 +126,12 @@ pub async fn save_master_edition( .to_owned(), ) .build(DbBackend::Postgres); + + query.sql = format!( + "{} WHERE excluded.slot_updated >= asset_v1_account_attachments.slot_updated", + query.sql + ); + txn.execute(query).await?; Ok(()) } diff --git a/program_transformers/src/token_metadata/mod.rs b/program_transformers/src/token_metadata/mod.rs index cbeb94171..0080303ed 100644 --- a/program_transformers/src/token_metadata/mod.rs +++ b/program_transformers/src/token_metadata/mod.rs @@ -7,7 +7,11 @@ use { }, AccountInfo, DownloadMetadataNotifier, }, - blockbuster::programs::token_metadata::{TokenMetadataAccountData, TokenMetadataAccountState}, + blockbuster::{ + programs::token_metadata::{TokenMetadataAccountData, TokenMetadataAccountState}, + token_metadata::types::TokenStandard, + }, + master_edition::save_edition, sea_orm::{DatabaseConnection, TransactionTrait}, }; @@ -45,9 +49,32 @@ pub async fn handle_token_metadata_account<'a, 'b>( txn.commit().await?; Ok(()) } + TokenMetadataAccountData::EditionV1(e) => { + let txn = db.begin().await?; + save_edition(account_info.pubkey, account_info.slot, e, &txn).await?; + txn.commit().await?; + Ok(()) + } + // TokenMetadataAccountData::EditionMarker(_) => {} // TokenMetadataAccountData::UseAuthorityRecord(_) => {} // TokenMetadataAccountData::CollectionAuthorityRecord(_) => {} _ => Err(ProgramTransformerError::NotImplemented), } } + +pub trait IsNonFungibe { + fn is_non_fungible(&self) -> bool; +} + +impl IsNonFungibe for TokenStandard { + fn is_non_fungible(&self) -> bool { + matches!( + self, + TokenStandard::NonFungible + | TokenStandard::NonFungibleEdition + | TokenStandard::ProgrammableNonFungible + | TokenStandard::ProgrammableNonFungibleEdition + ) + } +} diff --git a/program_transformers/src/token_metadata/v1_asset.rs b/program_transformers/src/token_metadata/v1_asset.rs index dd40df218..70634f5bc 100644 --- a/program_transformers/src/token_metadata/v1_asset.rs +++ b/program_transformers/src/token_metadata/v1_asset.rs @@ -1,4 +1,5 @@ use { + super::IsNonFungibe, crate::{ asset_upserts::{ upsert_assets_metadata_account_columns, upsert_assets_mint_account_columns, @@ -32,6 +33,7 @@ use { ConnectionTrait, DbBackend, DbErr, TransactionTrait, }, solana_sdk::pubkey, + solana_sdk::pubkey::Pubkey, sqlx::types::Decimal, tracing::warn, }; @@ -408,6 +410,11 @@ pub async fn save_v1_asset( } txn.commit().await?; + // If the asset is a non-fungible token, then we need to insert to the asset_v1_account_attachments table + if let Some(true) = metadata.token_standard.map(|t| t.is_non_fungible()) { + upsert_asset_v1_account_attachments(conn, &mint_pubkey, slot).await?; + } + if uri.is_empty() { warn!( "URI is empty for mint {}. Skipping background task.", @@ -418,3 +425,31 @@ pub async fn save_v1_asset( Ok(Some(DownloadMetadataInfo::new(mint_pubkey_vec, uri))) } + +async fn upsert_asset_v1_account_attachments( + conn: &T, + mint_pubkey: &Pubkey, + slot: u64, +) -> ProgramTransformerResult<()> { + let edition_pubkey = MasterEdition::find_pda(mint_pubkey).0; + let mint_pubkey_vec = mint_pubkey.to_bytes().to_vec(); + let attachment = asset_v1_account_attachments::ActiveModel { + id: ActiveValue::Set(edition_pubkey.to_bytes().to_vec()), + asset_id: ActiveValue::Set(Some(mint_pubkey_vec.clone())), + slot_updated: ActiveValue::Set(slot as i64), + // by default, the attachment type is MasterEditionV2 + attachment_type: ActiveValue::Set(V1AccountAttachments::MasterEditionV2), + ..Default::default() + }; + let query = asset_v1_account_attachments::Entity::insert(attachment) + .on_conflict( + OnConflict::columns([asset_v1_account_attachments::Column::Id]) + .update_columns([asset_v1_account_attachments::Column::AssetId]) + .to_owned(), + ) + .build(DbBackend::Postgres); + + conn.execute(query).await?; + + Ok(()) +}