diff --git a/docs/bin/openapi.json b/docs/bin/openapi.json index 59613d30..921409ca 100644 --- a/docs/bin/openapi.json +++ b/docs/bin/openapi.json @@ -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", @@ -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", @@ -921,7 +938,8 @@ "plutusDatum", "status", "amount", - "asset", + "assetName", + "policyId", "previousTxOutputIndex", "previousTxHash", "actionOutputIndex", @@ -935,6 +953,9 @@ }, "ProjectedNftRangeRequest": { "properties": { + "address": { + "type": "string" + }, "range": { "properties": { "maxSlot": { diff --git a/docs/docs/indexer/Tasks/MultiEraProjectedNftTask.md b/docs/docs/indexer/Tasks/MultiEraProjectedNftTask.md index a0300c4b..2aa5f7c9 100644 --- a/docs/docs/indexer/Tasks/MultiEraProjectedNftTask.md +++ b/docs/docs/indexer/Tasks/MultiEraProjectedNftTask.md @@ -6,13 +6,13 @@ Parses projected NFT contract data Configuration ```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, } ``` 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..11f77367 100644 --- a/webserver/server/app/controllers/ProjectedNftRangeController.ts +++ b/webserver/server/app/controllers/ProjectedNftRangeController.ts @@ -5,8 +5,10 @@ import pool from '../services/PgPoolSingleton'; import type { ErrorShape } from '../../../shared/errors'; import type { EndpointTypes } from '../../../shared/routes'; import { Routes } from '../../../shared/routes'; -import { projectedNftRange } from '../services/ProjectedNftRange'; -import type {ProjectedNftRangeResponse} from '../../../shared/models/ProjectedNftRange'; +import { projectedNftRange, projectedNftRangeByAddress } from '../services/ProjectedNftRange'; +import type {ProjectedNftRangeResponse, ProjectedNftStatus} from '../../../shared/models/ProjectedNftRange'; +import {PROJECTED_NFT_LIMIT} from "../../../shared/constants"; +import {Errors, genErrorMessage} from "../../../shared/errors"; const route = Routes.projectedNftEventsRange; @@ -22,6 +24,40 @@ export class ProjectedNftRangeController extends Controller { StatusCodes.BAD_REQUEST | StatusCodes.CONFLICT | StatusCodes.UNPROCESSABLE_ENTITY, ErrorShape > + ): Promise { + const slotRangeSize = requestBody.range.maxSlot - requestBody.range.minSlot; + + if (requestBody.address !== undefined) { + if (slotRangeSize > PROJECTED_NFT_LIMIT.SINGLE_USER_SLOT_RANGE) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return errorResponse( + StatusCodes.BAD_REQUEST, + genErrorMessage(Errors.SlotRangeLimitExceeded, { + limit: PROJECTED_NFT_LIMIT.SINGLE_USER_SLOT_RANGE, + found: slotRangeSize, + }) + ); + } + + return await this.handle_by_address_query(requestBody.address, requestBody); + } else { + if (slotRangeSize > PROJECTED_NFT_LIMIT.SLOT_RANGE) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return errorResponse( + StatusCodes.BAD_REQUEST, + genErrorMessage(Errors.SlotRangeLimitExceeded, { + limit: PROJECTED_NFT_LIMIT.SLOT_RANGE, + found: slotRangeSize, + }) + ); + } + + return await this.handle_general_query(requestBody); + } + } + + async handle_general_query( + requestBody: EndpointTypes[typeof route]['input'], ): Promise { const response = await tx< ProjectedNftRangeResponse @@ -37,9 +73,42 @@ 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, + policyId: data.policy_id, + assetName: data.asset_name, + amount: data.amount, + status: data.status as ProjectedNftStatus | null, + plutusDatum: data.plutus_datum, + actionSlot: data.action_slot, + forHowLong: data.for_how_long, + })); + }); + + return response; + } + + async handle_by_address_query( + address: string, + requestBody: EndpointTypes[typeof route]['input'], + ): Promise { + const response = await tx< + ProjectedNftRangeResponse + >(pool, async dbTx => { + const data = await projectedNftRangeByAddress({ + address: address, + range: requestBody.range, + dbTx + }); + + return data.map(data => ({ + ownerAddress: data.owner_address, + previousTxHash: data.previous_tx_hash, + previousTxOutputIndex: data.previous_tx_output_index != null ? parseInt(data.previous_tx_output_index) : null, + actionTxId: data.action_tx_id, + actionOutputIndex: data.action_output_index, + policyId: data.policy_id, + assetName: data.asset_name, amount: data.amount, - status: data.status, + status: data.status as ProjectedNftStatus | null, plutusDatum: data.plutus_datum, actionSlot: data.action_slot, forHowLong: data.for_how_long, 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/server/app/models/projected_nft/projectedNftRangeByAddress.queries.ts b/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.queries.ts new file mode 100644 index 00000000..15ed1558 --- /dev/null +++ b/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.queries.ts @@ -0,0 +1,79 @@ +/** Types generated for queries found in "app/models/projected_nft/projectedNftRangeByAddress.sql" */ +import { PreparedQuery } from '@pgtyped/query'; + +/** 'SqlProjectedNftRangeByAddress' parameters type */ +export interface ISqlProjectedNftRangeByAddressParams { + max_slot: number; + min_slot: number; + owner_address: string; +} + +/** 'SqlProjectedNftRangeByAddress' return type */ +export interface ISqlProjectedNftRangeByAddressResult { + action_output_index: number | null; + action_slot: number; + action_tx_id: string | null; + amount: 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; +} + +/** 'SqlProjectedNftRangeByAddress' query type */ +export interface ISqlProjectedNftRangeByAddressQuery { + params: ISqlProjectedNftRangeByAddressParams; + result: ISqlProjectedNftRangeByAddressResult; +} + +const sqlProjectedNftRangeByAddressIR: any = {"usedParamSet":{"owner_address":true,"min_slot":true,"max_slot":true},"params":[{"name":"owner_address","required":true,"transform":{"type":"scalar"},"locs":[{"a":1250,"b":1264}]},{"name":"min_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":1289,"b":1298}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":1324,"b":1333}]}],"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 encode(\"ProjectedNFT\".owner_address, 'hex') = :owner_address!\n AND \"Block\".slot > :min_slot!\n AND \"Block\".slot <= :max_slot!\nORDER BY (\"Block\".height, \"Transaction\".tx_index) ASC"}; + +/** + * Query generated from SQL: + * ``` + * SELECT + * encode("ProjectedNFT".owner_address, 'hex') as owner_address, + * + * encode("ProjectedNFT".previous_utxo_tx_hash, 'hex') as previous_tx_hash, + * "ProjectedNFT".previous_utxo_tx_output_index as previous_tx_output_index, + * + * CASE + * WHEN "TransactionOutput".output_index = NULL THEN NULL + * ELSE "TransactionOutput".output_index + * END AS action_output_index, + * + * encode("Transaction".hash, 'hex') as action_tx_id, + * + * "ProjectedNFT".policy_id as policy_id, + * "ProjectedNFT".asset_name as asset_name, + * "ProjectedNFT".amount as amount, + * + * CASE + * WHEN "ProjectedNFT".operation = 0 THEN 'Lock' + * WHEN "ProjectedNFT".operation = 1 THEN 'Unlocking' + * WHEN "ProjectedNFT".operation = 2 THEN 'Claim' + * ELSE 'Invalid' + * END AS status, + * + * encode("ProjectedNFT".plutus_datum, 'hex') as plutus_datum, + * "ProjectedNFT".for_how_long as for_how_long, + * + * "Block".slot as action_slot + * FROM "ProjectedNFT" + * LEFT JOIN "TransactionOutput" ON "TransactionOutput".id = "ProjectedNFT".hololocker_utxo_id + * JOIN "Transaction" ON "Transaction".id = "ProjectedNFT".tx_id + * JOIN "Block" ON "Transaction".block_id = "Block".id + * WHERE + * encode("ProjectedNFT".owner_address, 'hex') = :owner_address! + * AND "Block".slot > :min_slot! + * AND "Block".slot <= :max_slot! + * ORDER BY ("Block".height, "Transaction".tx_index) ASC + * ``` + */ +export const sqlProjectedNftRangeByAddress = new PreparedQuery(sqlProjectedNftRangeByAddressIR); + + diff --git a/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.sql b/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.sql new file mode 100644 index 00000000..8aae388c --- /dev/null +++ b/webserver/server/app/models/projected_nft/projectedNftRangeByAddress.sql @@ -0,0 +1,40 @@ +/* +@name sqlProjectedNftRangeByAddress +*/ +SELECT + encode("ProjectedNFT".owner_address, 'hex') as owner_address, + + encode("ProjectedNFT".previous_utxo_tx_hash, 'hex') as previous_tx_hash, + "ProjectedNFT".previous_utxo_tx_output_index as previous_tx_output_index, + + CASE + WHEN "TransactionOutput".output_index = NULL THEN NULL + ELSE "TransactionOutput".output_index + END AS action_output_index, + + encode("Transaction".hash, 'hex') as action_tx_id, + + "ProjectedNFT".policy_id as policy_id, + "ProjectedNFT".asset_name as asset_name, + "ProjectedNFT".amount as amount, + + CASE + WHEN "ProjectedNFT".operation = 0 THEN 'Lock' + WHEN "ProjectedNFT".operation = 1 THEN 'Unlocking' + WHEN "ProjectedNFT".operation = 2 THEN 'Claim' + ELSE 'Invalid' + END AS status, + + encode("ProjectedNFT".plutus_datum, 'hex') as plutus_datum, + "ProjectedNFT".for_how_long as for_how_long, + + "Block".slot as action_slot +FROM "ProjectedNFT" + LEFT JOIN "TransactionOutput" ON "TransactionOutput".id = "ProjectedNFT".hololocker_utxo_id + JOIN "Transaction" ON "Transaction".id = "ProjectedNFT".tx_id + JOIN "Block" ON "Transaction".block_id = "Block".id +WHERE + encode("ProjectedNFT".owner_address, 'hex') = :owner_address! + AND "Block".slot > :min_slot! + AND "Block".slot <= :max_slot! +ORDER BY ("Block".height, "Transaction".tx_index) ASC; diff --git a/webserver/server/app/services/ProjectedNftRange.ts b/webserver/server/app/services/ProjectedNftRange.ts index 83d0eb7f..49d0dd26 100644 --- a/webserver/server/app/services/ProjectedNftRange.ts +++ b/webserver/server/app/services/ProjectedNftRange.ts @@ -1,6 +1,7 @@ import type { PoolClient } from 'pg'; import type { ISqlProjectedNftRangeResult} from '../models/projected_nft/projectedNftRange.queries'; import { sqlProjectedNftRange } from '../models/projected_nft/projectedNftRange.queries'; +import { sqlProjectedNftRangeByAddress } from '../models/projected_nft/projectedNftRangeByAddress.queries'; export async function projectedNftRange(request: { range: { minSlot: number, maxSlot: number }, @@ -11,3 +12,15 @@ export async function projectedNftRange(request: { max_slot: request.range.maxSlot, }, request.dbTx)); } + +export async function projectedNftRangeByAddress(request: { + address: string, + range: { minSlot: number, maxSlot: number }, + dbTx: PoolClient, +}): Promise { + return (await sqlProjectedNftRangeByAddress.run({ + owner_address: request.address, + min_slot: request.range.minSlot, + max_slot: request.range.maxSlot, + }, request.dbTx)); +} diff --git a/webserver/shared/constants.ts b/webserver/shared/constants.ts index fad51cd6..725412aa 100644 --- a/webserver/shared/constants.ts +++ b/webserver/shared/constants.ts @@ -27,6 +27,11 @@ export const DEX_PRICE_LIMIT = { RESPONSE: 1000, }; +export const PROJECTED_NFT_LIMIT = { + SLOT_RANGE: 100000, + SINGLE_USER_SLOT_RANGE: 10000000000, +}; + export const POOL_DELEGATION_LIMIT = { POOLS: 50, SLOT_RANGE: 10000, diff --git a/webserver/shared/models/ProjectedNftRange.ts b/webserver/shared/models/ProjectedNftRange.ts index 20b4a1ea..e14f365b 100644 --- a/webserver/shared/models/ProjectedNftRange.ts +++ b/webserver/shared/models/ProjectedNftRange.ts @@ -15,7 +15,15 @@ export type ProjectedNftRangeRequest = { * @example 46154860 */ maxSlot: number - } + }, + address: string | undefined +}; + +export enum ProjectedNftStatus { + Lock = 'Lock', + Unlocking = 'Unlocking', + Claim = 'Claim', + Invalid = 'Invalid' }; export type ProjectedNftRangeResponse = { @@ -64,14 +72,20 @@ export type ProjectedNftRangeResponse = { * @example 1 */ previousTxOutputIndex: number | null, - /** - * Asset that relates to Projected NFT event. Consists of 2 parts: PolicyId and AssetName + * Asset policy id that relates to Projected NFT event + * + * @pattern [0-9a-fA-F]{56} + * @example "96f7dc9749ede0140f042516f4b723d7261610d6b12ccb19f3475278" + */ + policyId: string, + /** + * Asset name that relates to Projected NFT event * - * @pattern [0-9a-fA-F]+.[0-9a-fA-F]+ - * @example "96f7dc9749ede0140f042516f4b723d7261610d6b12ccb19f3475278.415045" + * @pattern ([0-9a-fA-F]{2}){0,32} + * @example "415045" */ - asset: string, + assetName: string, /** * Number of assets of `asset` type used in this Projected NFT event. * @@ -83,7 +97,7 @@ export type ProjectedNftRangeResponse = { * * @example "Lock" */ - status: string | null, + status: ProjectedNftStatus | null, /** * Projected NFT datum: serialized state of the Projected NFT *