Skip to content

Commit

Permalink
Split asset name and policy id for simplified future searches (#167)
Browse files Browse the repository at this point in the history
* Split asset name and policy id for simplified future searches

* Add query by address

* Make address undefined

* Make separate slot ranges for specified and not specified address

* remove asset

* Remove strict pagination

* Revert "Remove strict pagination"

This reverts commit 7b9117b.

* Update docs

* fix merge conflict

---------

Co-authored-by: Sebastien Guillemot <[email protected]>
  • Loading branch information
gostkin and SebastienGllmt authored Dec 26, 2023
1 parent 0c50934 commit 97c8d4a
Show file tree
Hide file tree
Showing 13 changed files with 364 additions and 60 deletions.
45 changes: 33 additions & 12 deletions docs/bin/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -839,15 +839,23 @@
],
"type": "object"
},
"ProjectedNftStatus": {
"enum": [
"Lock",
"Unlocking",
"Claim",
"Invalid"
],
"type": "string"
},
"ProjectedNftRangeResponse": {
"items": {
"properties": {
"forHowLong": {
"type": "number",
"format": "double",
"type": "string",
"nullable": true,
"description": "UNIX timestamp till which the funds can't be claimed in the Unlocking state.\nIf the status is not Unlocking this is always null.",
"example": 1701266986000
"example": "1701266986000"
},
"plutusDatum": {
"type": "string",
Expand All @@ -857,22 +865,31 @@
"pattern": "[0-9a-fA-F]+"
},
"status": {
"type": "string",
"allOf": [
{
"$ref": "#/components/schemas/ProjectedNftStatus"
}
],
"nullable": true,
"description": "Projected NFT status: Lock / Unlocking / Claim / Invalid",
"example": "Lock"
},
"amount": {
"type": "number",
"format": "double",
"type": "string",
"description": "Number of assets of `asset` type used in this Projected NFT event.",
"example": 1
"example": "1"
},
"asset": {
"assetName": {
"type": "string",
"description": "Asset that relates to Projected NFT event. Consists of 2 parts: PolicyId and AssetName",
"example": "96f7dc9749ede0140f042516f4b723d7261610d6b12ccb19f3475278.415045",
"pattern": "[0-9a-fA-F]+.[0-9a-fA-F]+"
"description": "Asset name that relates to Projected NFT event",
"example": "415045",
"pattern": "([0-9a-fA-F]{2}){0,32}"
},
"policyId": {
"type": "string",
"description": "Asset policy id that relates to Projected NFT event",
"example": "96f7dc9749ede0140f042516f4b723d7261610d6b12ccb19f3475278",
"pattern": "[0-9a-fA-F]{56}"
},
"previousTxOutputIndex": {
"type": "number",
Expand Down Expand Up @@ -921,7 +938,8 @@
"plutusDatum",
"status",
"amount",
"asset",
"assetName",
"policyId",
"previousTxOutputIndex",
"previousTxHash",
"actionOutputIndex",
Expand All @@ -935,6 +953,9 @@
},
"ProjectedNftRangeRequest": {
"properties": {
"address": {
"type": "string"
},
"range": {
"properties": {
"maxSlot": {
Expand Down
12 changes: 6 additions & 6 deletions docs/docs/indexer/Tasks/MultiEraProjectedNftTask.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ Parses projected NFT contract data
<summary>Configuration</summary>

```rust
use super::PayloadConfig::PayloadConfig;
use super::ReadonlyConfig::ReadonlyConfig;
use pallas::ledger::addresses::Address;
use pallas::ledger::primitives::alonzo::PlutusScript;
use pallas::ledger::primitives::babbage::PlutusV2Script;

#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)]
pub struct PayloadAndReadonlyConfig {
pub include_payload: bool,
pub readonly: bool,
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct AddressConfig {
pub address: String,
}

```
Expand Down
3 changes: 2 additions & 1 deletion indexer/entity/src/projected_nft.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ pub struct Model {
pub hololocker_utxo_id: Option<i64>,
#[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
Expand Down
3 changes: 2 additions & 1 deletion indexer/migration/src/m20231025_000017_projected_nft.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
108 changes: 83 additions & 25 deletions indexer/tasks/src/multiera/multiera_projected_nft.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use anyhow::anyhow;
use cardano_multiplatform_lib::error::DeserializeError;
use cml_core::serialization::FromBytes;
use cml_crypto::RawBytesEncoding;
Expand Down Expand Up @@ -102,11 +101,18 @@ pub(crate) struct ProjectedNftInputsQueryOutputResult {
pub tx_hash: Vec<u8>,
pub operation: i32,
pub owner_address: Vec<u8>,
pub asset: String,
pub policy_id: String,
pub asset_name: String,
pub amount: i64,
pub plutus_datum: Vec<u8>,
}

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>,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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<u8>, i64)> = None;

Expand All @@ -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::<Vec<_>>();
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));
Expand Down Expand Up @@ -316,18 +327,19 @@ fn handle_partial_withdraw(
// make a balance map
let mut input_asset_to_value = HashMap::<String, ProjectedNftInputsQueryOutputResult>::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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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![]),
Expand Down Expand Up @@ -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<AssetData, DbErr> {
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<u8>,
Expand All @@ -518,7 +573,7 @@ struct ProjectedNftData {
pub for_how_long: Option<i64>,
// this field is set only on unlocking outputs that were created through partial withdraw
pub partial_withdrawn_from_input: Option<(Vec<u8>, i64)>,
pub non_ada_assets: Vec<(String, i64)>,
pub non_ada_assets: Vec<AssetData>,
pub hololocker_utxo_id: i64,
}

Expand Down Expand Up @@ -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::<Vec<(String, i64)>>();
.collect::<Vec<AssetData>>();
match parsed.status {
Status::Locked => ProjectedNftData {
address: owner_address,
Expand Down
Loading

0 comments on commit 97c8d4a

Please sign in to comment.