diff --git a/das_api/src/api/api_impl.rs b/das_api/src/api/api_impl.rs index 5b98c1340..3952e6d6a 100644 --- a/das_api/src/api/api_impl.rs +++ b/das_api/src/api/api_impl.rs @@ -4,19 +4,22 @@ use digital_asset_types::{ sea_orm_active_enums::{ OwnerType, RoyaltyTargetType, SpecificationAssetClass, SpecificationVersions, }, - SearchAssetsQuery, + Cursor, PageOptions, SearchAssetsQuery, }, dapi::{ - get_asset, get_assets_by_authority, get_assets_by_creator, get_assets_by_group, - get_assets_by_owner, get_proof_for_asset, search_assets, + get_asset, get_asset_proofs, get_assets, get_assets_by_authority, get_assets_by_creator, + get_assets_by_group, get_assets_by_owner, get_proof_for_asset, search_assets, + }, + rpc::{ + filter::{AssetSortBy, SearchConditionType}, + response::GetGroupingResponse, }, - rpc::{filter::SearchConditionType, response::GetGroupingResponse}, rpc::{OwnershipModel, RoyaltyModel}, }; use open_rpc_derive::document_rpc; use sea_orm::{sea_query::ConditionType, ConnectionTrait, DbBackend, Statement}; -use crate::validation::validate_opt_pubkey; +use crate::validation::{validate_opt_pubkey, validate_search_with_name}; use open_rpc_schema::document::OpenrpcDocument; use { crate::api::*, @@ -46,21 +49,37 @@ impl DasApi { }) } + fn get_cursor(&self, cursor: &Option) -> Result { + match cursor { + Some(cursor_b64) => { + let cursor_vec = bs58::decode(cursor_b64) + .into_vec() + .map_err(|_| DasApiError::CursorValidationError(cursor_b64.clone()))?; + let cursor_struct = Cursor { + id: Some(cursor_vec), + }; + Ok(cursor_struct) + } + None => Ok(Cursor::default()), + } + } + fn validate_pagination( &self, limit: &Option, page: &Option, before: &Option, after: &Option, - ) -> Result<(), DasApiError> { - if page.is_none() && before.is_none() && after.is_none() { - return Err(DasApiError::PaginationEmptyError); - } + cursor: &Option, + sorting: &Option<&AssetSorting>, + ) -> Result { + let mut is_cursor_enabled = true; + let mut page_opt = PageOptions::default(); if let Some(limit) = limit { // make config item if *limit > 1000 { - return Err(DasApiError::PaginationError); + return Err(DasApiError::PaginationExceededError); } } @@ -70,20 +89,57 @@ impl DasApi { } // make config item - if before.is_some() || after.is_some() { + if before.is_some() || after.is_some() || cursor.is_some() { return Err(DasApiError::PaginationError); } + + is_cursor_enabled = false; } if let Some(before) = before { + if cursor.is_some() { + return Err(DasApiError::PaginationError); + } + if let Some(sort) = &sorting { + if sort.sort_by != AssetSortBy::Id { + return Err(DasApiError::PaginationSortingValidationError); + } + } validate_pubkey(before.clone())?; + is_cursor_enabled = false; } if let Some(after) = after { + if cursor.is_some() { + return Err(DasApiError::PaginationError); + } + if let Some(sort) = &sorting { + if sort.sort_by != AssetSortBy::Id { + return Err(DasApiError::PaginationSortingValidationError); + } + } validate_pubkey(after.clone())?; + is_cursor_enabled = false; } - Ok(()) + page_opt.limit = limit.map(|x| x as u64).unwrap_or(1000); + if is_cursor_enabled { + if let Some(sort) = &sorting { + if sort.sort_by != AssetSortBy::Id { + return Err(DasApiError::PaginationSortingValidationError); + } + page_opt.cursor = Some(self.get_cursor(&cursor)?); + } + } else { + page_opt.page = page.map(|x| x as u64); + page_opt.before = before + .clone() + .map(|x| bs58::decode(x).into_vec().unwrap_or_default()); + page_opt.after = after + .clone() + .map(|x| bs58::decode(x).into_vec().unwrap_or_default()); + } + Ok(page_opt) } } @@ -121,14 +177,70 @@ impl ApiContract for DasApi { .map_err(Into::into) } + async fn get_asset_proofs( + self: &DasApi, + payload: GetAssetProofs, + ) -> Result>, DasApiError> { + let GetAssetProofs { ids } = payload; + + let batch_size = ids.len(); + if batch_size > 1000 { + return Err(DasApiError::BatchSizeExceededError); + } + + let id_bytes = ids + .iter() + .map(|id| validate_pubkey(id.clone()).map(|id| id.to_bytes().to_vec())) + .collect::>, _>>()?; + + let proofs = get_asset_proofs(&self.db_connection, id_bytes).await?; + + let result: HashMap> = ids + .iter() + .map(|id| (id.clone(), proofs.get(id).cloned())) + .collect(); + Ok(result) + } + async fn get_asset(self: &DasApi, payload: GetAsset) -> Result { - let id = validate_pubkey(payload.id.clone())?; - let id_bytes = id.to_bytes().to_vec(); - get_asset(&self.db_connection, id_bytes) + let GetAsset { id, options } = payload; + let id_bytes = validate_pubkey(id.clone())?.to_bytes().to_vec(); + let options = options.unwrap_or_default(); + get_asset(&self.db_connection, id_bytes, &options.into()) .await .map_err(Into::into) } + async fn get_assets( + self: &DasApi, + payload: GetAssets, + ) -> Result>, DasApiError> { + let GetAssets { ids, options } = payload; + + let batch_size = ids.len(); + if batch_size > 1000 { + return Err(DasApiError::BatchSizeExceededError); + } + + let id_bytes = ids + .iter() + .map(|id| validate_pubkey(id.clone()).map(|id| id.to_bytes().to_vec())) + .collect::>, _>>()?; + + let options = options.unwrap_or_default(); + + let assets = get_assets( + &self.db_connection, + id_bytes, + batch_size as u64, + &options.into(), + ) + .await?; + + let result: Vec> = ids.iter().map(|id| assets.get(id).cloned()).collect(); + Ok(result) + } + async fn get_assets_by_owner( self: &DasApi, payload: GetAssetsByOwner, @@ -140,21 +252,23 @@ impl ApiContract for DasApi { page, before, after, + options, + cursor, } = payload; let before: Option = before.filter(|before| !before.is_empty()); let after: Option = after.filter(|after| !after.is_empty()); let owner_address = validate_pubkey(owner_address.clone())?; let owner_address_bytes = owner_address.to_bytes().to_vec(); let sort_by = sort_by.unwrap_or_default(); - self.validate_pagination(&limit, &page, &before, &after)?; + let options = options.unwrap_or_default(); + let page_options = + self.validate_pagination(&limit, &page, &before, &after, &cursor, &Some(&sort_by))?; get_assets_by_owner( &self.db_connection, owner_address_bytes, sort_by, - limit.map(|x| x as u64).unwrap_or(1000), - page.map(|x| x as u64), - before.map(|x| bs58::decode(x).into_vec().unwrap_or_default()), - after.map(|x| bs58::decode(x).into_vec().unwrap_or_default()), + &page_options, + &options, ) .await .map_err(Into::into) @@ -172,20 +286,22 @@ impl ApiContract for DasApi { page, before, after, + options, + cursor, } = payload; let before: Option = before.filter(|before| !before.is_empty()); let after: Option = after.filter(|after| !after.is_empty()); let sort_by = sort_by.unwrap_or_default(); - self.validate_pagination(&limit, &page, &before, &after)?; + let options = options.unwrap_or_default(); + let page_options = + self.validate_pagination(&limit, &page, &before, &after, &cursor, &Some(&sort_by))?; get_assets_by_group( &self.db_connection, group_key, group_value, sort_by, - limit.map(|x| x as u64).unwrap_or(1000), - page.map(|x| x as u64), - before.map(|x| bs58::decode(x).into_vec().unwrap_or_default()), - after.map(|x| bs58::decode(x).into_vec().unwrap_or_default()), + &page_options, + &options, ) .await .map_err(Into::into) @@ -203,22 +319,24 @@ impl ApiContract for DasApi { page, before, after, + options, + cursor, } = payload; let creator_address = validate_pubkey(creator_address.clone())?; let creator_address_bytes = creator_address.to_bytes().to_vec(); - self.validate_pagination(&limit, &page, &before, &after)?; let sort_by = sort_by.unwrap_or_default(); + let page_options = + self.validate_pagination(&limit, &page, &before, &after, &cursor, &Some(&sort_by))?; let only_verified = only_verified.unwrap_or_default(); + let options = options.unwrap_or_default(); get_assets_by_creator( &self.db_connection, creator_address_bytes, only_verified, sort_by, - limit.map(|x| x as u64).unwrap_or(1000), - page.map(|x| x as u64), - before.map(|x| bs58::decode(x).into_vec().unwrap_or_default()), - after.map(|x| bs58::decode(x).into_vec().unwrap_or_default()), + &page_options, + &options, ) .await .map_err(Into::into) @@ -235,20 +353,22 @@ impl ApiContract for DasApi { page, before, after, + options, + cursor, } = payload; let sort_by = sort_by.unwrap_or_default(); let authority_address = validate_pubkey(authority_address.clone())?; let authority_address_bytes = authority_address.to_bytes().to_vec(); + let options = options.unwrap_or_default(); - self.validate_pagination(&limit, &page, &before, &after)?; + let page_options = + self.validate_pagination(&limit, &page, &before, &after, &cursor, &Some(&sort_by))?; get_assets_by_authority( &self.db_connection, authority_address_bytes, sort_by, - limit.map(|x| x as u64).unwrap_or(1000), - page.map(|x| x as u64), - before.map(|x| bs58::decode(x).into_vec().unwrap_or_default()), - after.map(|x| bs58::decode(x).into_vec().unwrap_or_default()), + &page_options, + &options, ) .await .map_err(Into::into) @@ -282,9 +402,12 @@ impl ApiContract for DasApi { before, after, json_uri, + options, + cursor, + name, } = payload; + // Deserialize search assets query - self.validate_pagination(&limit, &page, &before, &after)?; let spec: Option<(SpecificationVersions, SpecificationAssetClass)> = interface.map(|x| x.into()); let specification_version = spec.clone().map(|x| x.0); @@ -294,6 +417,7 @@ impl ApiContract for DasApi { SearchConditionType::All => ConditionType::All, }); let owner_address = validate_opt_pubkey(&owner_address)?; + let name = validate_search_with_name(&name, &owner_address)?; let creator_address = validate_opt_pubkey(&creator_address)?; let delegate = validate_opt_pubkey(&delegate)?; @@ -332,20 +456,16 @@ impl ApiContract for DasApi { royalty_amount, burnt, json_uri, + name, }; + let options = options.unwrap_or_default(); let sort_by = sort_by.unwrap_or_default(); + let page_options = + self.validate_pagination(&limit, &page, &before, &after, &cursor, &Some(&sort_by))?; // Execute query - search_assets( - &self.db_connection, - saq, - sort_by, - limit.map(|x| x as u64).unwrap_or(1000), - page.map(|x| x as u64), - before.map(|x| bs58::decode(x).into_vec().unwrap_or_default()), - after.map(|x| bs58::decode(x).into_vec().unwrap_or_default()), - ) - .await - .map_err(Into::into) + search_assets(&self.db_connection, saq, sort_by, &page_options, &options) + .await + .map_err(Into::into) } async fn get_grouping( diff --git a/das_api/src/api/mod.rs b/das_api/src/api/mod.rs index 788c2adf2..a2db05d66 100644 --- a/das_api/src/api/mod.rs +++ b/das_api/src/api/mod.rs @@ -1,12 +1,14 @@ use crate::DasApiError; use async_trait::async_trait; use digital_asset_types::rpc::filter::SearchConditionType; +use digital_asset_types::rpc::options::Options; use digital_asset_types::rpc::response::AssetList; 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}; use open_rpc_schema::schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; mod api_impl; pub use api_impl::*; @@ -21,6 +23,10 @@ pub struct GetAssetsByGroup { pub page: Option, pub before: Option, pub after: Option, + #[serde(default, alias = "displayOptions")] + pub options: Option, + #[serde(default)] + pub cursor: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] @@ -32,12 +38,26 @@ pub struct GetAssetsByOwner { pub page: Option, pub before: Option, pub after: Option, + #[serde(default, alias = "displayOptions")] + pub options: Option, + #[serde(default)] + pub cursor: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields, rename_all = "camelCase")] pub struct GetAsset { pub id: String, + #[serde(default, alias = "displayOptions")] + pub options: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +pub struct GetAssets { + pub ids: Vec, + #[serde(default, alias = "displayOptions")] + pub options: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] @@ -46,6 +66,12 @@ pub struct GetAssetProof { pub id: String, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +pub struct GetAssetProofs { + pub ids: Vec, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields, rename_all = "camelCase")] pub struct GetAssetsByCreator { @@ -56,6 +82,10 @@ pub struct GetAssetsByCreator { pub page: Option, pub before: Option, pub after: Option, + #[serde(default, alias = "displayOptions")] + pub options: Option, + #[serde(default)] + pub cursor: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] @@ -87,6 +117,12 @@ pub struct SearchAssets { pub after: Option, #[serde(default)] pub json_uri: Option, + #[serde(default, alias = "displayOptions")] + pub options: Option, + #[serde(default)] + pub cursor: Option, + #[serde(default)] + pub name: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] @@ -98,6 +134,10 @@ pub struct GetAssetsByAuthority { pub page: Option, pub before: Option, pub after: Option, + #[serde(default, alias = "displayOptions")] + pub options: Option, + #[serde(default)] + pub cursor: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] @@ -117,12 +157,27 @@ pub trait ApiContract: Send + Sync + 'static { summary = "Get a merkle proof for a compressed asset by its ID" )] async fn get_asset_proof(&self, payload: GetAssetProof) -> Result; + #[rpc( + name = "getAssetProofs", + params = "named", + summary = "Get merkle proofs for compressed assets by their IDs" + )] + async fn get_asset_proofs( + &self, + payload: GetAssetProofs, + ) -> Result>, DasApiError>; #[rpc( name = "getAsset", params = "named", summary = "Get an asset by its ID" )] async fn get_asset(&self, payload: GetAsset) -> Result; + #[rpc( + name = "getAssets", + params = "named", + summary = "Get assets by their IDs" + )] + async fn get_assets(&self, payload: GetAssets) -> Result>, DasApiError>; #[rpc( name = "getAssetsByOwner", params = "named", diff --git a/das_api/src/builder.rs b/das_api/src/builder.rs index 342d2fba3..5d0005e36 100644 --- a/das_api/src/builder.rs +++ b/das_api/src/builder.rs @@ -22,12 +22,31 @@ impl RpcApiBuilder { })?; module.register_alias("getAssetProof", "get_asset_proof")?; + module.register_async_method("get_asset_proofs", |rpc_params, rpc_context| async move { + let payload = rpc_params.parse::()?; + rpc_context + .get_asset_proofs(payload) + .await + .map_err(Into::into) + })?; + module.register_alias("getAssetProofs", "get_asset_proofs")?; + module.register_alias("get_asset_proof_batch", "get_asset_proofs")?; + module.register_alias("getAssetProofBatch", "get_asset_proofs")?; + module.register_async_method("get_asset", |rpc_params, rpc_context| async move { let payload = rpc_params.parse::()?; rpc_context.get_asset(payload).await.map_err(Into::into) })?; module.register_alias("getAsset", "get_asset")?; + module.register_async_method("get_assets", |rpc_params, rpc_context| async move { + let payload = rpc_params.parse::()?; + rpc_context.get_assets(payload).await.map_err(Into::into) + })?; + module.register_alias("getAssets", "get_assets")?; + module.register_alias("get_asset_batch", "get_assets")?; + module.register_alias("getAssetBatch", "get_assets")?; + module.register_async_method( "get_assets_by_owner", |rpc_params, rpc_context| async move { diff --git a/das_api/src/error.rs b/das_api/src/error.rs index 3640da765..4b009ceac 100644 --- a/das_api/src/error.rs +++ b/das_api/src/error.rs @@ -22,6 +22,14 @@ pub enum DasApiError { PaginationEmptyError, #[error("Deserialization error: {0}")] DeserializationError(#[from] serde_json::Error), + #[error("Batch Size Error. Batch size should not be greater than 1000.")] + BatchSizeExceededError, + #[error("Pagination Error. Limit should not be greater than 1000.")] + PaginationExceededError, + #[error("Cursor Validation Err: {0} is invalid")] + CursorValidationError(String), + #[error("Pagination Sorting Error. Only sorting based on id is support for this pagination")] + PaginationSortingValidationError, } impl Into for DasApiError { diff --git a/das_api/src/validation.rs b/das_api/src/validation.rs index b3377a7b4..394463208 100644 --- a/das_api/src/validation.rs +++ b/das_api/src/validation.rs @@ -6,6 +6,23 @@ pub fn validate_pubkey(str_pubkey: String) -> Result { Pubkey::from_str(&str_pubkey).map_err(|_| DasApiError::PubkeyValidationError(str_pubkey)) } +pub fn validate_search_with_name( + name: &Option, + owner: &Option>, +) -> Result>, DasApiError> { + let opt_name = if let Some(n) = name { + if owner.is_none() { + return Err(DasApiError::ValidationError( + "Owner address must be provided in order to search assets by name".to_owned(), + )); + } + Some(n.clone().into_bytes()) + } else { + None + }; + Ok(opt_name) +} + pub fn validate_opt_pubkey(pubkey: &Option) -> Result>, DasApiError> { let opt_bytes = if let Some(pubkey) = pubkey { let pubkey = Pubkey::from_str(pubkey) diff --git a/digital_asset_types/src/dao/mod.rs b/digital_asset_types/src/dao/mod.rs index e30c92755..2622c3659 100644 --- a/digital_asset_types/src/dao/mod.rs +++ b/digital_asset_types/src/dao/mod.rs @@ -2,23 +2,37 @@ mod full_asset; mod generated; pub mod scopes; -pub use full_asset::*; -pub use generated::*; - use self::sea_orm_active_enums::{ OwnerType, RoyaltyTargetType, SpecificationAssetClass, SpecificationVersions, }; +pub use full_asset::*; +pub use generated::*; use sea_orm::{ entity::*, sea_query::Expr, - sea_query::{ConditionType, IntoCondition}, + sea_query::{ConditionType, IntoCondition, SimpleExpr}, Condition, DbErr, RelationDef, }; +use serde::{Deserialize, Serialize}; pub struct GroupingSize { pub size: u64, } +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct PageOptions { + pub limit: u64, + pub page: Option, + pub before: Option>, + pub after: Option>, + pub cursor: Option, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +pub struct Cursor { + pub id: Option>, +} + pub enum Pagination { Keyset { before: Option>, @@ -27,6 +41,7 @@ pub enum Pagination { Page { page: u64, }, + Cursor(Cursor), } #[derive(Debug, Clone, PartialEq)] @@ -54,6 +69,7 @@ pub struct SearchAssetsQuery { pub royalty_amount: Option, pub burnt: Option, pub json_uri: Option, + pub name: Option>, } impl SearchAssetsQuery { @@ -115,6 +131,9 @@ impl SearchAssetsQuery { if self.json_uri.is_some() { num_conditions += 1; } + if self.name.is_some() { + num_conditions += 1; + } num_conditions } @@ -245,6 +264,28 @@ impl SearchAssetsQuery { joins.push(rel); } + if let Some(n) = self.name.to_owned() { + let name_as_str = std::str::from_utf8(&n).map_err(|_| { + DbErr::Custom( + "Could not convert raw name bytes into string for comparison".to_owned(), + ) + })?; + + let name_expr = + SimpleExpr::Custom(format!("chain_data->>'name' LIKE '%{}%'", name_as_str).into()); + + conditions = conditions.add(name_expr); + let rel = asset_data::Relation::Asset + .def() + .rev() + .on_condition(|left, right| { + Expr::tbl(right, asset_data::Column::Id) + .eq(Expr::tbl(left, asset::Column::AssetData)) + .into_condition() + }); + joins.push(rel); + } + Ok(( match self.negate { None | Some(false) => conditions, diff --git a/digital_asset_types/src/dao/scopes/asset.rs b/digital_asset_types/src/dao/scopes/asset.rs index cd9db02ba..6f51ea8b9 100644 --- a/digital_asset_types/src/dao/scopes/asset.rs +++ b/digital_asset_types/src/dao/scopes/asset.rs @@ -1,29 +1,32 @@ -use crate::{ - dao::{ - asset::{self, Entity}, - asset_authority, asset_creators, asset_data, asset_grouping, FullAsset, - GroupingSize, Pagination, - }, - dapi::common::safe_select, - rpc::{response::AssetList}, +use crate::dao::{ + asset::{self}, + asset_authority, asset_creators, asset_data, asset_grouping, Cursor, FullAsset, GroupingSize, + Pagination, }; use indexmap::IndexMap; use sea_orm::{entity::*, query::*, ConnectionTrait, DbErr, Order}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; -pub fn paginate<'db, T>(pagination: &Pagination, limit: u64, stmt: T) -> T +pub fn paginate<'db, T, C>( + pagination: &Pagination, + limit: u64, + stmt: T, + sort_direction: Order, + column: C, +) -> T where T: QueryFilter + QuerySelect, + C: ColumnTrait, { let mut stmt = stmt; match pagination { Pagination::Keyset { before, after } => { if let Some(b) = before { - stmt = stmt.filter(asset::Column::Id.lt(b.clone())); + stmt = stmt.filter(column.lt(b.clone())); } if let Some(a) = after { - stmt = stmt.filter(asset::Column::Id.gt(a.clone())); + stmt = stmt.filter(column.gt(a.clone())); } } Pagination::Page { page } => { @@ -31,6 +34,15 @@ where stmt = stmt.offset((page - 1) * limit) } } + Pagination::Cursor(cursor) => { + if *cursor != Cursor::default() { + if sort_direction == sea_orm::Order::Asc { + stmt = stmt.filter(column.gt(cursor.id.clone())); + } else { + stmt = stmt.filter(column.lt(cursor.id.clone())); + } + } + } } stmt.limit(limit) } @@ -43,6 +55,7 @@ pub async fn get_by_creator( sort_direction: Order, pagination: &Pagination, limit: u64, + show_unverified_collections: bool, ) -> Result, DbErr> { let mut condition = Condition::all() .add(asset_creators::Column::Creator.eq(creator)) @@ -58,6 +71,7 @@ pub async fn get_by_creator( sort_direction, pagination, limit, + show_unverified_collections, ) .await } @@ -91,15 +105,20 @@ pub async fn get_by_grouping( sort_direction: Order, pagination: &Pagination, limit: u64, + show_unverified_collections: bool, ) -> Result, DbErr> { - let condition = asset_grouping::Column::GroupKey + let mut condition = asset_grouping::Column::GroupKey .eq(group_key) - .and(asset_grouping::Column::GroupValue.eq(group_value)) - .and( + .and(asset_grouping::Column::GroupValue.eq(group_value)); + + if !show_unverified_collections { + condition = condition.and( asset_grouping::Column::Verified .eq(true) .or(asset_grouping::Column::Verified.is_null()), ); + } + get_by_related_condition( conn, Condition::all() @@ -110,6 +129,7 @@ pub async fn get_by_grouping( sort_direction, pagination, limit, + show_unverified_collections, ) .await } @@ -121,6 +141,7 @@ pub async fn get_assets_by_owner( sort_direction: Order, pagination: &Pagination, limit: u64, + show_unverified_collections: bool, ) -> Result, DbErr> { let cond = Condition::all() .add(asset::Column::Owner.eq(owner)) @@ -133,6 +154,30 @@ pub async fn get_assets_by_owner( sort_direction, pagination, limit, + show_unverified_collections, + ) + .await +} + +pub async fn get_assets( + conn: &impl ConnectionTrait, + asset_ids: Vec>, + pagination: &Pagination, + limit: u64, +) -> 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, + vec![], + // Default values provided. The args below are not used for batch requests + None, + Order::Asc, + pagination, + limit, + false, ) .await } @@ -144,6 +189,7 @@ pub async fn get_by_authority( sort_direction: Order, pagination: &Pagination, limit: u64, + show_unverified_collections: bool, ) -> Result, DbErr> { let cond = Condition::all() .add(asset_authority::Column::Authority.eq(authority)) @@ -156,6 +202,7 @@ pub async fn get_by_authority( sort_direction, pagination, limit, + show_unverified_collections, ) .await } @@ -168,6 +215,7 @@ async fn get_by_related_condition( sort_direction: Order, pagination: &Pagination, limit: u64, + show_unverified_collections: bool, ) -> Result, DbErr> where E: RelationTrait, @@ -179,16 +227,19 @@ where if let Some(col) = sort_by { stmt = stmt .order_by(col, sort_direction.clone()) - .order_by(asset::Column::Id, sort_direction); + .order_by(asset::Column::Id, sort_direction.clone()); } - let assets = paginate(pagination, limit, stmt).all(conn).await?; - get_related_for_assets(conn, assets).await + let assets = paginate(pagination, limit, stmt, sort_direction, asset::Column::Id) + .all(conn) + .await?; + get_related_for_assets(conn, assets, show_unverified_collections).await } pub async fn get_related_for_assets( conn: &impl ConnectionTrait, assets: Vec, + show_unverified_collections: bool, ) -> Result, DbErr> { let asset_ids = assets.iter().map(|a| a.id.clone()).collect::>(); @@ -244,16 +295,20 @@ pub async fn get_related_for_assets( } } + let cond = if show_unverified_collections { + Condition::all() + } else { + Condition::any() + .add(asset_grouping::Column::Verified.eq(true)) + // Older versions of the indexer did not have the verified flag. A group would be present if and only if it was verified. + // Therefore if verified is null, we can assume that the group is verified. + .add(asset_grouping::Column::Verified.is_null()) + }; + let grouping = asset_grouping::Entity::find() .filter(asset_grouping::Column::AssetId.is_in(ids.clone())) .filter(asset_grouping::Column::GroupValue.is_not_null()) - .filter( - Condition::any() - .add(asset_grouping::Column::Verified.eq(true)) - // Older versions of the indexer did not have the verified flag. A group would be present if and only if it was verified. - // Therefore if verified is null, we can assume that the group is verified. - .add(asset_grouping::Column::Verified.is_null()), - ) + .filter(cond) .order_by_asc(asset_grouping::Column::AssetId) .all(conn) .await?; @@ -274,6 +329,7 @@ pub async fn get_assets_by_condition( sort_direction: Order, pagination: &Pagination, limit: u64, + show_unverified_collections: bool, ) -> Result, DbErr> { let mut stmt = asset::Entity::find(); for def in joins { @@ -283,11 +339,13 @@ pub async fn get_assets_by_condition( if let Some(col) = sort_by { stmt = stmt .order_by(col, sort_direction.clone()) - .order_by(asset::Column::Id, sort_direction); + .order_by(asset::Column::Id, sort_direction.clone()); } - let assets = paginate(pagination, limit, stmt).all(conn).await?; - let full_assets = get_related_for_assets(conn, assets).await?; + 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).await?; Ok(full_assets) } diff --git a/digital_asset_types/src/dapi/assets_by_authority.rs b/digital_asset_types/src/dapi/assets_by_authority.rs index 525881146..c6bbab375 100644 --- a/digital_asset_types/src/dapi/assets_by_authority.rs +++ b/digital_asset_types/src/dapi/assets_by_authority.rs @@ -1,5 +1,7 @@ use crate::dao::scopes; +use crate::dao::PageOptions; use crate::rpc::filter::AssetSorting; +use crate::rpc::options::Options; use crate::rpc::response::AssetList; use sea_orm::DatabaseConnection; use sea_orm::DbErr; @@ -10,12 +12,10 @@ pub async fn get_assets_by_authority( db: &DatabaseConnection, authority: Vec, sorting: AssetSorting, - limit: u64, - page: Option, - before: Option>, - after: Option>, + page_options: &PageOptions, + options: &Options, ) -> Result { - let pagination = create_pagination(before, after, page)?; + let pagination = create_pagination(&page_options)?; let (sort_direction, sort_column) = create_sorting(sorting); let assets = scopes::asset::get_by_authority( db, @@ -23,8 +23,14 @@ pub async fn get_assets_by_authority( sort_column, sort_direction, &pagination, - limit, + page_options.limit, + options.show_unverified_collections, ) .await?; - Ok(build_asset_response(assets, limit, &pagination)) + Ok(build_asset_response( + assets, + page_options.limit, + &pagination, + options, + )) } diff --git a/digital_asset_types/src/dapi/assets_by_creator.rs b/digital_asset_types/src/dapi/assets_by_creator.rs index e69a12f92..05cdb595f 100644 --- a/digital_asset_types/src/dapi/assets_by_creator.rs +++ b/digital_asset_types/src/dapi/assets_by_creator.rs @@ -1,5 +1,7 @@ use crate::dao::scopes; +use crate::dao::PageOptions; use crate::rpc::filter::AssetSorting; +use crate::rpc::options::Options; use crate::rpc::response::AssetList; use sea_orm::DatabaseConnection; use sea_orm::DbErr; @@ -11,12 +13,10 @@ pub async fn get_assets_by_creator( creator: Vec, only_verified: bool, sorting: AssetSorting, - limit: u64, - page: Option, - before: Option>, - after: Option>, + page_options: &PageOptions, + options: &Options, ) -> Result { - let pagination = create_pagination(before, after, page)?; + let pagination = create_pagination(&page_options)?; let (sort_direction, sort_column) = create_sorting(sorting); let assets = scopes::asset::get_by_creator( db, @@ -25,8 +25,14 @@ pub async fn get_assets_by_creator( sort_column, sort_direction, &pagination, - limit, + page_options.limit, + options.show_unverified_collections, ) .await?; - Ok(build_asset_response(assets, limit, &pagination)) + Ok(build_asset_response( + assets, + page_options.limit, + &pagination, + options, + )) } diff --git a/digital_asset_types/src/dapi/assets_by_group.rs b/digital_asset_types/src/dapi/assets_by_group.rs index 1e655b6ab..e633a51ab 100644 --- a/digital_asset_types/src/dapi/assets_by_group.rs +++ b/digital_asset_types/src/dapi/assets_by_group.rs @@ -1,5 +1,7 @@ use crate::dao::scopes; +use crate::dao::PageOptions; use crate::rpc::filter::AssetSorting; +use crate::rpc::options::Options; use crate::rpc::response::AssetList; use sea_orm::DatabaseConnection; use sea_orm::DbErr; @@ -10,12 +12,10 @@ pub async fn get_assets_by_group( group_key: String, group_value: String, sorting: AssetSorting, - limit: u64, - page: Option, - before: Option>, - after: Option>, + page_options: &PageOptions, + options: &Options, ) -> Result { - let pagination = create_pagination(before, after, page)?; + let pagination = create_pagination(&page_options)?; let (sort_direction, sort_column) = create_sorting(sorting); let assets = scopes::asset::get_by_grouping( db, @@ -24,8 +24,14 @@ pub async fn get_assets_by_group( sort_column, sort_direction, &pagination, - limit, + page_options.limit, + options.show_unverified_collections, ) .await?; - Ok(build_asset_response(assets, limit, &pagination)) + Ok(build_asset_response( + assets, + page_options.limit, + &pagination, + options, + )) } diff --git a/digital_asset_types/src/dapi/assets_by_owner.rs b/digital_asset_types/src/dapi/assets_by_owner.rs index c2d9cce0e..bf5d41f99 100644 --- a/digital_asset_types/src/dapi/assets_by_owner.rs +++ b/digital_asset_types/src/dapi/assets_by_owner.rs @@ -1,5 +1,7 @@ use crate::dao::scopes; +use crate::dao::PageOptions; use crate::rpc::filter::AssetSorting; +use crate::rpc::options::Options; use crate::rpc::response::AssetList; use sea_orm::DatabaseConnection; use sea_orm::DbErr; @@ -10,12 +12,10 @@ pub async fn get_assets_by_owner( db: &DatabaseConnection, owner_address: Vec, sort_by: AssetSorting, - limit: u64, - page: Option, - before: Option>, - after: Option>, + page_options: &PageOptions, + options: &Options, ) -> Result { - let pagination = create_pagination(before, after, page)?; + let pagination = create_pagination(&page_options)?; let (sort_direction, sort_column) = create_sorting(sort_by); let assets = scopes::asset::get_assets_by_owner( db, @@ -23,8 +23,14 @@ pub async fn get_assets_by_owner( sort_column, sort_direction, &pagination, - limit, + page_options.limit, + options.show_unverified_collections, ) .await?; - Ok(build_asset_response(assets, limit, &pagination)) + Ok(build_asset_response( + assets, + page_options.limit, + &pagination, + options, + )) } diff --git a/digital_asset_types/src/dapi/change_logs.rs b/digital_asset_types/src/dapi/change_logs.rs index a710c0a5a..406f9a903 100644 --- a/digital_asset_types/src/dapi/change_logs.rs +++ b/digital_asset_types/src/dapi/change_logs.rs @@ -1,6 +1,6 @@ -use log::debug; use sea_orm::sea_query::Expr; use sea_orm::{DatabaseConnection, DbBackend}; +use std::collections::HashMap; use { crate::dao::asset, crate::dao::cl_items, @@ -15,6 +15,22 @@ struct SimpleChangeLog { level: i64, node_idx: i64, seq: i64, + tree: Vec, +} + +#[derive(FromQueryResult, Debug, Default, Clone, Eq, PartialEq)] +struct LeafInfo { + id: Vec, + tree_id: Vec, + leaf_idx: i64, + node_idx: i64, + hash: Vec, +} + +#[derive(Hash, Debug, Default, Clone, Eq, PartialEq)] +struct Leaf { + tree_id: Vec, + leaf_idx: i64, } pub async fn get_proof_for_asset( @@ -42,9 +58,6 @@ pub async fn get_proof_for_asset( } let leaf = leaf.unwrap(); let req_indexes = get_required_nodes_for_proof(leaf.node_idx); - let expected_proof_size = req_indexes.len(); - let mut final_node_list: Vec = - vec![SimpleChangeLog::default(); expected_proof_size]; let mut query = cl_items::Entity::find() .select_only() .column(cl_items::Column::NodeIdx) @@ -61,48 +74,169 @@ pub async fn get_proof_for_asset( query.sql = query .sql .replace("SELECT", "SELECT DISTINCT ON (cl_items.node_idx)"); + let required_nodes: Vec = db.query_all(query).await.map(|qr| { + qr.iter() + .map(|q| SimpleChangeLog::from_query_result(q, "").unwrap()) + .collect() + })?; + let asset_proof = build_asset_proof( + leaf.tree, + leaf.node_idx, + leaf.hash, + &req_indexes, + &required_nodes, + ); + Ok(asset_proof) +} + +pub async fn get_asset_proofs( + db: &DatabaseConnection, + asset_ids: Vec>, +) -> Result, DbErr> { + // get the leaves (JOIN with `asset` table to get the asset ids) + let q = asset::Entity::find() + .join( + JoinType::InnerJoin, + asset::Entity::belongs_to(cl_items::Entity) + .from(asset::Column::Nonce) + .to(cl_items::Column::LeafIdx) + .into(), + ) + .select_only() + .column(asset::Column::Id) + .column(asset::Column::TreeId) + .column(cl_items::Column::LeafIdx) + .column(cl_items::Column::NodeIdx) + .column(cl_items::Column::Hash) + .filter(Expr::cust("asset.tree_id = cl_items.tree")) + // filter by user provided asset ids + .filter(asset::Column::Id.is_in(asset_ids.clone())) + .build(DbBackend::Postgres); + let leaves: Vec = db.query_all(q).await.map(|qr| { + qr.iter() + .map(|q| LeafInfo::from_query_result(q, "").unwrap()) + .collect() + })?; + + let mut asset_map: HashMap = HashMap::new(); + for l in &leaves { + let key = Leaf { + tree_id: l.tree_id.clone(), + leaf_idx: l.leaf_idx, + }; + asset_map.insert(key, l.clone()); + } + + // map: (tree_id, leaf_idx) -> [...req_indexes] + let mut tree_indexes: HashMap> = HashMap::new(); + for leaf in &leaves { + let key = Leaf { + tree_id: leaf.tree_id.clone(), + leaf_idx: leaf.leaf_idx, + }; + let req_indexes = get_required_nodes_for_proof(leaf.node_idx); + tree_indexes.insert(key, req_indexes); + } + + // get the required nodes for all assets + // SELECT * FROM cl_items WHERE (tree = ? AND node_idx IN (?)) OR (tree = ? AND node_idx IN (?)) OR ... + let mut condition = Condition::any(); + for (leaf, req_indexes) in &tree_indexes { + let cond = Condition::all() + .add(cl_items::Column::Tree.eq(leaf.tree_id.clone())) + .add(cl_items::Column::NodeIdx.is_in(req_indexes.clone())); + condition = condition.add(cond); + } + let query = cl_items::Entity::find() + .select_only() + .column(cl_items::Column::Tree) + .column(cl_items::Column::NodeIdx) + .column(cl_items::Column::Level) + .column(cl_items::Column::Seq) + .column(cl_items::Column::Hash) + .filter(condition) + .build(DbBackend::Postgres); let nodes: Vec = db.query_all(query).await.map(|qr| { qr.iter() .map(|q| SimpleChangeLog::from_query_result(q, "").unwrap()) .collect() })?; - for node in nodes.iter() { + + // map: (tree, node_idx) -> SimpleChangeLog + let mut node_map: HashMap<(Vec, i64), SimpleChangeLog> = HashMap::new(); + for node in nodes { + let key = (node.tree.clone(), node.node_idx); + node_map.insert(key, node); + } + + // construct the proofs + let mut asset_proofs: HashMap = HashMap::new(); + for (leaf, req_indexes) in &tree_indexes { + let required_nodes: Vec = req_indexes + .iter() + .filter_map(|n| { + let key = (leaf.tree_id.clone(), *n); + node_map.get(&key).cloned() + }) + .collect(); + + let leaf_info = asset_map.get(&leaf).unwrap(); + let asset_proof = build_asset_proof( + leaf_info.tree_id.clone(), + leaf_info.node_idx, + leaf_info.hash.clone(), + &req_indexes, + &required_nodes, + ); + + let asset_id = bs58::encode(leaf_info.id.to_owned()).into_string(); + asset_proofs.insert(asset_id, asset_proof); + } + + Ok(asset_proofs) +} + +fn build_asset_proof( + tree_id: Vec, + leaf_node_idx: i64, + leaf_hash: Vec, + req_indexes: &Vec, + required_nodes: &Vec, +) -> AssetProof { + let mut final_node_list = vec![SimpleChangeLog::default(); req_indexes.len()]; + for node in required_nodes.iter() { if node.level < final_node_list.len().try_into().unwrap() { final_node_list[node.level as usize] = node.to_owned(); } } - for (i, (n, nin)) in final_node_list.iter_mut().zip(req_indexes).enumerate() { + for (i, (n, nin)) in final_node_list + .iter_mut() + .zip(req_indexes.clone()) + .enumerate() + { if *n == SimpleChangeLog::default() { - *n = make_empty_node(i as i64, nin); + *n = make_empty_node(i as i64, nin, tree_id.clone()); } } - for n in final_node_list.iter() { - debug!( - "level {} index {} seq {} hash {}", - n.level, - n.node_idx, - n.seq, - bs58::encode(&n.hash).into_string() - ); - } - Ok(AssetProof { + AssetProof { root: bs58::encode(final_node_list.pop().unwrap().hash).into_string(), - leaf: bs58::encode(&leaf.hash).into_string(), + leaf: bs58::encode(leaf_hash).into_string(), proof: final_node_list .iter() .map(|model| bs58::encode(&model.hash).into_string()) .collect(), - node_index: leaf.node_idx, - tree_id: bs58::encode(&leaf.tree).into_string(), - }) + node_index: leaf_node_idx, + tree_id: bs58::encode(tree_id).into_string(), + } } -fn make_empty_node(lvl: i64, node_index: i64) -> SimpleChangeLog { +fn make_empty_node(lvl: i64, node_index: i64, tree: Vec) -> SimpleChangeLog { SimpleChangeLog { node_idx: node_index, level: lvl, hash: empty_node(lvl as u32).to_vec(), seq: 0, + tree, } } diff --git a/digital_asset_types/src/dapi/common/asset.rs b/digital_asset_types/src/dapi/common/asset.rs index 007e927b6..2377ddf32 100644 --- a/digital_asset_types/src/dapi/common/asset.rs +++ b/digital_asset_types/src/dapi/common/asset.rs @@ -1,8 +1,10 @@ use crate::dao::sea_orm_active_enums::SpecificationVersions; use crate::dao::FullAsset; +use crate::dao::PageOptions; use crate::dao::Pagination; use crate::dao::{asset, asset_authority, asset_creators, asset_data, asset_grouping}; use crate::rpc::filter::{AssetSortBy, AssetSortDirection, AssetSorting}; +use crate::rpc::options::Options; use crate::rpc::response::{AssetError, AssetList}; use crate::rpc::{ Asset as RpcAsset, Authority, Compression, Content, Creator, File, Group, Interface, @@ -48,17 +50,27 @@ pub fn build_asset_response( assets: Vec, limit: u64, pagination: &Pagination, + options: &Options, ) -> AssetList { let total = assets.len() as u32; - let (page, before, after) = match pagination { + 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, bef, aft, None) + } + Pagination::Page { page } => (Some(*page), None, None, None), + Pagination::Cursor(_) => { + if let Some(last_asset) = assets.last() { + let cursor_str = bs58::encode(&last_asset.asset.id.clone()).into_string(); + (None, None, None, Some(cursor_str)) + } else { + (None, None, None, None) + } } - Pagination::Page { page } => (Some(*page), None, None), }; - let (items, errors) = asset_list_to_rpc(assets); + + let (items, errors) = asset_list_to_rpc(assets, options); AssetList { total, limit: limit as u32, @@ -67,11 +79,13 @@ pub fn build_asset_response( after, items, errors, + cursor, } } pub fn create_sorting(sorting: AssetSorting) -> (sea_orm::query::Order, Option) { let sort_column = match sorting.sort_by { + AssetSortBy::Id => Some(asset::Column::Id), AssetSortBy::Created => Some(asset::Column::CreatedAt), AssetSortBy::Updated => Some(asset::Column::SlotUpdated), AssetSortBy::RecentAction => Some(asset::Column::SlotUpdated), @@ -84,18 +98,22 @@ pub fn create_sorting(sorting: AssetSorting) -> (sea_orm::query::Order, Option>, - after: Option>, - page: Option, -) -> Result { - match (&before, &after, &page) { - (_, _, None) => Ok(Pagination::Keyset { - before: before.map(|x| x.into()), - after: after.map(|x| x.into()), - }), - (None, None, Some(p)) => Ok(Pagination::Page { page: *p }), - _ => Err(DbErr::Custom("Invalid Pagination".to_string())), +pub fn create_pagination(page_options: &PageOptions) -> Result { + if let Some(cursor) = &page_options.cursor { + Ok(Pagination::Cursor(cursor.clone())) + } else { + match ( + page_options.before.as_ref(), + page_options.after.as_ref(), + page_options.page, + ) { + (_, _, None) => Ok(Pagination::Keyset { + before: page_options.before.clone(), + after: page_options.after.clone(), + }), + (None, None, Some(p)) => Ok(Pagination::Page { page: p }), + _ => Err(DbErr::Custom("Invalid Pagination".to_string())), + } } } @@ -150,6 +168,10 @@ pub fn v1_content_from_json(asset_data: &asset_data::Model) -> Result Result Ordering::Equal, }); - Ok(Content { schema: "https://schema.metaplex.com/nft1.0.json".to_string(), json_uri, @@ -231,10 +252,7 @@ pub fn v1_content_from_json(asset_data: &asset_data::Model) -> Result Result { +pub fn get_content(asset: &asset::Model, data: &asset_data::Model) -> Result { match asset.specification_version { Some(SpecificationVersions::V1) | Some(SpecificationVersions::V0) => { v1_content_from_json(data) @@ -265,20 +283,27 @@ pub fn to_creators(creators: Vec) -> Vec { .collect() } -pub fn to_grouping(groups: Vec) -> Result, DbErr> { - fn find_group(model: &asset_grouping::Model) -> Result { - Ok(Group { - group_key: model.group_key.clone(), - group_value: Some( - model - .group_value - .clone() - .ok_or(DbErr::Custom("Group value not found".to_string()))?, - ), +pub fn to_grouping( + groups: Vec, + options: &Options, +) -> Result, DbErr> { + let result: Vec = groups + .iter() + .filter_map(|model| { + let verified = match options.show_unverified_collections { + // Null verified indicates legacy data, meaning it is verified. + true => Some(model.verified.unwrap_or(true)), + false => None, + }; + // Filter out items where group_value is None. + model.group_value.clone().map(|group_value| Group { + group_key: model.group_key.clone(), + group_value: Some(group_value), + verified, + }) }) - } - - groups.iter().map(find_group).collect() + .collect(); + Ok(result) } pub fn get_interface(asset: &asset::Model) -> Result { @@ -297,9 +322,7 @@ pub fn get_interface(asset: &asset::Model) -> Result { } //TODO -> impl custom error type -pub fn asset_to_rpc( - asset: FullAsset -) -> Result { +pub fn asset_to_rpc(asset: FullAsset, options: &Options) -> Result { let FullAsset { asset, data, @@ -309,7 +332,7 @@ pub fn asset_to_rpc( } = asset; let rpc_authorities = to_authority(authorities); let rpc_creators = to_creators(creators); - let rpc_groups = to_grouping(groups)?; + let rpc_groups = to_grouping(groups, options)?; let interface = get_interface(&asset)?; let content = get_content(&asset, &data)?; let mut chain_data_selector_fn = jsonpath_lib::selector(&data.chain_data); @@ -390,13 +413,14 @@ pub fn asset_to_rpc( } pub fn asset_list_to_rpc( - asset_list: Vec + asset_list: Vec, + options: &Options, ) -> (Vec, Vec) { asset_list .into_iter() .fold((vec![], vec![]), |(mut assets, mut errors), asset| { let id = bs58::encode(asset.asset.id.clone()).into_string(); - match asset_to_rpc(asset) { + match asset_to_rpc(asset, options) { Ok(rpc_asset) => assets.push(rpc_asset), Err(e) => errors.push(AssetError { id, diff --git a/digital_asset_types/src/dapi/get_asset.rs b/digital_asset_types/src/dapi/get_asset.rs index 110d9cb62..3740562c3 100644 --- a/digital_asset_types/src/dapi/get_asset.rs +++ b/digital_asset_types/src/dapi/get_asset.rs @@ -1,10 +1,33 @@ +use super::common::{asset_to_rpc, build_asset_response}; +use crate::{ + dao::{scopes, Pagination}, + rpc::{options::Options, Asset}, +}; use sea_orm::{DatabaseConnection, DbErr}; +use std::collections::HashMap; -use crate::{dao::scopes, rpc::Asset}; - -use super::common::asset_to_rpc; - -pub async fn get_asset(db: &DatabaseConnection, id: Vec) -> Result { +pub async fn get_asset( + db: &DatabaseConnection, + id: Vec, + options: &Options, +) -> Result { let asset = scopes::asset::get_by_id(db, id, false).await?; - asset_to_rpc(asset) + asset_to_rpc(asset, options) +} + +pub async fn get_assets( + db: &DatabaseConnection, + ids: Vec>, + limit: u64, + options: &Options, +) -> Result, DbErr> { + let pagination = Pagination::Page { page: 1 }; + let assets = scopes::asset::get_assets(db, ids, &pagination, limit).await?; + let asset_list = build_asset_response(assets, limit, &pagination, options); + let asset_map = asset_list + .items + .into_iter() + .map(|asset| (asset.id.clone(), asset)) + .collect(); + Ok(asset_map) } diff --git a/digital_asset_types/src/dapi/search_assets.rs b/digital_asset_types/src/dapi/search_assets.rs index d6998ea76..1ce995b92 100644 --- a/digital_asset_types/src/dapi/search_assets.rs +++ b/digital_asset_types/src/dapi/search_assets.rs @@ -1,7 +1,7 @@ use super::common::{build_asset_response, create_pagination, create_sorting}; use crate::{ - dao::{scopes, SearchAssetsQuery}, - rpc::{filter::AssetSorting, response::AssetList}, + dao::{scopes, PageOptions, SearchAssetsQuery}, + rpc::{filter::AssetSorting, options::Options, response::AssetList}, }; use sea_orm::{DatabaseConnection, DbErr}; @@ -9,12 +9,10 @@ pub async fn search_assets( db: &DatabaseConnection, search_assets_query: SearchAssetsQuery, sorting: AssetSorting, - limit: u64, - page: Option, - before: Option>, - after: Option>, + page_options: &PageOptions, + options: &Options, ) -> Result { - let pagination = create_pagination(before, after, page)?; + let pagination = create_pagination(&page_options)?; let (sort_direction, sort_column) = create_sorting(sorting); let (condition, joins) = search_assets_query.conditions()?; let assets = scopes::asset::get_assets_by_condition( @@ -24,8 +22,14 @@ pub async fn search_assets( sort_column, sort_direction, &pagination, - limit, + page_options.limit, + options.show_unverified_collections, ) .await?; - Ok(build_asset_response(assets, limit, &pagination)) + Ok(build_asset_response( + assets, + page_options.limit, + &pagination, + options, + )) } diff --git a/digital_asset_types/src/rpc/asset.rs b/digital_asset_types/src/rpc/asset.rs index ea365e4b1..cfbb02d40 100644 --- a/digital_asset_types/src/rpc/asset.rs +++ b/digital_asset_types/src/rpc/asset.rs @@ -200,6 +200,8 @@ pub type GroupValue = String; pub struct Group { pub group_key: String, pub group_value: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub verified: Option, } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] diff --git a/digital_asset_types/src/rpc/filter.rs b/digital_asset_types/src/rpc/filter.rs index f6aa010f8..0de5574b7 100644 --- a/digital_asset_types/src/rpc/filter.rs +++ b/digital_asset_types/src/rpc/filter.rs @@ -19,8 +19,9 @@ impl Default for AssetSorting { } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] - pub enum AssetSortBy { + #[serde(rename = "id")] + Id, #[serde(rename = "created")] Created, #[serde(rename = "updated")] diff --git a/digital_asset_types/src/rpc/mod.rs b/digital_asset_types/src/rpc/mod.rs index 92325d382..37fb7d9f9 100644 --- a/digital_asset_types/src/rpc/mod.rs +++ b/digital_asset_types/src/rpc/mod.rs @@ -1,6 +1,7 @@ mod asset; pub mod filter; +pub mod options; pub mod response; pub use asset::*; diff --git a/digital_asset_types/src/rpc/options.rs b/digital_asset_types/src/rpc/options.rs new file mode 100644 index 000000000..a1fafa2c4 --- /dev/null +++ b/digital_asset_types/src/rpc/options.rs @@ -0,0 +1,9 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema, Default)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +pub struct Options { + #[serde(default)] + pub show_unverified_collections: bool, +} diff --git a/digital_asset_types/src/rpc/response.rs b/digital_asset_types/src/rpc/response.rs index 855061438..3945f6955 100644 --- a/digital_asset_types/src/rpc/response.rs +++ b/digital_asset_types/src/rpc/response.rs @@ -30,6 +30,8 @@ pub struct AssetList { pub before: Option, #[serde(skip_serializing_if = "Option::is_none")] pub after: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cursor: Option, pub items: Vec, #[serde(skip_serializing_if = "Vec::is_empty")] pub errors: Vec,