Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cdd9ec3622 | |||
| fbcf44eeb9 |
@@ -59,3 +59,6 @@ nym-api/redocly/formatted-openapi.json
|
||||
|
||||
*.sqlite
|
||||
.build
|
||||
|
||||
**/settings.sql
|
||||
**/enter_db.sh
|
||||
|
||||
Generated
+1
@@ -4760,6 +4760,7 @@ dependencies = [
|
||||
"humantime-serde",
|
||||
"itertools 0.14.0",
|
||||
"k256",
|
||||
"moka",
|
||||
"nym-api-requests",
|
||||
"nym-bandwidth-controller",
|
||||
"nym-bin-common",
|
||||
|
||||
@@ -27,6 +27,7 @@ humantime-serde = { workspace = true }
|
||||
k256 = { workspace = true, features = [
|
||||
"ecdsa-core",
|
||||
] } # needed for the Verifier trait; pull whatever version is used by other dependencies
|
||||
moka = { workspace = true }
|
||||
pin-project = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
@@ -128,6 +129,7 @@ v2-performance = []
|
||||
generate-ts = ["ts-rs"]
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
sqlx = { workspace = true, features = [
|
||||
"runtime-tokio-rustls",
|
||||
|
||||
+37
-1
@@ -1,13 +1,20 @@
|
||||
use sqlx::{Connection, FromRow, SqliteConnection};
|
||||
use std::env;
|
||||
|
||||
const SQLITE_DB_FILENAME: &str = "nym-api-example.sqlite";
|
||||
|
||||
// it's fine if compilation fails
|
||||
#[allow(clippy::unwrap_used)]
|
||||
#[allow(clippy::expect_used)]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let database_path = format!("{}/nym-api-example.sqlite", out_dir);
|
||||
let database_path = format!("{}/{}", out_dir, SQLITE_DB_FILENAME);
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
write_db_path_to_file(&out_dir, SQLITE_DB_FILENAME)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let mut conn = SqliteConnection::connect(&format!("sqlite://{}?mode=rwc", database_path))
|
||||
.await
|
||||
@@ -62,3 +69,32 @@ async fn main() {
|
||||
// not a valid windows path... but hey, it works...
|
||||
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
|
||||
}
|
||||
|
||||
/// use `./enter_db.sh` to inspect DB
|
||||
#[cfg(target_family = "unix")]
|
||||
async fn write_db_path_to_file(out_dir: &str, db_filename: &str) -> anyhow::Result<()> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use tokio::{fs::File, io::AsyncWriteExt};
|
||||
|
||||
if env::var("CI").is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut file = File::create("settings.sql").await?;
|
||||
let settings = ".mode columns
|
||||
.headers on";
|
||||
file.write_all(settings.as_bytes()).await?;
|
||||
|
||||
let mut file = File::create("enter_db.sh").await?;
|
||||
let contents = format!(
|
||||
"#!/bin/sh\n\
|
||||
sqlite3 -init settings.sql {}/{}",
|
||||
out_dir, db_filename,
|
||||
);
|
||||
file.write_all(contents.as_bytes()).await?;
|
||||
|
||||
file.set_permissions(std::fs::Permissions::from_mode(0o755))
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use crate::support::config;
|
||||
use crate::support::http::state::{AppState, ChainStatusCache, ForcedRefresh};
|
||||
use crate::support::nyxd::Client;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use crate::unstable_routes::account::cache::AddressInfoCache;
|
||||
use async_trait::async_trait;
|
||||
use axum::Router;
|
||||
use axum_test::http::StatusCode;
|
||||
@@ -1274,6 +1275,7 @@ impl TestFixture {
|
||||
AppState {
|
||||
nyxd_client,
|
||||
chain_status_cache: ChainStatusCache::new(Duration::from_secs(42)),
|
||||
address_info_cache: AddressInfoCache::new(),
|
||||
forced_refresh: ForcedRefresh::new(true),
|
||||
nym_contract_cache: NymContractCache::new(),
|
||||
node_status_cache: NodeStatusCache::new(),
|
||||
|
||||
+1
-1
@@ -26,8 +26,8 @@ pub(crate) mod nym_contract_cache;
|
||||
pub(crate) mod nym_nodes;
|
||||
mod status;
|
||||
pub(crate) mod support;
|
||||
mod unstable_routes;
|
||||
|
||||
// TODO rocket: remove all such Todos once rocket is phased out completely
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
cfg_if::cfg_if! {if #[cfg(feature = "console-subscriber")] {
|
||||
|
||||
@@ -327,7 +327,6 @@ pub(crate) type AxumResult<T> = Result<T, AxumErrorResponse>;
|
||||
// #[schema(title = "ErrorResponse")]
|
||||
pub(crate) struct AxumErrorResponse {
|
||||
message: RequestError,
|
||||
// #[schema(value_type = u16)]
|
||||
status: StatusCode,
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ pub(crate) mod semi_skimmed;
|
||||
pub(crate) mod skimmed;
|
||||
|
||||
#[allow(deprecated)]
|
||||
pub(crate) fn nym_node_routes_unstable() -> Router<AppState> {
|
||||
pub(crate) fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.nest(
|
||||
"/skimmed",
|
||||
|
||||
@@ -27,6 +27,7 @@ use crate::support::http::RouterBuilder;
|
||||
use crate::support::nyxd;
|
||||
use crate::support::storage::runtime_migrations::m001_directory_services_v2_1::migrate_to_directory_services_v2_1;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use crate::unstable_routes::account::cache::AddressInfoCache;
|
||||
use crate::{
|
||||
circulating_supply_api, ecash, epoch_operations, network_monitor, node_describe_cache,
|
||||
node_status_api, nym_contract_cache,
|
||||
@@ -193,6 +194,7 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHan
|
||||
let router = router.with_state(AppState {
|
||||
nyxd_client: nyxd_client.clone(),
|
||||
chain_status_cache: ChainStatusCache::new(DEFAULT_CHAIN_STATUS_CACHE_TTL),
|
||||
address_info_cache: AddressInfoCache::new(),
|
||||
forced_refresh: ForcedRefresh::new(
|
||||
config.topology_cacher.debug.node_describe_allow_illegal_ips,
|
||||
),
|
||||
|
||||
@@ -5,116 +5,6 @@ pub(crate) mod helpers;
|
||||
pub(crate) mod openapi;
|
||||
pub(crate) mod router;
|
||||
pub(crate) mod state;
|
||||
mod unstable_routes;
|
||||
|
||||
pub(crate) use router::RouterBuilder;
|
||||
|
||||
// pub(crate) async fn setup_rocket(
|
||||
// config: &Config,
|
||||
// network_details: NetworkDetails,
|
||||
// nyxd_client: nyxd::Client,
|
||||
// identity_keypair: identity::KeyPair,
|
||||
// coconut_keypair: ecash::keys::KeyPair,
|
||||
// storage: NymApiStorage,
|
||||
// ) -> anyhow::Result<Rocket<Ignite>> {
|
||||
// let openapi_settings = rocket_okapi::settings::OpenApiSettings::default();
|
||||
// let mut rocket = rocket::build();
|
||||
//
|
||||
// let mix_denom = network_details.network.chain_details.mix_denom.base.clone();
|
||||
//
|
||||
// mount_endpoints_and_merged_docs! {
|
||||
// rocket,
|
||||
// "/v1".to_owned(),
|
||||
// openapi_settings,
|
||||
// "/" => (vec![], openapi::custom_openapi_spec()),
|
||||
// "" => circulating_supply_api::circulating_supply_routes(&openapi_settings),
|
||||
// "" => nym_contract_cache::nym_contract_cache_routes(&openapi_settings),
|
||||
// "/status" => node_status_api::node_status_routes(&openapi_settings, config.network_monitor.enabled),
|
||||
// "/network" => network_routes(&openapi_settings),
|
||||
// "/api-status" => api_status_routes(&openapi_settings),
|
||||
// "/ecash" => ecash::routes_open_api(&openapi_settings, config.ecash_signer.enabled),
|
||||
// "" => nym_node_routes_deprecated(&openapi_settings),
|
||||
//
|
||||
// // => when we move those routes, we'll need to add a redirection for backwards compatibility
|
||||
// "/unstable/nym-nodes" => nym_node_routes_next(&openapi_settings)
|
||||
// }
|
||||
//
|
||||
// let rocket = rocket
|
||||
// .manage(network_details)
|
||||
// .manage(SharedCache::<DescribedNodes>::new())
|
||||
// .mount("/swagger", make_swagger_ui(&openapi::get_docs()))
|
||||
// .attach(setup_rocket_cors()?)
|
||||
// .attach(NymContractCache::stage())
|
||||
// .attach(NodeStatusCache::stage())
|
||||
// .attach(CirculatingSupplyCache::stage(mix_denom.clone()))
|
||||
// .manage(unstable::NodeInfoCache::default())
|
||||
// .manage(storage.clone());
|
||||
//
|
||||
// let mut status_state = ApiStatusState::new();
|
||||
//
|
||||
// let rocket = if config.ecash_signer.enabled {
|
||||
// // make sure we have some tokens to cover multisig fees
|
||||
// let balance = nyxd_client.balance(&mix_denom).await?;
|
||||
// if balance.amount < ecash::MINIMUM_BALANCE {
|
||||
// let address = nyxd_client.address().await;
|
||||
// let min = Coin::new(ecash::MINIMUM_BALANCE, mix_denom);
|
||||
// bail!("the account ({address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}")
|
||||
// }
|
||||
//
|
||||
// let cosmos_address = nyxd_client.address().await.to_string();
|
||||
// let announce_address = config
|
||||
// .ecash_signer
|
||||
// .announce_address
|
||||
// .clone()
|
||||
// .map(|u| u.to_string())
|
||||
// .unwrap_or_default();
|
||||
// status_state.add_zk_nym_signer(SignerState {
|
||||
// cosmos_address,
|
||||
// identity: identity_keypair.public_key().to_base58_string(),
|
||||
// announce_address,
|
||||
// ecash_keypair: coconut_keypair.clone(),
|
||||
// });
|
||||
//
|
||||
// let ecash_contract = nyxd_client
|
||||
// .get_ecash_contract_address()
|
||||
// .await
|
||||
// .context("e-cash contract address is required to setup the zk-nym signer")?;
|
||||
//
|
||||
// let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone());
|
||||
//
|
||||
// let ecash_state = EcashState::new(
|
||||
// ecash_contract,
|
||||
// nyxd_client.clone(),
|
||||
// identity_keypair,
|
||||
// coconut_keypair,
|
||||
// comm_channel,
|
||||
// storage.clone(),
|
||||
// )
|
||||
// .await?;
|
||||
//
|
||||
// rocket.manage(ecash_state)
|
||||
// } else {
|
||||
// rocket
|
||||
// };
|
||||
//
|
||||
// Ok(rocket.manage(status_state).ignite().await?)
|
||||
// }
|
||||
//
|
||||
// fn setup_rocket_cors() -> Result<Cors> {
|
||||
// let allowed_origins = AllowedOrigins::all();
|
||||
//
|
||||
// // You can also deserialize this
|
||||
// let cors = rocket_cors::CorsOptions {
|
||||
// allowed_origins,
|
||||
// allowed_methods: vec![rocket::http::Method::Post, rocket::http::Method::Get]
|
||||
// .into_iter()
|
||||
// .map(From::from)
|
||||
// .collect(),
|
||||
// allowed_headers: AllowedHeaders::all(),
|
||||
// allow_credentials: true,
|
||||
// ..Default::default()
|
||||
// }
|
||||
// .to_cors()?;
|
||||
//
|
||||
// Ok(cors)
|
||||
// }
|
||||
use crate::unstable_routes;
|
||||
|
||||
@@ -14,6 +14,8 @@ use crate::support::caching::cache::SharedCache;
|
||||
use crate::support::caching::Cache;
|
||||
use crate::support::nyxd::Client;
|
||||
use crate::support::storage;
|
||||
use crate::unstable_routes::account::cache::AddressInfoCache;
|
||||
use crate::unstable_routes::models::NyxAccountDetails;
|
||||
use axum::extract::FromRef;
|
||||
use nym_api_requests::models::{
|
||||
DetailedChainStatus, GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation,
|
||||
@@ -85,6 +87,7 @@ pub(crate) struct AppState {
|
||||
pub(crate) nyxd_client: Client,
|
||||
pub(crate) chain_status_cache: ChainStatusCache,
|
||||
|
||||
pub(crate) address_info_cache: AddressInfoCache,
|
||||
pub(crate) forced_refresh: ForcedRefresh,
|
||||
pub(crate) nym_contract_cache: NymContractCache,
|
||||
pub(crate) node_status_cache: NodeStatusCache,
|
||||
@@ -292,4 +295,43 @@ impl AppState {
|
||||
.await
|
||||
.ok_or_else(AxumErrorResponse::internal)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_address_info(
|
||||
self,
|
||||
account_id: nym_validator_client::nyxd::AccountId,
|
||||
) -> Result<NyxAccountDetails, AxumErrorResponse> {
|
||||
let address = account_id.to_string();
|
||||
match self.address_info_cache.get(&address).await {
|
||||
Some(guard) => {
|
||||
tracing::trace!("Fetching from cache...");
|
||||
let read_lock = guard.read().await;
|
||||
Ok(read_lock.clone())
|
||||
}
|
||||
None => {
|
||||
tracing::trace!("No cache for {}, refreshing data...", &address);
|
||||
|
||||
let address_info = self
|
||||
.address_info_cache
|
||||
.collect_balances(
|
||||
self.nyxd_client.clone(),
|
||||
self.nym_contract_cache.clone(),
|
||||
self.network_details()
|
||||
.network
|
||||
.chain_details
|
||||
.mix_denom
|
||||
.base
|
||||
.to_owned(),
|
||||
&address,
|
||||
account_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.address_info_cache
|
||||
.upsert_address_info(&address, address_info.clone())
|
||||
.await;
|
||||
|
||||
Ok(address_info)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::nym_nodes::handlers::unstable::nym_node_routes_unstable;
|
||||
use crate::support::http::state::AppState;
|
||||
use axum::Router;
|
||||
|
||||
// as those get stabilised, they should get deprecated and use a redirection instead
|
||||
pub(crate) fn unstable_routes() -> Router<AppState> {
|
||||
Router::new().nest("/nym-nodes", nym_node_routes_unstable())
|
||||
}
|
||||
@@ -29,14 +29,15 @@ use nym_mixnet_contract_common::mixnode::MixNodeDetails;
|
||||
use nym_mixnet_contract_common::nym_node::Role;
|
||||
use nym_mixnet_contract_common::reward_params::RewardingParams;
|
||||
use nym_mixnet_contract_common::{
|
||||
ConfigScoreParams, CurrentIntervalResponse, EpochRewardedSet, EpochStatus, ExecuteMsg,
|
||||
GatewayBond, HistoricalNymNodeVersionEntry, IdentityKey, NymNodeDetails, RewardedSet,
|
||||
RoleAssignment,
|
||||
ConfigScoreParams, CurrentIntervalResponse, Delegation, EpochRewardedSet, EpochStatus,
|
||||
ExecuteMsg, GatewayBond, HistoricalNymNodeVersionEntry, IdentityKey, NymNodeDetails,
|
||||
RewardedSet, RoleAssignment,
|
||||
};
|
||||
use nym_validator_client::coconut::EcashApiError;
|
||||
use nym_validator_client::nyxd::contract_traits::mixnet_query_client::MixnetQueryClientExt;
|
||||
use nym_validator_client::nyxd::contract_traits::PagedDkgQueryClient;
|
||||
use nym_validator_client::nyxd::error::NyxdError;
|
||||
use nym_validator_client::nyxd::Coin;
|
||||
use nym_validator_client::nyxd::{
|
||||
contract_traits::{
|
||||
DkgQueryClient, DkgSigningClient, EcashQueryClient, GroupQueryClient, MixnetQueryClient,
|
||||
@@ -48,7 +49,7 @@ use nym_validator_client::nyxd::{
|
||||
};
|
||||
use nym_validator_client::nyxd::{
|
||||
hash::{Hash, SHA256_HASH_SIZE},
|
||||
AccountId, Coin, TendermintTime,
|
||||
AccountId, TendermintTime,
|
||||
};
|
||||
use nym_validator_client::{
|
||||
nyxd, DirectSigningHttpRpcNyxdClient, EcashApiClient, QueryHttpRpcNyxdClient,
|
||||
@@ -403,6 +404,21 @@ impl Client {
|
||||
nyxd_signing!(self, reconcile_epoch_events(limit, None).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_all_delegator_delegations(
|
||||
&self,
|
||||
delegation_owner: &AccountId,
|
||||
) -> Result<Vec<Delegation>, NyxdError> {
|
||||
nyxd_query!(self, get_all_delegator_delegations(delegation_owner).await)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_address_balance(
|
||||
&self,
|
||||
address: &AccountId,
|
||||
denom: impl Into<String>,
|
||||
) -> Result<Option<Coin>, NyxdError> {
|
||||
nyxd_query!(self, get_balance(&address, denom.into()).await)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
use crate::{
|
||||
node_status_api::models::AxumResult,
|
||||
nym_contract_cache::cache::NymContractCache,
|
||||
unstable_routes::{
|
||||
account::data_collector::AddressDataCollector,
|
||||
models::{NyxAccountDelegationDetails, NyxAccountDetails},
|
||||
},
|
||||
};
|
||||
use moka::{future::Cache, Entry};
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AddressInfoCache {
|
||||
inner: Cache<String, Arc<RwLock<NyxAccountDetails>>>,
|
||||
}
|
||||
|
||||
impl AddressInfoCache {
|
||||
pub(crate) fn new() -> Self {
|
||||
// epoch duration = 1 hour
|
||||
// cache TTL is slightly lower than that to avoid too stale data in case
|
||||
// cache was refreshed JUST BEFORE epoch transition
|
||||
let cache_ttl = Duration::from_secs(60 * 30);
|
||||
let max_capacity = 1000;
|
||||
|
||||
AddressInfoCache {
|
||||
inner: Cache::builder()
|
||||
.time_to_live(cache_ttl)
|
||||
.max_capacity(max_capacity)
|
||||
.build(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get(&self, key: &str) -> Option<Arc<RwLock<NyxAccountDetails>>> {
|
||||
self.inner.get(key).await
|
||||
}
|
||||
|
||||
pub(crate) async fn upsert_address_info(
|
||||
&self,
|
||||
address: &str,
|
||||
address_info: NyxAccountDetails,
|
||||
) -> Entry<String, Arc<RwLock<NyxAccountDetails>>> {
|
||||
self.inner
|
||||
.entry_by_ref(address)
|
||||
.and_upsert_with(|maybe_entry| async {
|
||||
if let Some(entry) = maybe_entry {
|
||||
let v = entry.into_value();
|
||||
let mut guard = v.write().await;
|
||||
*guard = address_info;
|
||||
v.clone()
|
||||
} else {
|
||||
Arc::new(RwLock::new(address_info))
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn collect_balances(
|
||||
&self,
|
||||
nyxd_client: crate::nyxd::Client,
|
||||
nym_contract_cache: NymContractCache,
|
||||
base_denom: String,
|
||||
address: &str,
|
||||
account_id: AccountId,
|
||||
) -> AxumResult<NyxAccountDetails> {
|
||||
let mut collector = AddressDataCollector::new(
|
||||
nyxd_client,
|
||||
nym_contract_cache,
|
||||
base_denom,
|
||||
account_id.clone(),
|
||||
);
|
||||
|
||||
// ==> get balances of chain tokens <==
|
||||
let balance = collector.get_address_balance().await?;
|
||||
|
||||
// it's very difficult to lower existing balance to exactly 0
|
||||
// so assume this is an unused address and return early
|
||||
if balance.amount == 0 {
|
||||
let address_info = NyxAccountDetails {
|
||||
address: address.to_string(),
|
||||
balance: balance.clone().into(),
|
||||
total_value: balance.clone().into(),
|
||||
delegations: Vec::new(),
|
||||
accumulated_rewards: Vec::new(),
|
||||
total_delegations: balance.clone().into(),
|
||||
claimable_rewards: balance.clone().into(),
|
||||
operator_rewards: None,
|
||||
};
|
||||
|
||||
return Ok(address_info);
|
||||
}
|
||||
|
||||
// ==> get list of delegations (history) <==
|
||||
let delegation_data = collector.get_delegations().await?;
|
||||
|
||||
// ==> get the current reward for each active delegation <==
|
||||
// calculate rewards from nodes this delegator delegated to
|
||||
let accumulated_rewards = collector.calculate_rewards(&delegation_data).await?;
|
||||
|
||||
// ==> convert totals <==
|
||||
let claimable_rewards = collector.claimable_rewards();
|
||||
let total_value = collector.total_value();
|
||||
let total_delegations = collector.total_delegations();
|
||||
let operator_rewards = collector.operator_rewards();
|
||||
|
||||
let address_info = NyxAccountDetails {
|
||||
address: account_id.to_string(),
|
||||
balance: balance.into(),
|
||||
delegations: delegation_data
|
||||
.delegations()
|
||||
.into_iter()
|
||||
.map(|d| NyxAccountDelegationDetails {
|
||||
delegated: d.amount,
|
||||
height: d.height,
|
||||
node_id: d.node_id,
|
||||
proxy: d.proxy,
|
||||
})
|
||||
.collect(),
|
||||
accumulated_rewards,
|
||||
total_delegations,
|
||||
claimable_rewards,
|
||||
total_value,
|
||||
operator_rewards,
|
||||
};
|
||||
|
||||
Ok(address_info)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::{
|
||||
node_status_api::models::{AxumErrorResponse, AxumResult},
|
||||
nym_contract_cache::cache::NymContractCache,
|
||||
unstable_routes::models::NyxAccountDelegationRewardDetails,
|
||||
};
|
||||
use cosmwasm_std::{Coin, Decimal};
|
||||
use nym_mixnet_contract_common::NodeRewarding;
|
||||
use nym_topology::NodeId;
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use tracing::warn;
|
||||
|
||||
pub(crate) struct AddressDataCollector {
|
||||
nyxd_client: crate::nyxd::Client,
|
||||
nym_contract_cache: NymContractCache,
|
||||
account_id: AccountId,
|
||||
total_value: u128,
|
||||
operator_rewards: u128,
|
||||
claimable_rewards: u128,
|
||||
total_delegations: u128,
|
||||
base_denom: String,
|
||||
}
|
||||
|
||||
impl AddressDataCollector {
|
||||
pub(crate) fn new(
|
||||
nyxd_client: crate::nyxd::Client,
|
||||
nym_contract_cache: NymContractCache,
|
||||
base_denom: String,
|
||||
account_id: AccountId,
|
||||
) -> Self {
|
||||
Self {
|
||||
nyxd_client,
|
||||
nym_contract_cache,
|
||||
base_denom,
|
||||
account_id,
|
||||
total_value: 0,
|
||||
operator_rewards: 0,
|
||||
claimable_rewards: 0,
|
||||
total_delegations: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_address_balance(
|
||||
&mut self,
|
||||
) -> AxumResult<nym_validator_client::nyxd::Coin> {
|
||||
let balance = self
|
||||
.nyxd_client
|
||||
.get_address_balance(&self.account_id, &self.base_denom)
|
||||
.await?
|
||||
.unwrap_or_else(|| nym_validator_client::nyxd::Coin::new(0u128, &self.base_denom));
|
||||
self.total_value += balance.amount;
|
||||
|
||||
Ok(balance)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_delegations(&mut self) -> AxumResult<AddressDelegationInfo> {
|
||||
let og_delegations = self
|
||||
.nyxd_client
|
||||
.get_all_delegator_delegations(&self.account_id)
|
||||
.await?;
|
||||
|
||||
let delegated_to_nodes = og_delegations
|
||||
.iter()
|
||||
.map(|d| d.node_id)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let nym_nodes = self
|
||||
.nym_contract_cache
|
||||
.all_cached_nym_nodes()
|
||||
.await
|
||||
.ok_or_else(AxumErrorResponse::service_unavailable)?
|
||||
.iter()
|
||||
.filter_map(|node_details| {
|
||||
// is this an operator of this node?
|
||||
if self.account_id.to_string() == node_details.bond_information.owner.as_str() {
|
||||
let pending_operator_reward =
|
||||
node_details.pending_operator_reward().amount.u128();
|
||||
|
||||
// add operator rewards
|
||||
self.operator_rewards += pending_operator_reward;
|
||||
|
||||
// add to totals
|
||||
self.total_value += pending_operator_reward;
|
||||
}
|
||||
if delegated_to_nodes.contains(&node_details.node_id()) {
|
||||
Some((
|
||||
node_details.node_id(),
|
||||
// avoid cloning node data which we don't need
|
||||
(
|
||||
node_details.rewarding_details.clone(),
|
||||
node_details.is_unbonding(),
|
||||
),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
Ok(AddressDelegationInfo {
|
||||
delegations: og_delegations,
|
||||
delegated_to_nodes: nym_nodes,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn calculate_rewards(
|
||||
&mut self,
|
||||
delegation_data: &AddressDelegationInfo,
|
||||
) -> AxumResult<Vec<NyxAccountDelegationRewardDetails>> {
|
||||
let mut accumulated_rewards = Vec::new();
|
||||
for delegation in delegation_data.delegations.iter() {
|
||||
let node_id = &delegation.node_id;
|
||||
|
||||
if let Some((rewarding_details, is_unbonding)) =
|
||||
delegation_data.delegated_to_nodes.get(node_id)
|
||||
{
|
||||
match rewarding_details.determine_delegation_reward(delegation) {
|
||||
Ok(delegation_reward) => {
|
||||
let reward = NyxAccountDelegationRewardDetails {
|
||||
node_id: delegation.node_id,
|
||||
rewards: decimal_to_coin(delegation_reward, &self.base_denom),
|
||||
amount_staked: delegation.amount.clone(),
|
||||
node_still_fully_bonded: !is_unbonding,
|
||||
};
|
||||
// 4. sum the rewards and delegations
|
||||
self.total_delegations += delegation.amount.amount.u128();
|
||||
self.total_value += delegation.amount.amount.u128();
|
||||
self.total_value += reward.rewards.amount.u128();
|
||||
self.claimable_rewards += reward.rewards.amount.u128();
|
||||
|
||||
accumulated_rewards.push(reward);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"Couldn't determine delegations for {} on node {}: {}",
|
||||
&self.account_id, node_id, err
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(accumulated_rewards)
|
||||
}
|
||||
|
||||
pub(crate) fn claimable_rewards(&self) -> Coin {
|
||||
Coin::new(self.claimable_rewards, self.base_denom.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn total_value(&self) -> Coin {
|
||||
Coin::new(self.total_value, self.base_denom.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn total_delegations(&self) -> Coin {
|
||||
Coin::new(self.total_delegations, self.base_denom.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn operator_rewards(&self) -> Option<Coin> {
|
||||
if self.operator_rewards > 0 {
|
||||
Some(Coin::new(
|
||||
self.operator_rewards,
|
||||
self.base_denom.to_string(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct AddressDelegationInfo {
|
||||
delegations: Vec<nym_mixnet_contract_common::Delegation>,
|
||||
delegated_to_nodes: HashMap<NodeId, RewardAndBondInfo>,
|
||||
}
|
||||
|
||||
impl AddressDelegationInfo {
|
||||
pub(crate) fn delegations(self) -> Vec<nym_mixnet_contract_common::Delegation> {
|
||||
self.delegations
|
||||
}
|
||||
}
|
||||
|
||||
type RewardAndBondInfo = (NodeRewarding, bool);
|
||||
|
||||
fn decimal_to_coin(decimal: Decimal, denom: impl Into<String>) -> Coin {
|
||||
Coin::new(decimal.to_uint_floor(), denom)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn decimal_to_coin_test() {
|
||||
let test_values = [
|
||||
(1234, 0, 1234),
|
||||
(1234, 2, 12),
|
||||
(1_234_000_000_000_000u128, 6, 1_234_000_000u128),
|
||||
];
|
||||
|
||||
for (amount, decimal_places, coin_amount) in test_values {
|
||||
let decimal =
|
||||
Decimal::from_atomics(cosmwasm_std::Uint128::new(amount), decimal_places).unwrap();
|
||||
let coin_from_decimal = decimal_to_coin(decimal, "unym");
|
||||
assert_eq!(coin_from_decimal, Coin::new(coin_amount, "unym"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::{
|
||||
node_status_api::models::{AxumErrorResponse, AxumResult},
|
||||
support::http::state::AppState,
|
||||
unstable_routes::models::NyxAccountDetails,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use tracing::{error, instrument};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub(crate) mod cache;
|
||||
pub(crate) mod data_collector;
|
||||
|
||||
pub(crate) fn routes() -> Router<AppState> {
|
||||
Router::new().route("/:address", get(address))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, utoipa::IntoParams)]
|
||||
pub struct AddressQueryParam {
|
||||
#[serde(default)]
|
||||
pub address: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Unstable",
|
||||
get,
|
||||
path = "/{address}",
|
||||
context_path = "/v1/unstable/account",
|
||||
responses(
|
||||
(status = 200, body = NyxAccountDetails)
|
||||
),
|
||||
params(AddressQueryParam)
|
||||
)]
|
||||
#[instrument(level = "info", skip_all, fields(address=address))]
|
||||
async fn address(
|
||||
Path(AddressQueryParam { address }): Path<AddressQueryParam>,
|
||||
State(state): State<AppState>,
|
||||
) -> AxumResult<Json<NyxAccountDetails>> {
|
||||
let account_id = AccountId::from_str(&address).map_err(|err| {
|
||||
error!("{err}");
|
||||
AxumErrorResponse::not_found(&address)
|
||||
})?;
|
||||
|
||||
state.get_address_info(account_id).await.map(Json)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub(crate) mod account;
|
||||
pub(crate) mod models;
|
||||
|
||||
use crate::support::http::state::AppState;
|
||||
use axum::Router;
|
||||
|
||||
// as those get stabilised, they should get deprecated and use a redirection instead
|
||||
pub(crate) fn unstable_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.nest("/nym-nodes", crate::nym_nodes::handlers::unstable::routes())
|
||||
.nest("/account", account::routes())
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
use nym_topology::NodeId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::schema;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema, utoipa::ToResponse)]
|
||||
#[schema(title = "Coin")]
|
||||
pub struct CoinSchema {
|
||||
pub denom: String,
|
||||
pub amount: u128,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema, utoipa::ToResponse)]
|
||||
pub struct NyxAccountDelegationDetails {
|
||||
pub node_id: NodeId,
|
||||
#[schema(value_type = CoinSchema)]
|
||||
pub delegated: Coin,
|
||||
pub height: u64,
|
||||
#[schema(value_type = Option<String>)]
|
||||
pub proxy: Option<Addr>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema, utoipa::ToResponse)]
|
||||
pub struct NyxAccountDelegationRewardDetails {
|
||||
pub node_id: NodeId,
|
||||
#[schema(value_type = CoinSchema)]
|
||||
pub rewards: Coin,
|
||||
#[schema(value_type = String)]
|
||||
pub amount_staked: Coin,
|
||||
pub node_still_fully_bonded: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema, utoipa::ToResponse)]
|
||||
pub struct NyxAccountDetails {
|
||||
pub address: String,
|
||||
#[schema(value_type = CoinSchema)]
|
||||
pub balance: Coin,
|
||||
#[schema(value_type = CoinSchema)]
|
||||
pub total_value: Coin,
|
||||
pub delegations: Vec<NyxAccountDelegationDetails>,
|
||||
pub accumulated_rewards: Vec<NyxAccountDelegationRewardDetails>,
|
||||
#[schema(value_type = String)]
|
||||
pub total_delegations: Coin,
|
||||
#[schema(value_type = CoinSchema)]
|
||||
pub claimable_rewards: Coin,
|
||||
#[schema(value_type = Option<CoinSchema>)]
|
||||
pub operator_rewards: Option<Coin>,
|
||||
}
|
||||
@@ -4,8 +4,21 @@ use tauri_plugin_opener::OpenerExt;
|
||||
pub async fn open_url(url: String, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
println!("Opening URL: {}", url);
|
||||
|
||||
match app_handle.opener().open_url(&url, None::<&str>) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("Failed to open URL: {}", err)),
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Windows needs shell capability
|
||||
match app_handle.shell().open("", &url) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("Failed to open URL: {}", err)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
// macOS and Linux work well with opener
|
||||
match app_handle.opener().open_url(&url, None::<&str>) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("Failed to open URL: {}", err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user