From 1e22d98a4a81db634392dcc9115182177589a44d Mon Sep 17 00:00:00 2001 From: Eugene Gostkin Date: Mon, 18 Dec 2023 17:28:42 +0100 Subject: [PATCH] Split asset name and policy id for simplified future searches --- indexer/entity/src/projected_nft.rs | 3 +- .../src/m20231025_000017_projected_nft.rs | 3 +- .../src/multiera/multiera_projected_nft.rs | 108 ++++++++++++++---- .../ProjectedNftRangeController.ts | 4 +- .../projectedNftRange.queries.ts | 8 +- .../projected_nft/projectedNftRange.sql | 3 +- webserver/shared/models/ProjectedNftRange.ts | 14 +++ 7 files changed, 111 insertions(+), 32 deletions(-) diff --git a/indexer/entity/src/projected_nft.rs b/indexer/entity/src/projected_nft.rs index b7c03c04..35bca93c 100644 --- a/indexer/entity/src/projected_nft.rs +++ b/indexer/entity/src/projected_nft.rs @@ -14,7 +14,8 @@ pub struct Model { pub hololocker_utxo_id: Option, #[sea_orm(column_type = "BigInteger")] pub tx_id: i64, - pub asset: String, + pub policy_id: String, + pub asset_name: String, #[sea_orm(column_type = "BigInteger")] pub amount: i64, pub operation: i32, // lock / unlock / claim diff --git a/indexer/migration/src/m20231025_000017_projected_nft.rs b/indexer/migration/src/m20231025_000017_projected_nft.rs index c4fc2e0b..88522ee9 100644 --- a/indexer/migration/src/m20231025_000017_projected_nft.rs +++ b/indexer/migration/src/m20231025_000017_projected_nft.rs @@ -33,7 +33,8 @@ impl MigrationTrait for Migration { .col(ColumnDef::new(Column::PreviousUtxoTxOutputIndex).big_integer()) .col(ColumnDef::new(Column::HololockerUtxoId).big_integer()) .col(ColumnDef::new(Column::TxId).big_integer().not_null()) - .col(ColumnDef::new(Column::Asset).text().not_null()) + .col(ColumnDef::new(Column::AssetName).text().not_null()) + .col(ColumnDef::new(Column::PolicyId).text().not_null()) .col(ColumnDef::new(Column::Amount).big_integer().not_null()) .col(ColumnDef::new(Column::Operation).integer().not_null()) .col(ColumnDef::new(Column::PlutusDatum).binary().not_null()) diff --git a/indexer/tasks/src/multiera/multiera_projected_nft.rs b/indexer/tasks/src/multiera/multiera_projected_nft.rs index 2fb21fd4..32f07207 100644 --- a/indexer/tasks/src/multiera/multiera_projected_nft.rs +++ b/indexer/tasks/src/multiera/multiera_projected_nft.rs @@ -1,4 +1,3 @@ -use anyhow::anyhow; use cardano_multiplatform_lib::error::DeserializeError; use cml_core::serialization::FromBytes; use cml_crypto::RawBytesEncoding; @@ -102,11 +101,18 @@ pub(crate) struct ProjectedNftInputsQueryOutputResult { pub tx_hash: Vec, pub operation: i32, pub owner_address: Vec, - pub asset: String, + pub policy_id: String, + pub asset_name: String, pub amount: i64, pub plutus_datum: Vec, } +impl ProjectedNftInputsQueryOutputResult { + pub fn subject(&self) -> String { + format!("{}.{}", self.policy_id, self.asset_name) + } +} + async fn handle_projected_nft( db_tx: &DatabaseTransaction, block: BlockInfo<'_, MultiEraBlock<'_>, BlockGlobalInfo>, @@ -196,15 +202,16 @@ async fn handle_projected_nft( } for output_data in projected_nft_outputs.into_iter() { - for (asset_name, asset_value) in output_data.non_ada_assets.into_iter() { + for asset in output_data.non_ada_assets.into_iter() { queued_projected_nft_records.push(entity::projected_nft::ActiveModel { owner_address: Set(output_data.address.clone()), previous_utxo_tx_output_index: Set(output_data.previous_utxo_tx_output_index), previous_utxo_tx_hash: Set(output_data.previous_utxo_tx_hash.clone()), hololocker_utxo_id: Set(Some(output_data.hololocker_utxo_id)), tx_id: Set(cardano_transaction.id), - asset: Set(asset_name), - amount: Set(asset_value), + policy_id: Set(asset.policy_id), + asset_name: Set(asset.asset_name), + amount: Set(asset.amount), operation: Set(output_data.operation.into()), plutus_datum: Set(output_data.plutus_data.clone()), for_how_long: Set(output_data.for_how_long), @@ -236,7 +243,7 @@ fn find_lock_outputs_for_corresponding_partial_withdrawals( } let mut nft_data_assets = output_data.non_ada_assets.clone(); - nft_data_assets.sort_by_key(|(name, _)| name.clone()); + nft_data_assets.sort_by_key(|asset| asset.subject()); let mut withdrawal_input_to_remove: Option<(Vec, i64)> = None; @@ -254,9 +261,13 @@ fn find_lock_outputs_for_corresponding_partial_withdrawals( let mut withdrawal_assets = withdrawal .iter() - .map(|w| (w.asset.clone(), w.amount)) + .map(|w| AssetData { + policy_id: w.policy_id.clone(), + asset_name: w.asset_name.clone(), + amount: w.amount, + }) .collect::>(); - withdrawal_assets.sort_by_key(|(name, _)| name.clone()); + withdrawal_assets.sort_by_key(|asset| asset.subject()); if withdrawal_assets == nft_data_assets { withdrawal_input_to_remove = Some((input_hash.clone(), *input_index)); @@ -316,18 +327,19 @@ fn handle_partial_withdraw( // make a balance map let mut input_asset_to_value = HashMap::::new(); for entry in partial_withdrawal_input.iter() { - input_asset_to_value.insert(entry.asset.clone(), entry.clone()); + input_asset_to_value.insert(entry.subject(), entry.clone()); } // subtract all the assets - for (output_asset_name, output_asset_value) in output_projected_nft_data.non_ada_assets.iter() { + for output_asset_data in output_projected_nft_data.non_ada_assets.iter() { + let output_asset_subject = output_asset_data.subject(); input_asset_to_value - .get_mut(&output_asset_name.clone()) + .get_mut(&output_asset_subject) .ok_or(DbErr::Custom(format!( - "Expected to see asset {output_asset_name} in projected nft {}@{withdrawn_from_input_index}", + "Expected to see asset {output_asset_subject} in projected nft {}@{withdrawn_from_input_index}", hex::encode(withdrawn_from_input_hash.clone()) )))? - .amount -= output_asset_value; + .amount -= output_asset_data.amount; } *partial_withdrawal_input = input_asset_to_value @@ -384,7 +396,8 @@ async fn get_projected_nft_inputs( .column(TransactionOutputColumn::TxId) .column(TransactionOutputColumn::OutputIndex) .column(ProjectedNftColumn::Operation) - .column(ProjectedNftColumn::Asset) + .column(ProjectedNftColumn::PolicyId) + .column(ProjectedNftColumn::AssetName) .column(ProjectedNftColumn::Amount) .column(ProjectedNftColumn::OwnerAddress) .column(ProjectedNftColumn::PlutusDatum) @@ -451,7 +464,8 @@ fn handle_claims_and_partial_withdraws( queued_projected_nft_records.push(entity::projected_nft::ActiveModel { hololocker_utxo_id: Set(None), tx_id: Set(cardano_transaction.id), - asset: Set(projected_nft.asset.clone()), + policy_id: Set(projected_nft.policy_id.clone()), + asset_name: Set(projected_nft.asset_name.clone()), amount: Set(projected_nft.amount), operation: Set(ProjectedNftOperation::Claim.into()), plutus_datum: Set(vec![]), @@ -508,6 +522,47 @@ fn get_output_index_to_outputs_map( outputs_map } +#[derive(Debug, Clone, Default, Eq, PartialEq)] +pub struct AssetData { + pub policy_id: String, + pub asset_name: String, + pub amount: i64, +} + +impl AssetData { + pub fn subject(&self) -> String { + format!("{}.{}", self.policy_id, self.asset_name) + } + + pub fn from_subject(subject: String, amount: i64) -> Result { + let mut split = subject.split('.'); + let policy_id = if let Some(policy_id_hex) = split.next() { + policy_id_hex.to_string() + } else { + return Err(DbErr::Custom( + "No policy id found in asset subject".to_string(), + )); + }; + let asset_name = if let Some(asset_name) = split.next() { + asset_name.to_string() + } else { + return Err(DbErr::Custom( + "No asset name found in asset subject".to_string(), + )); + }; + if let Some(next) = split.next() { + return Err(DbErr::Custom(format!( + "Extra information is found in asset: {next}" + ))); + } + Ok(AssetData { + policy_id, + asset_name, + amount, + }) + } +} + #[derive(Debug, Clone, Default)] struct ProjectedNftData { pub previous_utxo_tx_hash: Vec, @@ -518,7 +573,7 @@ struct ProjectedNftData { pub for_how_long: Option, // this field is set only on unlocking outputs that were created through partial withdraw pub partial_withdrawn_from_input: Option<(Vec, i64)>, - pub non_ada_assets: Vec<(String, i64)>, + pub non_ada_assets: Vec, pub hololocker_utxo_id: i64, } @@ -583,16 +638,19 @@ fn extract_operation_and_datum( let non_ada_assets = output .non_ada_assets() .iter() - .map(|asset| { - ( - asset.subject(), - match asset { - Asset::Ada(value) => *value as i64, - Asset::NativeAsset(_, _, value) => *value as i64, - }, - ) + .map(|asset| match asset { + Asset::Ada(value) => AssetData { + policy_id: "".to_string(), + asset_name: "".to_string(), + amount: *value as i64, + }, + Asset::NativeAsset(policy_id, asset_name, value) => AssetData { + policy_id: hex::encode(policy_id), + asset_name: hex::encode(asset_name.clone()), + amount: *value as i64, + }, }) - .collect::>(); + .collect::>(); match parsed.status { Status::Locked => ProjectedNftData { address: owner_address, diff --git a/webserver/server/app/controllers/ProjectedNftRangeController.ts b/webserver/server/app/controllers/ProjectedNftRangeController.ts index dc12e7f6..344f43ae 100644 --- a/webserver/server/app/controllers/ProjectedNftRangeController.ts +++ b/webserver/server/app/controllers/ProjectedNftRangeController.ts @@ -37,7 +37,9 @@ export class ProjectedNftRangeController extends Controller { previousTxOutputIndex: data.previous_tx_output_index != null ? parseInt(data.previous_tx_output_index) : null, actionTxId: data.action_tx_id, actionOutputIndex: data.action_output_index, - asset: data.asset, + asset: `${data.policy_id}.${data.asset_name}`, + policyId: data.policy_id, + assetName: data.asset_name, amount: data.amount, status: data.status, plutusDatum: data.plutus_datum, diff --git a/webserver/server/app/models/projected_nft/projectedNftRange.queries.ts b/webserver/server/app/models/projected_nft/projectedNftRange.queries.ts index c4e97121..07cc2489 100644 --- a/webserver/server/app/models/projected_nft/projectedNftRange.queries.ts +++ b/webserver/server/app/models/projected_nft/projectedNftRange.queries.ts @@ -13,10 +13,11 @@ export interface ISqlProjectedNftRangeResult { action_slot: number; action_tx_id: string | null; amount: string; - asset: string; + asset_name: string; for_how_long: string | null; owner_address: string | null; plutus_datum: string | null; + policy_id: string; previous_tx_hash: string | null; previous_tx_output_index: string | null; status: string | null; @@ -28,7 +29,7 @@ export interface ISqlProjectedNftRangeQuery { result: ISqlProjectedNftRangeResult; } -const sqlProjectedNftRangeIR: any = {"usedParamSet":{"min_slot":true,"max_slot":true},"params":[{"name":"min_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":1166,"b":1175}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":1201,"b":1210}]}],"statement":"SELECT\n encode(\"ProjectedNFT\".owner_address, 'hex') as owner_address,\n\n encode(\"ProjectedNFT\".previous_utxo_tx_hash, 'hex') as previous_tx_hash,\n \"ProjectedNFT\".previous_utxo_tx_output_index as previous_tx_output_index,\n\n CASE\n WHEN \"TransactionOutput\".output_index = NULL THEN NULL\n ELSE \"TransactionOutput\".output_index\n END AS action_output_index,\n\n encode(\"Transaction\".hash, 'hex') as action_tx_id,\n\n \"ProjectedNFT\".asset as asset,\n \"ProjectedNFT\".amount as amount,\n\n CASE\n WHEN \"ProjectedNFT\".operation = 0 THEN 'Lock'\n WHEN \"ProjectedNFT\".operation = 1 THEN 'Unlocking'\n WHEN \"ProjectedNFT\".operation = 2 THEN 'Claim'\n ELSE 'Invalid'\n END AS status,\n\n encode(\"ProjectedNFT\".plutus_datum, 'hex') as plutus_datum,\n \"ProjectedNFT\".for_how_long as for_how_long,\n\n \"Block\".slot as action_slot\nFROM \"ProjectedNFT\"\n LEFT JOIN \"TransactionOutput\" ON \"TransactionOutput\".id = \"ProjectedNFT\".hololocker_utxo_id\n JOIN \"Transaction\" ON \"Transaction\".id = \"ProjectedNFT\".tx_id\n JOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\nWHERE\n \"Block\".slot > :min_slot!\n AND \"Block\".slot <= :max_slot!\nORDER BY (\"Block\".height, \"Transaction\".tx_index) ASC"}; +const sqlProjectedNftRangeIR: any = {"usedParamSet":{"min_slot":true,"max_slot":true},"params":[{"name":"min_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":1219,"b":1228}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":1254,"b":1263}]}],"statement":"SELECT\n encode(\"ProjectedNFT\".owner_address, 'hex') as owner_address,\n\n encode(\"ProjectedNFT\".previous_utxo_tx_hash, 'hex') as previous_tx_hash,\n \"ProjectedNFT\".previous_utxo_tx_output_index as previous_tx_output_index,\n\n CASE\n WHEN \"TransactionOutput\".output_index = NULL THEN NULL\n ELSE \"TransactionOutput\".output_index\n END AS action_output_index,\n\n encode(\"Transaction\".hash, 'hex') as action_tx_id,\n\n \"ProjectedNFT\".policy_id as policy_id,\n \"ProjectedNFT\".asset_name as asset_name,\n \"ProjectedNFT\".amount as amount,\n\n CASE\n WHEN \"ProjectedNFT\".operation = 0 THEN 'Lock'\n WHEN \"ProjectedNFT\".operation = 1 THEN 'Unlocking'\n WHEN \"ProjectedNFT\".operation = 2 THEN 'Claim'\n ELSE 'Invalid'\n END AS status,\n\n encode(\"ProjectedNFT\".plutus_datum, 'hex') as plutus_datum,\n \"ProjectedNFT\".for_how_long as for_how_long,\n\n \"Block\".slot as action_slot\nFROM \"ProjectedNFT\"\n LEFT JOIN \"TransactionOutput\" ON \"TransactionOutput\".id = \"ProjectedNFT\".hololocker_utxo_id\n JOIN \"Transaction\" ON \"Transaction\".id = \"ProjectedNFT\".tx_id\n JOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\nWHERE\n \"Block\".slot > :min_slot!\n AND \"Block\".slot <= :max_slot!\nORDER BY (\"Block\".height, \"Transaction\".tx_index) ASC"}; /** * Query generated from SQL: @@ -46,7 +47,8 @@ const sqlProjectedNftRangeIR: any = {"usedParamSet":{"min_slot":true,"max_slot": * * encode("Transaction".hash, 'hex') as action_tx_id, * - * "ProjectedNFT".asset as asset, + * "ProjectedNFT".policy_id as policy_id, + * "ProjectedNFT".asset_name as asset_name, * "ProjectedNFT".amount as amount, * * CASE diff --git a/webserver/server/app/models/projected_nft/projectedNftRange.sql b/webserver/server/app/models/projected_nft/projectedNftRange.sql index ede1b88d..df00dc11 100644 --- a/webserver/server/app/models/projected_nft/projectedNftRange.sql +++ b/webserver/server/app/models/projected_nft/projectedNftRange.sql @@ -14,7 +14,8 @@ SELECT encode("Transaction".hash, 'hex') as action_tx_id, - "ProjectedNFT".asset as asset, + "ProjectedNFT".policy_id as policy_id, + "ProjectedNFT".asset_name as asset_name, "ProjectedNFT".amount as amount, CASE diff --git a/webserver/shared/models/ProjectedNftRange.ts b/webserver/shared/models/ProjectedNftRange.ts index 20b4a1ea..a7a35d47 100644 --- a/webserver/shared/models/ProjectedNftRange.ts +++ b/webserver/shared/models/ProjectedNftRange.ts @@ -72,6 +72,20 @@ export type ProjectedNftRangeResponse = { * @example "96f7dc9749ede0140f042516f4b723d7261610d6b12ccb19f3475278.415045" */ asset: string, + /** + * Asset policy id that relates to Projected NFT event + * + * @pattern [0-9a-fA-F]+.[0-9a-fA-F]+ + * @example "96f7dc9749ede0140f042516f4b723d7261610d6b12ccb19f3475278" + */ + policyId: string, + /** + * Asset name that relates to Projected NFT event + * + * @pattern [0-9a-fA-F]+.[0-9a-fA-F]+ + * @example "415045" + */ + assetName: string, /** * Number of assets of `asset` type used in this Projected NFT event. *