Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: getSignaturesForAsset endpoint on top of new cl_audits_v2 table #155

Merged
merged 11 commits into from
Jan 20, 2024
35 changes: 34 additions & 1 deletion das_api/src/api/api_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use digital_asset_types::{
},
dapi::{
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,
get_assets_by_group, get_assets_by_owner, get_proof_for_asset, get_signatures_for_asset,
search_assets,
},
rpc::{
filter::{AssetSortBy, SearchConditionType},
Expand Down Expand Up @@ -461,6 +462,38 @@ impl ApiContract for DasApi {
.map_err(Into::into)
}

async fn get_signatures_for_asset(
self: &DasApi,
payload: GetSignaturesForAsset,
) -> Result<TransactionSignatureList, DasApiError> {
let GetSignaturesForAsset {
id,
limit,
page,
before,
after,
tree,
leaf_index,
cursor,
} = payload;

if !((id.is_some() && tree.is_none() && leaf_index.is_none())
|| (id.is_none() && tree.is_some() && leaf_index.is_some()))
{
return Err(DasApiError::ValidationError(
"Must provide either 'id' or both 'tree' and 'leafIndex'".to_string(),
));
}
let id = validate_opt_pubkey(&id)?;
let tree = validate_opt_pubkey(&tree)?;

let page_options = self.validate_pagination(limit, page, &before, &after, &cursor, None)?;

get_signatures_for_asset(&self.db_connection, id, tree, leaf_index, page_options)
.await
.map_err(Into::into)
}

async fn get_grouping(
self: &DasApi,
payload: GetGrouping,
Expand Down
25 changes: 24 additions & 1 deletion das_api/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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::response::{AssetList, 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 +147,20 @@ pub struct GetGrouping {
pub group_value: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct GetSignaturesForAsset {
pub id: Option<String>,
pub limit: Option<u32>,
pub page: Option<u32>,
pub before: Option<String>,
pub after: Option<String>,
pub tree: Option<String>,
pub leaf_index: Option<i64>,
#[serde(default)]
pub cursor: Option<String>,
}

#[document_rpc]
#[async_trait]
pub trait ApiContract: Send + Sync + 'static {
Expand Down Expand Up @@ -220,6 +234,15 @@ pub trait ApiContract: Send + Sync + 'static {
summary = "Search for assets by a variety of parameters"
)]
async fn search_assets(&self, payload: SearchAssets) -> Result<AssetList, DasApiError>;
#[rpc(
name = "getSignaturesForAsset",
params = "named",
summary = "Get transaction signatures for an asset"
)]
async fn get_signatures_for_asset(
&self,
payload: GetSignaturesForAsset,
) -> Result<TransactionSignatureList, DasApiError>;
#[rpc(
name = "getGrouping",
params = "named",
Expand Down
11 changes: 11 additions & 0 deletions das_api/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ impl RpcApiBuilder {
)?;
module.register_alias("getAssetsByGroup", "get_assets_by_group")?;

module.register_async_method(
tahsintunan marked this conversation as resolved.
Show resolved Hide resolved
"getSignaturesForAsset",
|rpc_params, rpc_context| async move {
let payload = rpc_params.parse::<GetSignaturesForAsset>()?;
rpc_context
.get_signatures_for_asset(payload)
.await
.map_err(Into::into)
},
)?;

module.register_async_method("search_assets", |rpc_params, rpc_context| async move {
let payload = rpc_params.parse::<SearchAssets>()?;
rpc_context.search_assets(payload).await.map_err(Into::into)
Expand Down
74 changes: 74 additions & 0 deletions digital_asset_types/src/dao/generated/cl_audits_v2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3

use super::sea_orm_active_enums::Instruction;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;

impl EntityName for Entity {
fn table_name(&self) -> &str {
"cl_audits_v2"
}
}

#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Serialize, Deserialize)]
pub struct Model {
pub id: i64,
pub tree: Vec<u8>,
pub leaf_idx: i64,
pub seq: i64,
pub created_at: DateTime,
pub tx: Vec<u8>,
pub instruction: Instruction,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
Id,
Tree,
LeafIdx,
Seq,
CreatedAt,
Tx,
Instruction,
}

#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
Id,
}

impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i64;
fn auto_increment() -> bool {
true
}
}

#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}

impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnDef {
match self {
Self::Id => ColumnType::BigInteger.def(),
Self::Tree => ColumnType::Binary.def(),
Self::LeafIdx => ColumnType::BigInteger.def(),
Self::Seq => ColumnType::BigInteger.def(),
Self::CreatedAt => ColumnType::DateTime.def(),
Self::Tx => ColumnType::Binary.def(),
Self::Instruction => Instruction::db_type(),
}
}
}

impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!("No RelationDef")
}
}

impl ActiveModelBehavior for ActiveModel {}
3 changes: 2 additions & 1 deletion digital_asset_types/src/dao/generated/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3

pub mod prelude;

Expand All @@ -10,6 +10,7 @@ pub mod asset_grouping;
pub mod asset_v1_account_attachments;
pub mod backfill_items;
pub mod cl_audits;
pub mod cl_audits_v2;
danenbm marked this conversation as resolved.
Show resolved Hide resolved
pub mod cl_items;
pub mod raw_txn;
pub mod sea_orm_active_enums;
Expand Down
4 changes: 2 additions & 2 deletions digital_asset_types/src/dao/generated/prelude.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3

#![allow(unused_imports)]
pub use super::asset::Entity as Asset;
Expand All @@ -8,7 +8,7 @@ pub use super::asset_data::Entity as AssetData;
pub use super::asset_grouping::Entity as AssetGrouping;
pub use super::asset_v1_account_attachments::Entity as AssetV1AccountAttachments;
pub use super::backfill_items::Entity as BackfillItems;
pub use super::cl_audits::Entity as ClAudits;
pub use super::cl_audits_v2::Entity as ClAuditsV2;
pub use super::cl_items::Entity as ClItems;
pub use super::raw_txn::Entity as RawTxn;
pub use super::tasks::Entity as Tasks;
Expand Down
77 changes: 77 additions & 0 deletions digital_asset_types/src/dao/generated/sea_orm_active_enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,80 @@ pub enum V1AccountAttachments {
#[sea_orm(string_value = "unknown")]
Unknown,
}

#[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)]
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "instruction")]
pub enum Instruction {
#[sea_orm(string_value = "burn")]
Burn,
#[sea_orm(string_value = "cancel_redeem")]
CancelRedeem,
#[sea_orm(string_value = "compress")]
Compress,
#[sea_orm(string_value = "decompress_v1")]
DecompressV1,
#[sea_orm(string_value = "delegate")]
Delegate,
#[sea_orm(string_value = "mint_to_collection_v1")]
MintToCollectionV1,
#[sea_orm(string_value = "mint_v1")]
MintV1,
#[sea_orm(string_value = "redeem")]
Redeem,
#[sea_orm(string_value = "set_and_verify_collection")]
SetAndVerifyCollection,
#[sea_orm(string_value = "transfer")]
Transfer,
#[sea_orm(string_value = "unknown")]
Unknown,
#[sea_orm(string_value = "unverify_collection")]
UnverifyCollection,
#[sea_orm(string_value = "unverify_creator")]
UnverifyCreator,
#[sea_orm(string_value = "verify_collection")]
VerifyCollection,
#[sea_orm(string_value = "verify_creator")]
VerifyCreator,
}
// Added manually for convenience.
impl Instruction {
pub fn from_str(s: &str) -> Self {
match s {
"Burn" => Instruction::Burn,
"CancelRedeem" => Instruction::CancelRedeem,
"Compress" => Instruction::Compress,
"DecompressV1" => Instruction::DecompressV1,
"Delegate" => Instruction::Delegate,
"MintToCollectionV1" => Instruction::MintToCollectionV1,
"MintV1" => Instruction::MintV1,
"Redeem" => Instruction::Redeem,
"SetAndVerifyCollection" => Instruction::SetAndVerifyCollection,
"Transfer" => Instruction::Transfer,
"UnverifyCollection" => Instruction::UnverifyCollection,
"UnverifyCreator" => Instruction::UnverifyCreator,
"VerifyCollection" => Instruction::VerifyCollection,
"VerifyCreator" => Instruction::VerifyCreator,
tahsintunan marked this conversation as resolved.
Show resolved Hide resolved
_ => Instruction::Unknown,
}
}

pub fn to_str(s: &Self) -> &str {
match s {
Instruction::Burn => "Burn",
Instruction::CancelRedeem => "CancelReddem",
Instruction::Compress => "Compress",
Instruction::DecompressV1 => "DecompressV1",
Instruction::Delegate => "Delegate",
Instruction::MintToCollectionV1 => "MintToCollectionV1",
Instruction::MintV1 => "MintV1",
Instruction::Redeem => "Redeem",
Instruction::SetAndVerifyCollection => "SetAndVerifyCollection",
Instruction::Transfer => "Transfer",
Instruction::UnverifyCollection => "UnverifyCollection",
Instruction::UnverifyCreator => "UnverifyCreator",
Instruction::VerifyCollection => "VerifyCollection",
Instruction::VerifyCreator => "VerifyCreator",
_ => "Unknown",
}
}
}
83 changes: 81 additions & 2 deletions digital_asset_types/src/dao/scopes/asset.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::dao::{
asset, asset_authority, asset_creators, asset_data, asset_grouping, Cursor, FullAsset,
GroupingSize, Pagination,
asset::{self},
asset_authority, asset_creators, asset_data, asset_grouping, cl_audits_v2,
sea_orm_active_enums::Instruction,
Cursor, FullAsset, GroupingSize, Pagination,
};
use indexmap::IndexMap;
use sea_orm::{entity::*, query::*, ConnectionTrait, DbErr, Order};
Expand Down Expand Up @@ -427,6 +429,83 @@ pub async fn get_by_id(
})
}

pub async fn fetch_transactions(
conn: &impl ConnectionTrait,
tree: Vec<u8>,
leaf_idx: i64,
pagination: &Pagination,
limit: u64,
) -> Result<Vec<(String, String)>, DbErr> {
let stmt = cl_audits_v2::Entity::find()
.filter(cl_audits_v2::Column::Tree.eq(tree))
.filter(cl_audits_v2::Column::LeafIdx.eq(leaf_idx))
.order_by(cl_audits_v2::Column::Seq, sea_orm::Order::Asc);

let stmt = paginate(
pagination,
limit,
stmt,
sea_orm::Order::Asc,
asset::Column::Id,
);
let transactions = stmt.all(conn).await?;
let transaction_list = transactions
.into_iter()
.map(|transaction| {
let tx = bs58::encode(transaction.tx).into_string();
let ix = Instruction::to_str(&transaction.instruction).to_string();
(tx, ix)
})
.collect();

Ok(transaction_list)
}

pub async fn get_signatures_for_asset(
conn: &impl ConnectionTrait,
asset_id: Option<Vec<u8>>,
tree_id: Option<Vec<u8>>,
leaf_idx: Option<i64>,
pagination: &Pagination,
limit: u64,
) -> Result<Vec<(String, String)>, DbErr> {
// if tree_id and leaf_idx are provided, use them directly to fetch transactions
if let (Some(tree_id), Some(leaf_idx)) = (tree_id, leaf_idx) {
let transactions = fetch_transactions(conn, tree_id, leaf_idx, pagination, limit).await?;
return Ok(transactions);
}

if asset_id.is_none() {
return Err(DbErr::Custom(
"Either 'id' or both 'tree' and 'leafIndex' must be provided".to_string(),
));
}

// if only asset_id is provided, fetch the latest tree and leaf_idx (asset.nonce) for the asset
// and use them to fetch transactions
let stmt = asset::Entity::find()
.distinct_on([(asset::Entity, asset::Column::Id)])
.filter(asset::Column::Id.eq(asset_id))
.order_by(asset::Column::Id, Order::Desc)
.limit(1);
danenbm marked this conversation as resolved.
Show resolved Hide resolved
let asset = stmt.one(conn).await?;
if let Some(asset) = asset {
let tree = asset
.tree_id
.ok_or(DbErr::RecordNotFound("Tree not found".to_string()))?;
if tree.is_empty() {
return Err(DbErr::Custom("Empty tree for asset".to_string()));
}
let leaf_idx = asset
.nonce
.ok_or(DbErr::RecordNotFound("Leaf ID does not exist".to_string()))?;
let transactions = fetch_transactions(conn, tree, leaf_idx, pagination, limit).await?;
Ok(transactions)
} else {
Ok(Vec::new())
}
}

fn filter_out_stale_creators(creators: &mut Vec<asset_creators::Model>) {
// If the first creator is an empty Vec, it means the creator array is empty (which is allowed
// for compressed assets in Bubblegum).
Expand Down
Loading
Loading