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(analytics): add nft and native token activity endpoints #560

Merged
merged 5 commits into from
Aug 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions bin/inx-chronicle/src/api/stardust/analytics/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use serde::{Deserialize, Serialize};

use crate::api::responses::impl_success_response;

/// Response of `GET /api/analytics/addresses[?start_timestamp=<i64>&end_timestamp=<i64>]`.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AddressAnalyticsResponse {
Expand All @@ -21,7 +20,6 @@ pub struct AddressAnalyticsResponse {

impl_success_response!(AddressAnalyticsResponse);

/// Response of `GET /api/analytics/transactions[?start_timestamp=<i64>&end_timestamp=<i64>]`.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OutputAnalyticsResponse {
Expand All @@ -31,7 +29,6 @@ pub struct OutputAnalyticsResponse {

impl_success_response!(OutputAnalyticsResponse);

/// Response of `GET /api/analytics/transactions[?start_timestamp=<i64>&end_timestamp=<i64>]`.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BlockAnalyticsResponse {
Expand All @@ -55,6 +52,16 @@ pub struct StorageDepositAnalyticsResponse {

impl_success_response!(StorageDepositAnalyticsResponse);

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OutputDiffAnalyticsResponse {
pub created_count: String,
pub transferred_count: String,
pub burned_count: String,
}

impl_success_response!(OutputDiffAnalyticsResponse);

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RichestAddressesResponse {
Expand Down
78 changes: 53 additions & 25 deletions bin/inx-chronicle/src/api/stardust/analytics/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ use chronicle::{
use super::{
extractors::{LedgerIndex, MilestoneRange, RichestAddressesQuery},
responses::{
AddressAnalyticsResponse, BlockAnalyticsResponse, OutputAnalyticsResponse, RichestAddressesResponse,
StorageDepositAnalyticsResponse, TokenDistributionResponse,
AddressAnalyticsResponse, BlockAnalyticsResponse, OutputAnalyticsResponse, OutputDiffAnalyticsResponse,
RichestAddressesResponse, StorageDepositAnalyticsResponse, TokenDistributionResponse,
},
};
use crate::api::{ApiError, ApiResult};
Expand All @@ -28,41 +28,43 @@ pub fn routes() -> Router {
.nest(
"/ledger",
Router::new()
.route("/storage-deposit", get(storage_deposit_analytics))
.route("/native-tokens", get(unspent_output_analytics::<FoundryOutput>))
.route("/nfts", get(unspent_output_analytics::<NftOutput>))
.route("/richest-addresses", get(richest_addresses))
.route("/token-distribution", get(token_distribution)),
.route("/storage-deposit", get(storage_deposit_ledger_analytics))
.route("/native-tokens", get(unspent_output_ledger_analytics::<FoundryOutput>))
.route("/nfts", get(unspent_output_ledger_analytics::<NftOutput>))
.route("/richest-addresses", get(richest_addresses_ledger_analytics))
.route("/token-distribution", get(token_distribution_ledger_analytics)),
)
.nest(
"/activity",
Router::new()
.route("/addresses", get(address_analytics))
.route("/addresses", get(address_activity_analytics))
.route("/native-tokens", get(native_token_activity_analytics))
.route("/nfts", get(nft_activity_analytics))
.nest(
"/blocks",
Router::new()
.route("/", get(block_analytics::<()>))
.route("/milestone", get(block_analytics::<MilestonePayload>))
.route("/transaction", get(block_analytics::<TransactionPayload>))
.route("/tagged-data", get(block_analytics::<TaggedDataPayload>))
.route("/", get(block_activity_analytics::<()>))
.route("/milestone", get(block_activity_analytics::<MilestonePayload>))
.route("/transaction", get(block_activity_analytics::<TransactionPayload>))
.route("/tagged-data", get(block_activity_analytics::<TaggedDataPayload>))
.route(
"/treasury-transaction",
get(block_analytics::<TreasuryTransactionPayload>),
get(block_activity_analytics::<TreasuryTransactionPayload>),
),
)
.nest(
"/outputs",
Router::new()
.route("/", get(output_analytics::<()>))
.route("/basic", get(output_analytics::<BasicOutput>))
.route("/alias", get(output_analytics::<AliasOutput>))
.route("/nft", get(output_analytics::<NftOutput>))
.route("/foundry", get(output_analytics::<FoundryOutput>)),
.route("/", get(output_activity_analytics::<()>))
.route("/basic", get(output_activity_analytics::<BasicOutput>))
.route("/alias", get(output_activity_analytics::<AliasOutput>))
.route("/nft", get(output_activity_analytics::<NftOutput>))
.route("/foundry", get(output_activity_analytics::<FoundryOutput>)),
),
)
}

async fn address_analytics(
async fn address_activity_analytics(
database: Extension<MongoDb>,
MilestoneRange { start_index, end_index }: MilestoneRange,
) -> ApiResult<AddressAnalyticsResponse> {
Expand All @@ -75,7 +77,7 @@ async fn address_analytics(
})
}

async fn block_analytics<B: PayloadKind>(
async fn block_activity_analytics<B: PayloadKind>(
database: Extension<MongoDb>,
MilestoneRange { start_index, end_index }: MilestoneRange,
) -> ApiResult<BlockAnalyticsResponse> {
Expand All @@ -86,7 +88,7 @@ async fn block_analytics<B: PayloadKind>(
})
}

async fn output_analytics<O: OutputKind>(
async fn output_activity_analytics<O: OutputKind>(
database: Extension<MongoDb>,
MilestoneRange { start_index, end_index }: MilestoneRange,
) -> ApiResult<OutputAnalyticsResponse> {
Expand All @@ -98,7 +100,7 @@ async fn output_analytics<O: OutputKind>(
})
}

async fn unspent_output_analytics<O: OutputKind>(
async fn unspent_output_ledger_analytics<O: OutputKind>(
database: Extension<MongoDb>,
LedgerIndex { ledger_index }: LedgerIndex,
) -> ApiResult<OutputAnalyticsResponse> {
Expand All @@ -113,7 +115,7 @@ async fn unspent_output_analytics<O: OutputKind>(
})
}

async fn storage_deposit_analytics(
async fn storage_deposit_ledger_analytics(
database: Extension<MongoDb>,
LedgerIndex { ledger_index }: LedgerIndex,
) -> ApiResult<StorageDepositAnalyticsResponse> {
Expand All @@ -138,7 +140,33 @@ async fn storage_deposit_analytics(
})
}

async fn richest_addresses(
async fn nft_activity_analytics(
database: Extension<MongoDb>,
MilestoneRange { start_index, end_index }: MilestoneRange,
) -> ApiResult<OutputDiffAnalyticsResponse> {
let res = database.get_nft_output_analytics(start_index, end_index).await?;

Ok(OutputDiffAnalyticsResponse {
created_count: res.created_count.to_string(),
transferred_count: res.transferred_count.to_string(),
burned_count: res.burned_count.to_string(),
})
}

async fn native_token_activity_analytics(
database: Extension<MongoDb>,
MilestoneRange { start_index, end_index }: MilestoneRange,
) -> ApiResult<OutputDiffAnalyticsResponse> {
let res = database.get_foundry_output_analytics(start_index, end_index).await?;

Ok(OutputDiffAnalyticsResponse {
created_count: res.created_count.to_string(),
transferred_count: res.transferred_count.to_string(),
burned_count: res.burned_count.to_string(),
})
}

async fn richest_addresses_ledger_analytics(
database: Extension<MongoDb>,
RichestAddressesQuery { top, ledger_index }: RichestAddressesQuery,
) -> ApiResult<RichestAddressesResponse> {
Expand All @@ -153,7 +181,7 @@ async fn richest_addresses(
})
}

async fn token_distribution(
async fn token_distribution_ledger_analytics(
database: Extension<MongoDb>,
LedgerIndex { ledger_index }: LedgerIndex,
) -> ApiResult<TokenDistributionResponse> {
Expand Down
125 changes: 123 additions & 2 deletions src/db/collections/outputs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ impl MongoDb {
"_id": null,
"count": { "$sum": 1 },
"total_value": { "$sum": { "$toDecimal": "$output.amount" } },
}},
} },
doc! { "$project": {
"count": 1,
"total_value": { "$toString": "$total_value" },
Expand Down Expand Up @@ -447,7 +447,7 @@ impl MongoDb {
"_id": null,
"count": { "$sum": 1 },
"total_value": { "$sum": { "$toDecimal": "$output.amount" } },
}},
} },
doc! { "$project": {
"count": 1,
"total_value": { "$toString": "$total_value" },
Expand All @@ -468,6 +468,127 @@ impl MongoDb {
}
}

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct OutputDiffAnalyticsResult {
pub created_count: u64,
pub transferred_count: u64,
pub burned_count: u64,
}

impl MongoDb {
/// Gathers nft output analytics.
pub async fn get_nft_output_analytics(
&self,
start_index: Option<MilestoneIndex>,
end_index: Option<MilestoneIndex>,
) -> Result<OutputDiffAnalyticsResult, Error> {
Ok(self
.db
.collection::<OutputDiffAnalyticsResult>(OutputDocument::COLLECTION)
.aggregate(
vec![
doc! { "$match": {
"output.kind": "nft",
"metadata.booked.milestone_index": { "$not": { "$gt": start_index } },
DaughterOfMars marked this conversation as resolved.
Show resolved Hide resolved
} },
doc! { "$facet": {
"start_state": [
{ "$match": {
"$or": [
{ "metadata.spent_metadata.spent": null },
{ "metadata.spent_metadata.spent.milestone_index": { "$not": { "$lte": start_index } } },
],
} },
{ "$project": {
"nft_id": "$output.nft_id"
} },
],
"end_state": [
{ "$match": {
"metadata.booked.milestone_index": { "$not": { "$lte": start_index } },
"$or": [
{ "metadata.spent_metadata.spent": null },
{ "metadata.spent_metadata.spent.milestone_index": { "$not": { "$lte": end_index } } },
],
} },
{ "$project": {
"nft_id": "$output.nft_id"
} },
],
} },
doc! { "$project": {
"created_count": { "$size": { "$setDifference": [ "$end_state", "$start_state" ] } },
"transferred_count": { "$size": { "$setIntersection": [ "$start_state", "$end_state" ] } },
"burned_count": { "$size": { "$setDifference": [ "$start_state", "$end_state" ] } },
} },
],
None,
)
.await?
.try_next()
.await?
.map(bson::from_document)
.transpose()?
.unwrap_or_default())
}

/// Gathers foundry output analytics.
pub async fn get_foundry_output_analytics(
&self,
start_index: Option<MilestoneIndex>,
end_index: Option<MilestoneIndex>,
) -> Result<OutputDiffAnalyticsResult, Error> {
Ok(self
.db
.collection::<OutputDiffAnalyticsResult>(OutputDocument::COLLECTION)
.aggregate(
vec![
doc! { "$match": {
"output.kind": "foundry",
"metadata.booked.milestone_index": { "$not": { "$gt": start_index } },
DaughterOfMars marked this conversation as resolved.
Show resolved Hide resolved
} },
doc! { "$facet": {
"start_state": [
{ "$match": {
"$or": [
{ "metadata.spent_metadata.spent": null },
{ "metadata.spent_metadata.spent.milestone_index": { "$not": { "$lte": start_index } } },
],
} },
DaughterOfMars marked this conversation as resolved.
Show resolved Hide resolved
{ "$project": {
"foundry_id": "$output.foundry_id"
} },
],
"end_state": [
{ "$match": {
"metadata.booked.milestone_index": { "$not": { "$lte": start_index } },
DaughterOfMars marked this conversation as resolved.
Show resolved Hide resolved
"$or": [
{ "metadata.spent_metadata.spent": null },
{ "metadata.spent_metadata.spent.milestone_index": { "$not": { "$lte": end_index } } },
],
} },
{ "$project": {
"foundry_id": "$output.foundry_id"
} },
],
} },
doc! { "$project": {
"created_count": { "$size": { "$setDifference": [ "$end_state", "$start_state" ] } },
"transferred_count": { "$size": { "$setIntersection": [ "$start_state", "$end_state" ] } },
"burned_count": { "$size": { "$setDifference": [ "$start_state", "$end_state" ] } },
} },
],
None,
)
.await?
.try_next()
.await?
.map(bson::from_document)
.transpose()?
.unwrap_or_default())
}
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct StorageDepositAnalyticsResult {
pub output_count: u64,
Expand Down