From 7af1fbaa10e25803248b4969daab975bf1cfdf3d Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 6 Feb 2025 10:29:01 +0000 Subject: [PATCH 01/13] add token supplies table --- .../2025-02-06-093718_token_supplies/down.sql | 11 ++++++ .../2025-02-06-093718_token_supplies/up.sql | 36 +++++++++++++++++++ orm/src/schema.rs | 12 +++++++ 3 files changed, 59 insertions(+) create mode 100644 orm/migrations/2025-02-06-093718_token_supplies/down.sql create mode 100644 orm/migrations/2025-02-06-093718_token_supplies/up.sql diff --git a/orm/migrations/2025-02-06-093718_token_supplies/down.sql b/orm/migrations/2025-02-06-093718_token_supplies/down.sql new file mode 100644 index 00000000..6af45f90 --- /dev/null +++ b/orm/migrations/2025-02-06-093718_token_supplies/down.sql @@ -0,0 +1,11 @@ +-- This file should undo anything in `up.sql` + +-- Drop the foreign key constraint on the address column +ALTER TABLE token_supplies_per_epoch + DROP CONSTRAINT fk_token_supplies_per_epoch_address; + +-- Drop the trigger for enforcing the effective constraint +DROP TRIGGER enforce_effective_constraint ON token_supplies_per_epoch; + +-- Drop the table itself +DROP TABLE token_supplies_per_epoch; diff --git a/orm/migrations/2025-02-06-093718_token_supplies/up.sql b/orm/migrations/2025-02-06-093718_token_supplies/up.sql new file mode 100644 index 00000000..b8d0e55c --- /dev/null +++ b/orm/migrations/2025-02-06-093718_token_supplies/up.sql @@ -0,0 +1,36 @@ +-- Your SQL goes here + +CREATE TABLE token_supplies_per_epoch ( + address VARCHAR(45) PRIMARY KEY, + epoch INT NOT NULL, + -- `2^256 - 1` will fit in `NUMERIC(78, 0)` + total NUMERIC(78, 0) NOT NULL, + effective NUMERIC(78, 0), + -- reference the `address` column in the `token` table + CONSTRAINT fk_token_supplies_per_epoch_address + FOREIGN KEY(address) REFERENCES token(address) ON DELETE CASCADE +); + +CREATE OR REPLACE FUNCTION check_effective_for_token_type() +RETURNS TRIGGER AS $$ +BEGIN + -- Check if the referenced token_type is 'native' + IF EXISTS ( + SELECT 1 + FROM token + WHERE token.address = NEW.address AND token.token_type = 'native' + ) THEN + -- If token_type is 'native', ensure token_supplies_per_epoch.effective is not NULL + IF NEW.effective IS NULL THEN + RAISE EXCEPTION 'token_supplies_per_epoch.effective cannot be NULL when token.token_type is ''native'''; + END IF; + END IF; + -- Allow the insert or update to proceed + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER enforce_effective_constraint +BEFORE INSERT OR UPDATE ON token_supplies_per_epoch +FOR EACH ROW +EXECUTE FUNCTION check_effective_for_token_type(); diff --git a/orm/src/schema.rs b/orm/src/schema.rs index 38a7d121..394431f5 100644 --- a/orm/src/schema.rs +++ b/orm/src/schema.rs @@ -327,6 +327,16 @@ diesel::table! { } } +diesel::table! { + token_supplies_per_epoch (address) { + #[max_length = 45] + address -> Varchar, + epoch -> Int4, + total -> Numeric, + effective -> Nullable, + } +} + diesel::table! { use diesel::sql_types::*; use super::sql_types::HistoryKind; @@ -397,6 +407,7 @@ diesel::joinable!(ibc_token -> token (address)); diesel::joinable!(inner_transactions -> wrapper_transactions (wrapper_id)); diesel::joinable!(pos_rewards -> validators (validator_id)); diesel::joinable!(public_good_funding -> governance_proposals (proposal_id)); +diesel::joinable!(token_supplies_per_epoch -> token (address)); diesel::joinable!(transaction_history -> inner_transactions (inner_tx_id)); diesel::joinable!(unbonds -> validators (validator_id)); diesel::joinable!(wrapper_transactions -> blocks (block_height)); @@ -419,6 +430,7 @@ diesel::allow_tables_to_appear_in_same_query!( public_good_funding, revealed_pk, token, + token_supplies_per_epoch, transaction_history, unbonds, validators, From 035e9e9ba511760966f221917357cd1565ef4841 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 6 Feb 2025 11:14:10 +0000 Subject: [PATCH 02/13] add util to await on futures in options --- shared/src/futures.rs | 24 ++++++++++++++++++++++++ shared/src/lib.rs | 1 + 2 files changed, 25 insertions(+) create mode 100644 shared/src/futures.rs diff --git a/shared/src/futures.rs b/shared/src/futures.rs new file mode 100644 index 00000000..c5a2e650 --- /dev/null +++ b/shared/src/futures.rs @@ -0,0 +1,24 @@ +use std::future::Future; + +pub trait AwaitOption { + type Output; + + #[allow(async_fn_in_trait)] + async fn future(self) -> Option; +} + +impl AwaitOption for Option +where + F: Future, +{ + type Output = T; + + #[inline] + async fn future(self) -> Option { + if let Some(fut) = self { + Some(fut.await) + } else { + None + } + } +} diff --git a/shared/src/lib.rs b/shared/src/lib.rs index e495659e..1d210065 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -6,6 +6,7 @@ pub mod checksums; pub mod crawler; pub mod crawler_state; pub mod error; +pub mod futures; pub mod gas; pub mod genesis; pub mod header; From d2c4f6868a40d19a54633af0fd867591b6b6fd16 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 6 Feb 2025 11:15:00 +0000 Subject: [PATCH 03/13] add methods to query native token supplies --- chain/src/services/namada.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/chain/src/services/namada.rs b/chain/src/services/namada.rs index 8abc5a3b..3502a4bc 100644 --- a/chain/src/services/namada.rs +++ b/chain/src/services/namada.rs @@ -42,6 +42,30 @@ pub async fn get_native_token(client: &HttpClient) -> anyhow::Result { Ok(Id::from(native_token)) } +pub async fn query_native_token_total_supply( + client: &HttpClient, +) -> anyhow::Result { + let native_token = RPC + .shell() + .native_token(client) + .await + .context("Failed to query native token")?; + + rpc::get_token_total_supply(client, &native_token) + .await + .map(Amount::from) + .context("Failed to query total supply of native token") +} + +pub async fn query_native_token_effective_supply( + client: &HttpClient, +) -> anyhow::Result { + rpc::get_effective_native_supply(client) + .await + .map(Amount::from) + .context("Failed to query effective supply of native token") +} + pub async fn get_first_block_in_epoch( client: &HttpClient, ) -> anyhow::Result { From 9372c4dc448786db152c040b7f263691844f6052 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 6 Feb 2025 11:28:09 +0000 Subject: [PATCH 04/13] generalize container fut mechanism --- shared/src/futures.rs | 47 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/shared/src/futures.rs b/shared/src/futures.rs index c5a2e650..e7b72789 100644 --- a/shared/src/futures.rs +++ b/shared/src/futures.rs @@ -1,20 +1,22 @@ use std::future::Future; -pub trait AwaitOption { - type Output; +pub trait AwaitContainer { + type Output; #[allow(async_fn_in_trait)] - async fn future(self) -> Option; + async fn future(self) -> Self::Output + where + F: Future; } -impl AwaitOption for Option -where - F: Future, -{ - type Output = T; +impl AwaitContainer for Option { + type Output = Option; #[inline] - async fn future(self) -> Option { + async fn future(self) -> Option + where + F: Future, + { if let Some(fut) = self { Some(fut.await) } else { @@ -22,3 +24,30 @@ where } } } + +impl AwaitContainer for Result { + type Output = Result; + + #[inline] + async fn future(self) -> Result + where + F: Future, + { + match self { + Ok(fut) => Ok(fut.await), + Err(err) => Err(err), + } + } +} + +impl AwaitContainer for Vec { + type Output = Vec; + + #[inline] + async fn future(self) -> Vec + where + F: Future, + { + ::futures::future::join_all(self).await + } +} From 466a4ad7cf93407556d52daca623df89825054fc Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 6 Feb 2025 11:15:18 +0000 Subject: [PATCH 05/13] wip: store native token supplies in db --- chain/src/main.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/chain/src/main.rs b/chain/src/main.rs index 10a22f0f..28f7efb2 100644 --- a/chain/src/main.rs +++ b/chain/src/main.rs @@ -26,6 +26,7 @@ use shared::checksums::Checksums; use shared::crawler::crawl; use shared::crawler_state::ChainCrawlerState; use shared::error::{AsDbError, AsRpcError, ContextDbInteractError, MainError}; +use shared::futures::AwaitContainer; use shared::id::Id; use shared::token::Token; use shared::utils::BalanceChange; @@ -235,6 +236,25 @@ async fn crawling_fn( )); let addresses = block.addresses_with_balance_change(&native_token); + let _native_token_supplies = first_block_in_epoch + .eq(&block_height) + .then_some(async { + let total_supply_fut = + namada_service::query_native_token_total_supply(&client); + let effective_supply_fut = + namada_service::query_native_token_effective_supply(&client); + + let (total_supply, effective_supply) = + futures::try_join!(total_supply_fut, effective_supply_fut) + .context("Failed to query native token supplies")?; + + anyhow::Ok((total_supply, effective_supply)) + }) + .future() + .await + .transpose() + .into_rpc_error()?; + let validators_addresses = if first_block_in_epoch.eq(&block_height) { namada_service::get_all_consensus_validators_addresses_at( &client, From 70c529a5411610cedb6d53c21441d0b7d8ab7968 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 6 Feb 2025 14:28:56 +0000 Subject: [PATCH 06/13] convert between amounts and bigdecs --- Cargo.lock | 1 + Cargo.toml | 1 + shared/Cargo.toml | 3 +- shared/src/balance.rs | 71 ++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 66d513f9..83dd0e1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6834,6 +6834,7 @@ dependencies = [ "namada_proof_of_stake", "namada_sdk", "namada_tx", + "num-bigint", "rand", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index c1fd0402..d1e50fb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ namada_ibc = { git = "https://github.com/anoma/namada", tag = "libs-v0.47.0" } namada_token = { git = "https://github.com/anoma/namada", tag = "libs-v0.47.0" } namada_parameters = { git = "https://github.com/anoma/namada", tag = "libs-v0.47.0" } namada_proof_of_stake = { git = "https://github.com/anoma/namada", tag = "libs-v0.47.0" } +num-bigint = "0.4.6" tendermint = "0.38.0" tendermint-config = "0.38.0" tendermint-rpc = { version = "0.38.0", features = ["http-client"] } diff --git a/shared/Cargo.toml b/shared/Cargo.toml index c8a8ae21..a3f4998a 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -28,6 +28,7 @@ namada_proof_of_stake.workspace = true namada_ibc.workspace = true namada_sdk.workspace = true namada_tx.workspace = true +num-bigint.workspace = true serde.workspace = true serde_json.workspace = true subtle-encoding.workspace = true @@ -39,4 +40,4 @@ tokio.workspace = true tracing.workspace = true tracing-subscriber.workspace = true fake.workspace = true -rand.workspace = true \ No newline at end of file +rand.workspace = true diff --git a/shared/src/balance.rs b/shared/src/balance.rs index 9c96d9ba..c0815169 100644 --- a/shared/src/balance.rs +++ b/shared/src/balance.rs @@ -6,6 +6,7 @@ use namada_sdk::token::{ Amount as NamadaAmount, DenominatedAmount as NamadaDenominatedAmount, Denomination as NamadaDenomination, }; +use num_bigint::BigUint; use crate::id::Id; use crate::token::Token; @@ -21,10 +22,37 @@ impl From for Amount { impl From for Amount { fn from(amount: BigDecimal) -> Amount { - Amount( - NamadaAmount::from_string_precise(&amount.to_string()) - .expect("Invalid amount"), - ) + (&amount).into() + } +} + +impl From<&BigDecimal> for Amount { + fn from(amount: &BigDecimal) -> Amount { + let (big_int, _scale) = amount.as_bigint_and_scale(); + let (_sign, amount_bytes) = big_int.to_bytes_le(); + + let uint_bytes: [u64; 4] = { + // interpret as uint, regardless of sign. we + // also truncate to the first 32 bytes. + let mut uint_bytes = [0u8; 32]; + let min_len = amount_bytes.len().min(32); + + uint_bytes[..min_len].copy_from_slice(&amount_bytes[..min_len]); + + unsafe { std::mem::transmute(uint_bytes) } + }; + + Amount(NamadaAmount::from(namada_core::uint::Uint(uint_bytes))) + } +} + +impl From for BigDecimal { + fn from(amount: Amount) -> BigDecimal { + let digits: [u8; 32] = { + let uint: namada_core::uint::Uint = amount.0.into(); + unsafe { std::mem::transmute(uint.0) } + }; + BigDecimal::from_biguint(BigUint::from_bytes_le(&digits), 0) } } @@ -114,3 +142,38 @@ impl Balance { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn conversion_between_bigdec_and_amount() { + let initial_amount = + Amount(NamadaAmount::from(namada_core::uint::Uint([ + 1u64, 2u64, 3u64, 4u64, + ]))); + + let initial_bigdec: BigDecimal = + "25108406941546723056364004793593481054836439088298861789185" + .parse() + .unwrap(); + + assert_eq!(initial_amount, Amount::from(&initial_bigdec)); + assert_eq!(BigDecimal::from(initial_amount.clone()), initial_bigdec); + + let amount_round_trip = { + let x: BigDecimal = initial_amount.clone().into(); + let x: Amount = x.into(); + x + }; + assert_eq!(initial_amount, amount_round_trip); + + let bigdec_round_trip = { + let x: Amount = (&initial_bigdec).into(); + let x: BigDecimal = x.into(); + x + }; + assert_eq!(initial_bigdec, bigdec_round_trip); + } +} From 595c5dd7051052a4fdf4d4cfb22d2374f1af0f20 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 7 Feb 2025 09:46:23 +0000 Subject: [PATCH 07/13] add unique constraint on token supplies table --- orm/migrations/2025-02-06-093718_token_supplies/up.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/orm/migrations/2025-02-06-093718_token_supplies/up.sql b/orm/migrations/2025-02-06-093718_token_supplies/up.sql index b8d0e55c..1afa6602 100644 --- a/orm/migrations/2025-02-06-093718_token_supplies/up.sql +++ b/orm/migrations/2025-02-06-093718_token_supplies/up.sql @@ -11,6 +11,8 @@ CREATE TABLE token_supplies_per_epoch ( FOREIGN KEY(address) REFERENCES token(address) ON DELETE CASCADE ); +ALTER TABLE token_supplies_per_epoch ADD UNIQUE (address, epoch); + CREATE OR REPLACE FUNCTION check_effective_for_token_type() RETURNS TRIGGER AS $$ BEGIN From 655a7ed21a4d09f71ac5f89d02177397e7b34f3b Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 7 Feb 2025 09:46:50 +0000 Subject: [PATCH 08/13] add orm for token supplies --- orm/src/lib.rs | 1 + orm/src/token_supplies_per_epoch.rs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 orm/src/token_supplies_per_epoch.rs diff --git a/orm/src/lib.rs b/orm/src/lib.rs index 2427200c..2a71355a 100644 --- a/orm/src/lib.rs +++ b/orm/src/lib.rs @@ -15,6 +15,7 @@ pub mod pos_rewards; pub mod revealed_pk; pub mod schema; pub mod token; +pub mod token_supplies_per_epoch; pub mod transactions; pub mod unbond; pub mod validators; diff --git a/orm/src/token_supplies_per_epoch.rs b/orm/src/token_supplies_per_epoch.rs new file mode 100644 index 00000000..9f81bc1f --- /dev/null +++ b/orm/src/token_supplies_per_epoch.rs @@ -0,0 +1,16 @@ +use bigdecimal::BigDecimal; +use diesel::{Insertable, Queryable, Selectable}; + +use crate::schema::token_supplies_per_epoch; + +#[derive(Debug, Clone, Queryable, Selectable, Insertable)] +#[diesel(table_name = token_supplies_per_epoch)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct TokenSupplies { + pub address: String, + pub epoch: i32, + pub total: BigDecimal, + pub effective: Option, +} + +pub type TokenSuppliesInsertDb = TokenSupplies; From 210bad2da21836a64306f259c641092b6ce89245 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 7 Feb 2025 09:47:12 +0000 Subject: [PATCH 09/13] insert token supplies into db --- chain/src/main.rs | 20 ++++++++------------ chain/src/repository/balance.rs | 22 +++++++++++++++++++++- chain/src/services/namada.rs | 21 +++++++++++++++++++++ 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/chain/src/main.rs b/chain/src/main.rs index 28f7efb2..fba9612a 100644 --- a/chain/src/main.rs +++ b/chain/src/main.rs @@ -236,19 +236,10 @@ async fn crawling_fn( )); let addresses = block.addresses_with_balance_change(&native_token); - let _native_token_supplies = first_block_in_epoch + let native_token_supplies = first_block_in_epoch .eq(&block_height) - .then_some(async { - let total_supply_fut = - namada_service::query_native_token_total_supply(&client); - let effective_supply_fut = - namada_service::query_native_token_effective_supply(&client); - - let (total_supply, effective_supply) = - futures::try_join!(total_supply_fut, effective_supply_fut) - .context("Failed to query native token supplies")?; - - anyhow::Ok((total_supply, effective_supply)) + .then(|| { + namada_service::get_token_supplies(&client, &native_token, epoch) }) .future() .await @@ -431,6 +422,11 @@ async fn crawling_fn( ibc_tokens, )?; + repository::balance::insert_token_supplies( + transaction_conn, + native_token_supplies.map(|supply| vec![supply]), + )?; + repository::block::upsert_block( transaction_conn, block, diff --git a/chain/src/repository/balance.rs b/chain/src/repository/balance.rs index 253e8c3e..5d7bef70 100644 --- a/chain/src/repository/balance.rs +++ b/chain/src/repository/balance.rs @@ -1,8 +1,11 @@ use anyhow::Context; use diesel::{PgConnection, RunQueryDsl}; use orm::balances::BalanceChangesInsertDb; -use orm::schema::{balance_changes, ibc_token, token}; +use orm::schema::{ + balance_changes, ibc_token, token, token_supplies_per_epoch, +}; use orm::token::{IbcTokenInsertDb, TokenInsertDb}; +use orm::token_supplies_per_epoch::TokenSupplies; use shared::balance::Balances; use shared::token::Token; use shared::tuple_len::TupleLen; @@ -70,6 +73,23 @@ pub fn insert_tokens( anyhow::Ok(()) } +pub fn insert_token_supplies( + transaction_conn: &mut PgConnection, + supplies: Option>, +) -> anyhow::Result<()> { + if let Some(supplies) = supplies { + tracing::debug!(?supplies, "Adding new token supplies to db"); + + diesel::insert_into(token_supplies_per_epoch::table) + .values(supplies) + .on_conflict_do_nothing() + .execute(transaction_conn) + .context("Failed to update token supplies in db")?; + } + + anyhow::Ok(()) +} + #[cfg(test)] mod tests { diff --git a/chain/src/services/namada.rs b/chain/src/services/namada.rs index 3502a4bc..de107a54 100644 --- a/chain/src/services/namada.rs +++ b/chain/src/services/namada.rs @@ -18,6 +18,7 @@ use namada_sdk::rpc::{ use namada_sdk::state::Key; use namada_sdk::token::Amount as NamadaSdkAmount; use namada_sdk::{borsh, rpc, token}; +use orm::token_supplies_per_epoch::TokenSupplies; use shared::balance::{Amount, Balance, Balances}; use shared::block::{BlockHeight, Epoch}; use shared::bond::{Bond, BondAddresses, Bonds}; @@ -821,3 +822,23 @@ pub async fn get_pgf_receipients( }) .collect::>() } + +pub async fn get_token_supplies( + client: &HttpClient, + native_token: &Id, + epoch: u32, +) -> anyhow::Result { + let total_supply_fut = query_native_token_total_supply(client); + let effective_supply_fut = query_native_token_effective_supply(client); + + let (total_supply, effective_supply) = + futures::try_join!(total_supply_fut, effective_supply_fut) + .context("Failed to query native token supplies")?; + + anyhow::Ok(TokenSupplies { + address: native_token.to_string(), + epoch: epoch as _, + total: total_supply.into(), + effective: Some(effective_supply.into()), + }) +} From 1f9076a4e235ea9a9f9d07f3581f4bc239532f72 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 7 Feb 2025 09:53:38 +0000 Subject: [PATCH 10/13] build a specific bin --- justfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/justfile b/justfile index 93d33be8..a4a2c2ae 100644 --- a/justfile +++ b/justfile @@ -9,8 +9,8 @@ toolchains: @echo {{ RUST_STABLE }} @echo {{ RUST_NIGTHLY }} -build: - cargo +{{ RUST_STABLE }} build --all +build *BIN: + cargo +{{ RUST_STABLE }} build {{ if BIN != "" { prepend("--bin ", BIN) } else { "--all" } }} check: cargo +{{ RUST_STABLE }} check --all From 10050d5b27022bac50208406976804c089391237 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 7 Feb 2025 10:20:51 +0000 Subject: [PATCH 11/13] use different primary key for token supplies table --- chain/src/main.rs | 2 +- chain/src/repository/balance.rs | 34 +++++++++++-------- chain/src/services/namada.rs | 7 ++-- .../2025-02-06-093718_token_supplies/up.sql | 3 +- orm/src/schema.rs | 3 +- orm/src/token_supplies_per_epoch.rs | 32 +++++++++++++++-- shared/src/balance.rs | 8 +++++ 7 files changed, 66 insertions(+), 23 deletions(-) diff --git a/chain/src/main.rs b/chain/src/main.rs index fba9612a..f60305b1 100644 --- a/chain/src/main.rs +++ b/chain/src/main.rs @@ -424,7 +424,7 @@ async fn crawling_fn( repository::balance::insert_token_supplies( transaction_conn, - native_token_supplies.map(|supply| vec![supply]), + native_token_supplies, )?; repository::block::upsert_block( diff --git a/chain/src/repository/balance.rs b/chain/src/repository/balance.rs index 5d7bef70..af951a8f 100644 --- a/chain/src/repository/balance.rs +++ b/chain/src/repository/balance.rs @@ -5,8 +5,8 @@ use orm::schema::{ balance_changes, ibc_token, token, token_supplies_per_epoch, }; use orm::token::{IbcTokenInsertDb, TokenInsertDb}; -use orm::token_supplies_per_epoch::TokenSupplies; -use shared::balance::Balances; +use orm::token_supplies_per_epoch::TokenSuppliesInsertDb; +use shared::balance::{Balances, TokenSupply}; use shared::token::Token; use shared::tuple_len::TupleLen; @@ -73,19 +73,25 @@ pub fn insert_tokens( anyhow::Ok(()) } -pub fn insert_token_supplies( +pub fn insert_token_supplies( transaction_conn: &mut PgConnection, - supplies: Option>, -) -> anyhow::Result<()> { - if let Some(supplies) = supplies { - tracing::debug!(?supplies, "Adding new token supplies to db"); - - diesel::insert_into(token_supplies_per_epoch::table) - .values(supplies) - .on_conflict_do_nothing() - .execute(transaction_conn) - .context("Failed to update token supplies in db")?; - } + supplies: S, +) -> anyhow::Result<()> +where + S: IntoIterator, +{ + let supplies: Vec<_> = supplies + .into_iter() + .map(TokenSuppliesInsertDb::from) + .collect(); + + tracing::debug!(?supplies, "Adding new token supplies to db"); + + diesel::insert_into(token_supplies_per_epoch::table) + .values(supplies) + .on_conflict_do_nothing() + .execute(transaction_conn) + .context("Failed to update token supplies in db")?; anyhow::Ok(()) } diff --git a/chain/src/services/namada.rs b/chain/src/services/namada.rs index de107a54..08349217 100644 --- a/chain/src/services/namada.rs +++ b/chain/src/services/namada.rs @@ -18,8 +18,7 @@ use namada_sdk::rpc::{ use namada_sdk::state::Key; use namada_sdk::token::Amount as NamadaSdkAmount; use namada_sdk::{borsh, rpc, token}; -use orm::token_supplies_per_epoch::TokenSupplies; -use shared::balance::{Amount, Balance, Balances}; +use shared::balance::{Amount, Balance, Balances, TokenSupply}; use shared::block::{BlockHeight, Epoch}; use shared::bond::{Bond, BondAddresses, Bonds}; use shared::id::Id; @@ -827,7 +826,7 @@ pub async fn get_token_supplies( client: &HttpClient, native_token: &Id, epoch: u32, -) -> anyhow::Result { +) -> anyhow::Result { let total_supply_fut = query_native_token_total_supply(client); let effective_supply_fut = query_native_token_effective_supply(client); @@ -835,7 +834,7 @@ pub async fn get_token_supplies( futures::try_join!(total_supply_fut, effective_supply_fut) .context("Failed to query native token supplies")?; - anyhow::Ok(TokenSupplies { + anyhow::Ok(TokenSupply { address: native_token.to_string(), epoch: epoch as _, total: total_supply.into(), diff --git a/orm/migrations/2025-02-06-093718_token_supplies/up.sql b/orm/migrations/2025-02-06-093718_token_supplies/up.sql index 1afa6602..9d73bc99 100644 --- a/orm/migrations/2025-02-06-093718_token_supplies/up.sql +++ b/orm/migrations/2025-02-06-093718_token_supplies/up.sql @@ -1,7 +1,8 @@ -- Your SQL goes here CREATE TABLE token_supplies_per_epoch ( - address VARCHAR(45) PRIMARY KEY, + id SERIAL PRIMARY KEY, + address VARCHAR(45) NOT NULL, epoch INT NOT NULL, -- `2^256 - 1` will fit in `NUMERIC(78, 0)` total NUMERIC(78, 0) NOT NULL, diff --git a/orm/src/schema.rs b/orm/src/schema.rs index 394431f5..98920148 100644 --- a/orm/src/schema.rs +++ b/orm/src/schema.rs @@ -328,7 +328,8 @@ diesel::table! { } diesel::table! { - token_supplies_per_epoch (address) { + token_supplies_per_epoch (id) { + id -> Int4, #[max_length = 45] address -> Varchar, epoch -> Int4, diff --git a/orm/src/token_supplies_per_epoch.rs b/orm/src/token_supplies_per_epoch.rs index 9f81bc1f..6b4eb005 100644 --- a/orm/src/token_supplies_per_epoch.rs +++ b/orm/src/token_supplies_per_epoch.rs @@ -1,16 +1,44 @@ use bigdecimal::BigDecimal; use diesel::{Insertable, Queryable, Selectable}; +use shared::balance::TokenSupply as SharedTokenSupply; use crate::schema::token_supplies_per_epoch; -#[derive(Debug, Clone, Queryable, Selectable, Insertable)] +#[derive(Debug, Clone, Queryable, Selectable)] #[diesel(table_name = token_supplies_per_epoch)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct TokenSupplies { + pub id: i32, + pub address: String, + pub epoch: i32, + pub total: BigDecimal, + pub effective: Option, +} + +#[derive(Debug, Clone, Queryable, Selectable, Insertable)] +#[diesel(table_name = token_supplies_per_epoch)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct TokenSuppliesInsertDb { pub address: String, pub epoch: i32, pub total: BigDecimal, pub effective: Option, } -pub type TokenSuppliesInsertDb = TokenSupplies; +impl From for TokenSuppliesInsertDb { + fn from(supply: SharedTokenSupply) -> Self { + let SharedTokenSupply { + address, + epoch, + total, + effective, + } = supply; + + Self { + address, + epoch, + total, + effective, + } + } +} diff --git a/shared/src/balance.rs b/shared/src/balance.rs index c0815169..9f4719fa 100644 --- a/shared/src/balance.rs +++ b/shared/src/balance.rs @@ -143,6 +143,14 @@ impl Balance { } } +#[derive(Debug, Clone)] +pub struct TokenSupply { + pub address: String, + pub epoch: i32, + pub total: BigDecimal, + pub effective: Option, +} + #[cfg(test)] mod tests { use super::*; From d58203c40e1f700eef4b40f9322c46a721f29f3d Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 7 Feb 2025 10:24:25 +0000 Subject: [PATCH 12/13] avoid empty token supply inserts --- chain/src/repository/balance.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/chain/src/repository/balance.rs b/chain/src/repository/balance.rs index af951a8f..cf66d300 100644 --- a/chain/src/repository/balance.rs +++ b/chain/src/repository/balance.rs @@ -85,6 +85,10 @@ where .map(TokenSuppliesInsertDb::from) .collect(); + if supplies.is_empty() { + return anyhow::Ok(()); + } + tracing::debug!(?supplies, "Adding new token supplies to db"); diesel::insert_into(token_supplies_per_epoch::table) From af04490002ddbf6906c08ab877b1bc9fdeb226cf Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 7 Feb 2025 14:55:57 +0000 Subject: [PATCH 13/13] token supply webserver endpoints --- orm/src/token_supplies_per_epoch.rs | 2 +- swagger.yml | 37 +++++++++++++++++++ webserver/src/app.rs | 4 ++ webserver/src/dto/chain.rs | 10 +++++ webserver/src/dto/mod.rs | 1 + webserver/src/handler/chain.rs | 14 +++++++ webserver/src/repository/chain.rs | 57 ++++++++++++++++++++++++++++- webserver/src/response/chain.rs | 8 ++++ webserver/src/service/chain.rs | 20 +++++++++- 9 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 webserver/src/dto/chain.rs diff --git a/orm/src/token_supplies_per_epoch.rs b/orm/src/token_supplies_per_epoch.rs index 6b4eb005..d6fe3e78 100644 --- a/orm/src/token_supplies_per_epoch.rs +++ b/orm/src/token_supplies_per_epoch.rs @@ -7,7 +7,7 @@ use crate::schema::token_supplies_per_epoch; #[derive(Debug, Clone, Queryable, Selectable)] #[diesel(table_name = token_supplies_per_epoch)] #[diesel(check_for_backend(diesel::pg::Pg))] -pub struct TokenSupplies { +pub struct TokenSuppliesDb { pub id: i32, pub address: String, pub epoch: i32, diff --git a/swagger.yml b/swagger.yml index 478570cb..35383508 100644 --- a/swagger.yml +++ b/swagger.yml @@ -656,6 +656,29 @@ paths: value: - address: tnam1pkg30gnt4q0zn7j00r6hms4ajrxn6f5ysyyl7w9m trace: transfer/channel-2/uatom + /api/v1/chain/token-supply: + get: + summary: Get the supply of some token at the given epoch + parameters: + - in: query + name: address + schema: + type: string + required: true + description: Address of the token + - in: query + name: epoch + schema: + type: integer + minimum: 0 + description: Epoch to query + responses: + "200": + description: Chain token supply + content: + application/json: + schema: + $ref: "#/components/schemas/TokenSupply" /api/v1/chain/parameters: get: summary: Get chain parameters @@ -1140,6 +1163,20 @@ components: type: string trace: type: string + TokenSupply: + type: object + required: + [ + address, + totalSupply, + ] + properties: + address: + type: string + totalSupply: + type: string + effectiveSupply: + type: string Parameters: type: object required: diff --git a/webserver/src/app.rs b/webserver/src/app.rs index 1a822957..e11ffcf4 100644 --- a/webserver/src/app.rs +++ b/webserver/src/app.rs @@ -131,6 +131,10 @@ impl ApplicationServer { .route("/chain/parameters", get(chain_handlers::get_parameters)) .route("/chain/rpc-url", get(chain_handlers::get_rpc_url)) .route("/chain/token", get(chain_handlers::get_tokens)) + .route( + "/chain/token-supply", + get(chain_handlers::get_token_supply), + ) .route( "/chain/block/latest", get(chain_handlers::get_last_processed_block), diff --git a/webserver/src/dto/chain.rs b/webserver/src/dto/chain.rs new file mode 100644 index 00000000..a8119867 --- /dev/null +++ b/webserver/src/dto/chain.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; +use validator::Validate; + +#[derive(Clone, Serialize, Deserialize, Validate)] +#[serde(rename_all = "camelCase")] +pub struct TokenSupply { + #[validate(range(min = 0))] + pub epoch: Option, + pub address: String, +} diff --git a/webserver/src/dto/mod.rs b/webserver/src/dto/mod.rs index 207e9644..d967031d 100644 --- a/webserver/src/dto/mod.rs +++ b/webserver/src/dto/mod.rs @@ -1,3 +1,4 @@ +pub mod chain; pub mod crawler_state; pub mod gas; pub mod governance; diff --git a/webserver/src/handler/chain.rs b/webserver/src/handler/chain.rs index e4961e1d..6647969c 100644 --- a/webserver/src/handler/chain.rs +++ b/webserver/src/handler/chain.rs @@ -6,12 +6,15 @@ use axum::extract::State; use axum::http::HeaderMap; use axum::response::Sse; use axum::response::sse::{Event, KeepAlive}; +use axum_extra::extract::Query; use futures::Stream; use tokio_stream::StreamExt; +use crate::dto::chain::TokenSupply as TokenSupplyDto; use crate::error::api::ApiError; use crate::response::chain::{ LastProcessedBlock, LastProcessedEpoch, Parameters, RpcUrl, Token, + TokenSupply as TokenSupplyRsp, }; use crate::state::common::CommonState; @@ -99,3 +102,14 @@ pub async fn get_last_processed_epoch( epoch: last_processed_block.to_string(), })) } + +pub async fn get_token_supply( + Query(query): Query, + State(state): State, +) -> Result>, ApiError> { + let supply = state + .chain_service + .get_token_supply(query.address, query.epoch) + .await?; + Ok(Json(supply)) +} diff --git a/webserver/src/repository/chain.rs b/webserver/src/repository/chain.rs index 247429cf..0d838f90 100644 --- a/webserver/src/repository/chain.rs +++ b/webserver/src/repository/chain.rs @@ -1,12 +1,16 @@ use axum::async_trait; use diesel::dsl::max; use diesel::{ - ExpressionMethods, JoinOnDsl, QueryDsl, RunQueryDsl, SelectableHelper, + BoolExpressionMethods, ExpressionMethods, JoinOnDsl, OptionalExtension, + QueryDsl, RunQueryDsl, SelectableHelper, }; use orm::crawler_state::{ChainCrawlerStateDb, CrawlerNameDb}; use orm::parameters::ParametersDb; -use orm::schema::{chain_parameters, crawler_state, ibc_token, token}; +use orm::schema::{ + chain_parameters, crawler_state, ibc_token, token, token_supplies_per_epoch, +}; use orm::token::{IbcTokenDb, TokenDb}; +use orm::token_supplies_per_epoch::TokenSuppliesDb; use crate::appstate::AppState; @@ -30,6 +34,12 @@ pub trait ChainRepositoryTrait { async fn find_tokens( &self, ) -> Result)>, String>; + + async fn get_token_supply( + &self, + address: String, + epoch: Option, + ) -> Result, String>; } #[async_trait] @@ -120,4 +130,47 @@ impl ChainRepositoryTrait for ChainRepository { .map_err(|e| e.to_string())? .map_err(|e| e.to_string()) } + + async fn get_token_supply( + &self, + address: String, + epoch: Option, + ) -> Result, String> { + let conn = self.app_state.get_db_connection().await; + + conn.interact(move |conn| { + conn.build_transaction().read_only().run(move |conn| { + let epoch = epoch.map_or_else( + || { + crawler_state::dsl::crawler_state + .filter( + crawler_state::dsl::name + .eq(CrawlerNameDb::Chain), + ) + .select(max( + crawler_state::dsl::last_processed_epoch, + )) + .first::>(conn) + .map(|maybe_epoch| maybe_epoch.unwrap_or(0i32)) + }, + Ok, + )?; + + token_supplies_per_epoch::table + .filter( + token_supplies_per_epoch::dsl::address + .eq(&address) + .and( + token_supplies_per_epoch::dsl::epoch.eq(&epoch), + ), + ) + .select(TokenSuppliesDb::as_select()) + .first(conn) + .optional() + }) + }) + .await + .map_err(|e| e.to_string())? + .map_err(|e| e.to_string()) + } } diff --git a/webserver/src/response/chain.rs b/webserver/src/response/chain.rs index ecaebdff..a6ac4b9a 100644 --- a/webserver/src/response/chain.rs +++ b/webserver/src/response/chain.rs @@ -97,3 +97,11 @@ impl From for Token { } } } + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TokenSupply { + pub address: String, + pub total_supply: String, + pub effective_supply: Option, +} diff --git a/webserver/src/service/chain.rs b/webserver/src/service/chain.rs index be989f26..4e508bc9 100644 --- a/webserver/src/service/chain.rs +++ b/webserver/src/service/chain.rs @@ -4,7 +4,7 @@ use shared::token::{IbcToken, Token}; use crate::appstate::AppState; use crate::error::chain::ChainError; use crate::repository::chain::{ChainRepository, ChainRepositoryTrait}; -use crate::response::chain::Parameters; +use crate::response::chain::{Parameters, TokenSupply}; #[derive(Clone)] pub struct ChainService { @@ -65,4 +65,22 @@ impl ChainService { Ok(tokens) } + + pub async fn get_token_supply( + &self, + address: String, + epoch: Option, + ) -> Result, ChainError> { + let maybe_token_supply_db = self + .chain_repo + .get_token_supply(address, epoch) + .await + .map_err(ChainError::Database)?; + + Ok(maybe_token_supply_db.map(|supply| TokenSupply { + address: supply.address, + total_supply: supply.total.to_string(), + effective_supply: supply.effective.map(|s| s.to_string()), + })) + } }