Skip to content

Commit

Permalink
Query NFT Editions (metaplex-foundation#216)
Browse files Browse the repository at this point in the history
Associate editions to the nft mint of the master edition. Add new method
to API get_nft_editions to find editions for the mint.
  • Loading branch information
Nagaprasadvr authored Jan 7, 2025
1 parent e8aaded commit 9654780
Show file tree
Hide file tree
Showing 25 changed files with 401 additions and 40 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion blockbuster/src/programs/token_extensions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
35 changes: 31 additions & 4 deletions das_api/src/api/api_impl.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -501,6 +501,7 @@ impl ApiContract for DasApi {
.await
.map_err(Into::into)
}

async fn get_grouping(
self: &DasApi,
payload: GetGrouping,
Expand Down Expand Up @@ -545,4 +546,30 @@ impl ApiContract for DasApi {
.await
.map_err(Into::into)
}

async fn get_nft_editions(
self: &DasApi,
payload: GetNftEditions,
) -> Result<NftEditions, DasApiError> {
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)
}
}
22 changes: 21 additions & 1 deletion das_api/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<u32>,
pub limit: Option<u32>,
pub before: Option<String>,
pub after: Option<String>,
#[serde(default)]
pub cursor: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct GetAssetSignatures {
Expand Down Expand Up @@ -276,4 +290,10 @@ pub trait ApiContract: Send + Sync + 'static {
&self,
payload: GetTokenAccounts,
) -> Result<TokenAccountList, DasApiError>;
#[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<NftEditions, DasApiError>;
}
10 changes: 9 additions & 1 deletion das_api/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<GetNftEditions>()?;
rpc_context
.get_nft_editions(payload)
.await
.map_err(Into::into)
})?;
module.register_alias("getNftEditions", "get_nft_editions")?;

Ok(module)
}
}
1 change: 1 addition & 0 deletions digital_asset_types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
124 changes: 120 additions & 4 deletions digital_asset_types/src/dao/scopes/asset.rs
Original file line number Diff line number Diff line change
@@ -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<T, C>(
Expand Down Expand Up @@ -595,3 +604,110 @@ pub async fn get_token_accounts(

Ok(token_accounts)
}

pub fn get_edition_data_from_json<T: DeserializeOwned>(data: Value) -> Result<T, DbErr> {
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<NftEdition, DbErr> {
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<NftEditions, DbErr> {
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::<Result<Vec<NftEdition>, _>>()?;

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,
})
}
29 changes: 29 additions & 0 deletions digital_asset_types/src/rpc/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,32 @@ pub struct TokenAccountList {
pub cursor: Option<String>,
pub errors: Vec<DasError>,
}

#[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<u64>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub editions: Vec<NftEdition>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions integration_tests/tests/integration_tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod common;
mod fungibles_and_token_extensions_tests;
mod general_scenario_tests;
mod mpl_core_tests;
mod nft_editions_tests;
mod regular_nft_tests;
mod test_show_zero_balance_filter;
mod token_accounts_tests;
50 changes: 50 additions & 0 deletions integration_tests/tests/integration_tests/nft_editions_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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_nft_editions() {
let name = trim_test_name(function_name!());
let setup = TestSetup::new_with_options(
name.clone(),
TestSetupOptions {
network: Some(Network::Mainnet),
},
)
.await;

let seeds: Vec<SeedEvent> = 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);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
---
source: integration_tests/tests/integration_tests/fungibles_and_token_extensions_tests.rs
expression: response
snapshot_kind: text
---
{
"interface": "FungibleToken",
Expand Down Expand Up @@ -56,7 +55,6 @@ snapshot_kind: text
"additional_metadata": []
},
"metadata_pointer": {
"authority": "Em34oqDQYQZ9b6ycPHD28K47mttrRsdNu1S1pgK6NtPL",
"metadata_address": "BPU5vrAHafRuVeK33CgfdwTKSsmC4p6t3aqyav3cFF7Y"
}
}
Expand Down
Loading

0 comments on commit 9654780

Please sign in to comment.