Skip to content

Commit

Permalink
feat(analytics): add richest-addresses and token-distribution end…
Browse files Browse the repository at this point in the history
…points (#523)

* Use outputs table for all analytics, and add additional endpoints.

* Fix start/end timestamps

* Fix Eq derives

* Add richlist endpoint

* Update storage deposit endpoint and add more output endpoints

* Cleanup

* Add block analytics and stringify responses

* I should really test before pushing

* 🤦‍♂️

* Revert native token

* missed merge changes

* Use bee rent structure

* Refactor routes and add additional ledger analytics. Use milestone indexes instead of timestamps.

* Fix addresses analytics

* Fix up data types and add examples for API spec. Move richlist to ledger endpoints.

* Split richlist endpoint

* Rename wealth
  • Loading branch information
DaughterOfMars committed Aug 17, 2022
1 parent 70fe622 commit 99049b6
Show file tree
Hide file tree
Showing 6 changed files with 406 additions and 6 deletions.
32 changes: 32 additions & 0 deletions bin/inx-chronicle/src/api/stardust/analytics/extractors.rs
Expand Up @@ -8,6 +8,38 @@ use serde::Deserialize;

use crate::api::ApiError;

const MAX_TOP_RICHLIST: usize = 1000;
const DEFAULT_TOP_RICHLIST: usize = 100;

#[derive(Clone, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct RichestAddressesQuery {
pub top: usize,
pub ledger_index: Option<MilestoneIndex>,
}

impl Default for RichestAddressesQuery {
fn default() -> Self {
Self {
top: DEFAULT_TOP_RICHLIST,
ledger_index: None,
}
}
}

#[async_trait]
impl<B: Send> FromRequest<B> for RichestAddressesQuery {
type Rejection = ApiError;

async fn from_request(req: &mut axum::extract::RequestParts<B>) -> Result<Self, Self::Rejection> {
let Query(mut query) = Query::<RichestAddressesQuery>::from_request(req)
.await
.map_err(ApiError::QueryError)?;
query.top = query.top.min(MAX_TOP_RICHLIST);
Ok(query)
}
}

#[derive(Copy, Clone, Deserialize, Default)]
#[serde(default, deny_unknown_fields, rename_all = "camelCase")]
pub struct LedgerIndex {
Expand Down
55 changes: 55 additions & 0 deletions bin/inx-chronicle/src/api/stardust/analytics/responses.rs
@@ -1,7 +1,11 @@
// Copyright 2022 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use std::ops::Range;

use bee_api_types_stardust::responses::RentStructureResponse;
use bee_block_stardust::address::dto::AddressDto;
use chronicle::db::collections::{AddressStat, DistributionStat};
use serde::{Deserialize, Serialize};

use crate::api::responses::impl_success_response;
Expand Down Expand Up @@ -50,3 +54,54 @@ pub struct StorageDepositAnalyticsResponse {
}

impl_success_response!(StorageDepositAnalyticsResponse);

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RichestAddressesResponse {
pub top: Vec<AddressStatDto>,
pub ledger_index: u32,
}

impl_success_response!(RichestAddressesResponse);

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AddressStatDto {
pub address: AddressDto,
pub balance: String,
}

impl From<AddressStat> for AddressStatDto {
fn from(s: AddressStat) -> Self {
Self {
address: AddressDto::from(&bee_block_stardust::address::Address::from(s.address)),
balance: s.balance,
}
}
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenDistributionResponse {
pub distribution: Vec<DistributionStatDto>,
pub ledger_index: u32,
}

impl_success_response!(TokenDistributionResponse);

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DistributionStatDto {
pub range: Range<u64>,
pub address_count: String,
pub total_balance: String,
}

impl From<DistributionStat> for DistributionStatDto {
fn from(s: DistributionStat) -> Self {
Self {
range: 10_u64.pow(s.index)..10_u64.pow(s.index + 1),
address_count: s.address_count.to_string(),
total_balance: s.total_balance,
}
}
}
39 changes: 36 additions & 3 deletions bin/inx-chronicle/src/api/stardust/analytics/routes.rs
Expand Up @@ -15,9 +15,10 @@ use chronicle::{
};

use super::{
extractors::{LedgerIndex, MilestoneRange},
extractors::{LedgerIndex, MilestoneRange, RichestAddressesQuery},
responses::{
AddressAnalyticsResponse, BlockAnalyticsResponse, OutputAnalyticsResponse, StorageDepositAnalyticsResponse,
AddressAnalyticsResponse, BlockAnalyticsResponse, OutputAnalyticsResponse, RichestAddressesResponse,
StorageDepositAnalyticsResponse, TokenDistributionResponse,
},
};
use crate::api::{ApiError, ApiResult};
Expand All @@ -29,7 +30,9 @@ pub fn routes() -> Router {
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("/nfts", get(unspent_output_analytics::<NftOutput>))
.route("/richest-addresses", get(richest_addresses))
.route("/token-distribution", get(token_distribution)),
)
.nest(
"/activity",
Expand Down Expand Up @@ -134,3 +137,33 @@ async fn storage_deposit_analytics(
},
})
}

async fn richest_addresses(
database: Extension<MongoDb>,
RichestAddressesQuery { top, ledger_index }: RichestAddressesQuery,
) -> ApiResult<RichestAddressesResponse> {
let res = database
.get_richest_addresses(ledger_index, top)
.await?
.ok_or(ApiError::NoResults)?;

Ok(RichestAddressesResponse {
top: res.top.into_iter().map(Into::into).collect(),
ledger_index: res.ledger_index.0,
})
}

async fn token_distribution(
database: Extension<MongoDb>,
LedgerIndex { ledger_index }: LedgerIndex,
) -> ApiResult<TokenDistributionResponse> {
let res = database
.get_token_distribution(ledger_index)
.await?
.ok_or(ApiError::NoResults)?;

Ok(TokenDistributionResponse {
distribution: res.distribution.into_iter().map(Into::into).collect(),
ledger_index: res.ledger_index.0,
})
}
144 changes: 144 additions & 0 deletions docs/api-analytics.yml
Expand Up @@ -76,6 +76,57 @@ paths:
$ref: "#/components/responses/NoResults"
"500":
$ref: "#/components/responses/InternalError"
/api/analytics/v2/ledger/richest-addresses:
get:
tags:
- ledger
summary: Returns the top richest addresses.
description: >-
Returns analytics about the top richest addresses at the ledger state specified by the provided index
parameters:
- $ref: "#/components/parameters/ledgerIndex"
- $ref: "#/components/parameters/top"
responses:
"200":
description: Successful operation.
content:
application/json:
schema:
$ref: "#/components/schemas/RichestAddressesResponse"
examples:
default:
$ref: "#/components/examples/richest-addresses-example"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NoResults"
"500":
$ref: "#/components/responses/InternalError"
/api/analytics/v2/ledger/token-distribution:
get:
tags:
- ledger
summary: Returns the current token distribution.
description: >-
Returns analytics about the distribution of IOTA tokens at the ledger state specified by the provided index
parameters:
- $ref: "#/components/parameters/ledgerIndex"
responses:
"200":
description: Successful operation.
content:
application/json:
schema:
$ref: "#/components/schemas/WealthDistributionResponse"
examples:
default:
$ref: "#/components/examples/token-distribution-example"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NoResults"
"500":
$ref: "#/components/responses/InternalError"
/api/analytics/v2/activity/addresses:
get:
tags:
Expand Down Expand Up @@ -351,6 +402,60 @@ components:
- totalByteCost
- ledgerIndex
- rentStructure
RichestAddressesResponse:
description: Richest addresses statistics.
properties:
top:
type: array
description: The top wealthiest addresses.
items:
type: object
properties:
address:
oneOf:
- $ref: "https://raw.githubusercontent.com/iotaledger/tips/stardust-api/tips/TIP-0025/core-rest-api.yaml#/components/schemas/Ed25519Address"
- $ref: "https://raw.githubusercontent.com/iotaledger/tips/stardust-api/tips/TIP-0025/core-rest-api.yaml#/components/schemas/AliasAddress"
- $ref: "https://raw.githubusercontent.com/iotaledger/tips/stardust-api/tips/TIP-0025/core-rest-api.yaml#/components/schemas/NFTAddress"
balance:
type: string
description: The total balance within this range.
required:
- address
- balance
required:
- top
WealthDistributionResponse:
description: Wealth distribution statistics.
properties:
distribution:
type: array
description: The distribution of IOTA tokens.
items:
type: object
properties:
range:
type: object
description: The range of balances.
properties:
start:
type: number
end:
type: number
required:
- start
- end
addressCount:
type: string
description: The number of addresses in this range.
totalBalance:
type: string
description: The total balance within this range.
required:
- range
- addressCount
- totalBalance
required:
- distribution
responses:
Block:
description: Successful operation.
Expand Down Expand Up @@ -437,6 +542,14 @@ components:
description: >-
The milestone index to be used to determine the ledger state. Defaults to the
application's current ledger index.
top:
in: query
name: top
schema:
type: number
example: 100
required: false
description: The milestone index to be used to determine the ledger state. Defaults to 200.
examples:
storage-deposit-example:
value:
Expand All @@ -463,3 +576,34 @@ components:
totalActiveAddresses: "443"
receivingAddresses: "443"
sendingAddresses: "0"
richest-addresses-example:
value:
top:
- address:
- type: 0
pubKeyHash: "0x3845105b59429361a75b3203a6e24e16d19540aad6bd1936449b62f1c4bbe5da"
balance: "2779164783277761"
- address:
- type: 0
pubKeyHash: "0xd0d361341fa3bb2f6855039a82ee9ea470c3336eaf34d22767fdfa901ba63e31"
balance: "7398600000"
ledgerIndex: 1005429
token-distribution-example:
value:
distribution:
- range:
- start: 100000
end: 1000000
addressCount: "39"
totalBalance: "14612000"
- range:
- start: 1000000
end: 10000000
addressCount: "22"
totalBalance: "41274500"
- range:
- start: 100000000
end: 1000000000
addressCount: "27"
totalBalance: "25486528000"
ledgerIndex: 1005429
4 changes: 2 additions & 2 deletions src/db/collections/mod.rs
Expand Up @@ -18,8 +18,8 @@ pub use self::{
ledger_update::{LedgerUpdateByAddressRecord, LedgerUpdateByMilestoneRecord, ParseSortError, SortOrder},
milestone::SyncData,
outputs::{
AliasOutputsQuery, BasicOutputsQuery, FoundryOutputsQuery, IndexedId, NftOutputsQuery, OutputMetadataResult,
OutputWithMetadataResult, OutputsResult, UtxoChangesResult,
AddressStat, AliasOutputsQuery, BasicOutputsQuery, DistributionStat, FoundryOutputsQuery, IndexedId,
NftOutputsQuery, OutputMetadataResult, OutputWithMetadataResult, OutputsResult, UtxoChangesResult,
},
treasury::TreasuryResult,
};
Expand Down

0 comments on commit 99049b6

Please sign in to comment.