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
*