diff --git a/.github/workflows/queue_and_merge.yml b/.github/workflows/queue_and_merge.yml index 89fe201a7..ea53c39db 100644 --- a/.github/workflows/queue_and_merge.yml +++ b/.github/workflows/queue_and_merge.yml @@ -107,7 +107,7 @@ jobs: docker: name: Docker runs-on: ubuntu-latest - if: ${{ github.event_name == 'merge_group' || github.event_name == 'pull_request' }} + # if: ${{ github.event_name == 'merge_group' || github.event_name == 'pull_request' }} env: GIT_LFS_SKIP_SMUDGE: 1 REGISTRY_URL: ghcr.io @@ -157,7 +157,7 @@ jobs: context: . file: Dockerfile build-args: PACKAGE=${{ matrix.docker.package }} - push: ${{ github.event.pull_request.head.repo.full_name == 'anoma/namada-indexer' }} + push: ${{ github.event.pull_request.head.repo.full_name == 'anoma/namada-indexer' || github.event_name == 'push' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha @@ -194,5 +194,4 @@ jobs: run: | echo "ALL_SUCCESS=$(echo "$NEEDS_JSON" | jq '. | to_entries | map([.value.result == "success", .value.result == "skipped"] | any) | all')" >> $GITHUB_ENV - name: check outcomes - run: "[ $ALL_SUCCESS == true ]" - + run: "[ $ALL_SUCCESS == true ]" \ No newline at end of file diff --git a/.github/workflows/scripts/update-package.py b/.github/workflows/scripts/update-package.py index a283c98b3..40de2661a 100644 --- a/.github/workflows/scripts/update-package.py +++ b/.github/workflows/scripts/update-package.py @@ -5,7 +5,8 @@ configuration = { "npmName": "@namada/indexer-client", - "npmVersion": package_version + "npmVersion": package_version, + "httpUserAgent": "" } with open("swagger-codegen.json", 'w+', encoding='utf-8') as f: diff --git a/Cargo.toml b/Cargo.toml index 2d6377f34..d0ae8cb6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ authors = ["Heliax "] edition = "2021" license = "GPL-3.0" readme = "README.md" -version = "1.0.0" +version = "1.1.6" [workspace.dependencies] clokwerk = "0.4.0" @@ -40,14 +40,14 @@ axum-extra = { version = "0.9.3", features = ["query"] } chrono = { version = "0.4.30", features = ["serde"] } async-trait = "0.1.73" anyhow = "1.0.75" -namada_core = { git = "https://github.com/anoma/namada", branch = "fraccaman/rpc-pgf-payments" } -namada_sdk = { git = "https://github.com/anoma/namada", branch = "fraccaman/rpc-pgf-payments", default-features = false, features = ["std", "async-send", "download-params"] } -namada_tx = { git = "https://github.com/anoma/namada", branch = "fraccaman/rpc-pgf-payments" } -namada_governance = { git = "https://github.com/anoma/namada", branch = "fraccaman/rpc-pgf-payments" } -namada_ibc = { git = "https://github.com/anoma/namada", branch = "fraccaman/rpc-pgf-payments" } -namada_token = { git = "https://github.com/anoma/namada", branch = "fraccaman/rpc-pgf-payments" } -namada_parameters = { git = "https://github.com/anoma/namada", branch = "fraccaman/rpc-pgf-payments" } -namada_proof_of_stake = { git = "https://github.com/anoma/namada", branch = "fraccaman/rpc-pgf-payments" } +namada_core = { git = "https://github.com/anoma/namada", branch = "main" } +namada_sdk = { git = "https://github.com/anoma/namada", branch = "main", default-features = false, features = ["std", "async-send", "download-params"] } +namada_tx = { git = "https://github.com/anoma/namada", branch = "main" } +namada_governance = { git = "https://github.com/anoma/namada", branch = "main" } +namada_ibc = { git = "https://github.com/anoma/namada", branch = "main" } +namada_token = { git = "https://github.com/anoma/namada", branch = "main" } +namada_parameters = { git = "https://github.com/anoma/namada", branch = "main" } +namada_proof_of_stake = { git = "https://github.com/anoma/namada", branch = "main" } tendermint = "0.38.0" tendermint-config = "0.38.0" tendermint-rpc = { version = "0.38.0", features = ["http-client"] } diff --git a/Dockerfile b/Dockerfile index ddb1574e7..2ca3e3df0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM lukemathwalker/cargo-chef:latest-rust-1.79-bookworm AS chef +FROM lukemathwalker/cargo-chef:latest-rust-1.81-bookworm AS chef RUN apt-get update && apt-get install -y protobuf-compiler build-essential clang-tools-14 FROM chef AS planner diff --git a/chain/src/main.rs b/chain/src/main.rs index f64aa4405..07d4617c1 100644 --- a/chain/src/main.rs +++ b/chain/src/main.rs @@ -28,7 +28,9 @@ use shared::crawler_state::ChainCrawlerState; use shared::error::{AsDbError, AsRpcError, ContextDbInteractError, MainError}; use shared::id::Id; use shared::token::Token; +use shared::utils::BalanceChange; use shared::validator::ValidatorSet; +use tendermint_rpc::endpoint::block::Response as TendermintBlockResponse; use tendermint_rpc::HttpClient; use tokio_retry::strategy::{jitter, ExponentialBackoff}; use tokio_retry::Retry; @@ -140,6 +142,7 @@ async fn main() -> Result<(), MainError> { initial_query( &client, &conn, + checksums.clone(), config.initial_query_retry_time, config.initial_query_retry_attempts, ) @@ -186,46 +189,15 @@ async fn crawling_fn( return Err(MainError::NoAction); } - tracing::debug!(block = block_height, "Query block..."); - let tm_block_response = - tendermint_service::query_raw_block_at_height(&client, block_height) - .await - .into_rpc_error()?; - tracing::debug!( - block = block_height, - "Raw block contains {} txs...", - tm_block_response.block.data.len() - ); - - tracing::debug!(block = block_height, "Query block results..."); - let tm_block_results_response = - tendermint_service::query_raw_block_results_at_height( - &client, - block_height, - ) - .await - .into_rpc_error()?; - let block_results = BlockResult::from(tm_block_results_response); - - tracing::debug!(block = block_height, "Query epoch..."); - let epoch = - namada_service::get_epoch_at_block_height(&client, block_height) - .await - .into_rpc_error()?; - tracing::debug!(block = block_height, "Query first block in epoch..."); let first_block_in_epoch = namada_service::get_first_block_in_epoch(&client) .await .into_rpc_error()?; - let block = Block::from( - tm_block_response, - &block_results, - checksums, - epoch, - block_height, - ); + let (block, tm_block_response, epoch) = + get_block(block_height, &client, checksums).await?; + tracing::debug!( block = block_height, txs = block.transactions.len(), @@ -243,8 +215,33 @@ async fn crawling_fn( .map(Token::Ibc) .collect::>(); + let native_addresses = + namada_service::query_native_addresses_balance_change(Token::Native( + native_token.clone(), + )); let addresses = block.addresses_with_balance_change(&native_token); + let validators_addresses = if first_block_in_epoch.eq(&block_height) { + namada_service::get_all_consensus_validators_addresses_at( + &client, + epoch - 1, + native_token.clone(), + ) + .await + .into_rpc_error()? + } else { + HashSet::default() + }; + + let block_proposer_address = block + .header + .proposer_address_namada + .as_ref() + .map(|address| BalanceChange { + address: Id::Account(address.clone()), + token: Token::Native(native_token.clone()), + }); + let pgf_receipient_addresses = if first_block_in_epoch.eq(&block_height) { conn.interact(move |conn| { namada_pgf_repository::get_pgf_receipients_balance_changes( @@ -260,8 +257,12 @@ async fn crawling_fn( HashSet::default() }; - let all_balance_changed_addresses = pgf_receipient_addresses - .union(&addresses) + let all_balance_changed_addresses = addresses + .iter() + .chain(block_proposer_address.iter()) + .chain(pgf_receipient_addresses.iter()) + .chain(validators_addresses.iter()) + .chain(native_addresses.iter()) .cloned() .collect::>(); @@ -272,7 +273,13 @@ async fn crawling_fn( ) .await .into_rpc_error()?; - tracing::info!("Updating balance for {} addresses...", addresses.len()); + + tracing::debug!( + block = block_height, + addresses = all_balance_changed_addresses.len(), + "Updating balance for {} addresses...", + all_balance_changed_addresses.len() + ); let next_governance_proposal_id = namada_service::query_next_governance_id(&client, block_height) @@ -298,12 +305,18 @@ async fn crawling_fn( proposals_votes.len() ); - let validators = block.validators(); + let validators = block.new_validators(); let validator_set = ValidatorSet { validators: validators.clone(), epoch, }; + let validators_state_change = block.update_validators_state(); + tracing::debug!( + "Updating {} validators state", + validators_state_change.len() + ); + let addresses = block.bond_addresses(); let bonds = query_bonds(&client, addresses).await.into_rpc_error()?; tracing::debug!( @@ -368,6 +381,7 @@ async fn crawling_fn( withdraws = withdraw_addreses.len(), claimed_rewards = reward_claimers.len(), revealed_pks = revealed_pks.len(), + validator_state = validators_state_change.len(), epoch = epoch, first_block_in_epoch = first_block_in_epoch, block = block_height, @@ -383,6 +397,12 @@ async fn crawling_fn( ibc_tokens, )?; + repository::block::upsert_block( + transaction_conn, + block, + tm_block_response, + )?; + repository::balance::insert_balances( transaction_conn, balances, @@ -402,6 +422,11 @@ async fn crawling_fn( validator_set, )?; + repository::pos::upsert_validator_state( + transaction_conn, + validators_state_change, + )?; + // We first remove all the bonds and then insert the new ones repository::pos::clear_bonds( transaction_conn, @@ -453,29 +478,35 @@ async fn crawling_fn( async fn initial_query( client: &HttpClient, conn: &Object, + checksums: Checksums, retry_time: u64, retry_attempts: usize, ) -> Result<(), MainError> { let retry_strategy = ExponentialBackoff::from_millis(retry_time) .map(jitter) .take(retry_attempts); - Retry::spawn(retry_strategy, || try_initial_query(client, conn)).await + Retry::spawn(retry_strategy, || { + try_initial_query(client, conn, checksums.clone()) + }) + .await } async fn try_initial_query( client: &HttpClient, conn: &Object, + checksums: Checksums, ) -> Result<(), MainError> { tracing::debug!("Querying initial data..."); let block_height = query_last_block_height(client).await.into_rpc_error()?; - let epoch = namada_service::get_epoch_at_block_height(client, block_height) - .await - .into_rpc_error()?; + let first_block_in_epoch = namada_service::get_first_block_in_epoch(client) .await .into_rpc_error()?; + let (block, tm_block_response, epoch) = + get_block(block_height, client, checksums.clone()).await?; + let tokens = query_tokens(client).await.into_rpc_error()?; // This can sometimes fail if the last block height in the node has moved @@ -535,6 +566,12 @@ async fn try_initial_query( .run(|transaction_conn| { repository::balance::insert_tokens(transaction_conn, tokens)?; + repository::block::upsert_block( + transaction_conn, + block, + tm_block_response, + )?; + tracing::debug!( block = block_height, "Inserting {} balances...", @@ -611,3 +648,60 @@ async fn update_crawler_timestamp( .and_then(identity) .into_db_error() } + +async fn get_block( + block_height: u32, + client: &HttpClient, + checksums: Checksums, +) -> Result<(Block, TendermintBlockResponse, u32), MainError> { + tracing::debug!(block = block_height, "Query block..."); + let tm_block_response = + tendermint_service::query_raw_block_at_height(client, block_height) + .await + .into_rpc_error()?; + tracing::debug!( + block = block_height, + "Raw block contains {} txs...", + tm_block_response.block.data.len() + ); + + tracing::debug!(block = block_height, "Query block results..."); + let tm_block_results_response = + tendermint_service::query_raw_block_results_at_height( + client, + block_height, + ) + .await + .into_rpc_error()?; + let block_results = BlockResult::from(tm_block_results_response); + + tracing::debug!(block = block_height, "Query epoch..."); + let epoch = namada_service::get_epoch_at_block_height(client, block_height) + .await + .into_rpc_error()?; + + let proposer_address_namada = namada_service::get_validator_namada_address( + client, + &Id::from(&tm_block_response.block.header.proposer_address), + ) + .await + .into_rpc_error()?; + + tracing::info!( + block = block_height, + tm_address = tm_block_response.block.header.proposer_address.to_string(), + namada_address = ?proposer_address_namada, + "Got block proposer address" + ); + + let block = Block::from( + &tm_block_response, + &block_results, + &proposer_address_namada, + checksums, + epoch, + block_height, + ); + + Ok((block, tm_block_response, epoch)) +} diff --git a/chain/src/repository/balance.rs b/chain/src/repository/balance.rs index 101d08722..253e8c3ef 100644 --- a/chain/src/repository/balance.rs +++ b/chain/src/repository/balance.rs @@ -73,6 +73,8 @@ pub fn insert_tokens( #[cfg(test)] mod tests { + use std::collections::HashSet; + use anyhow::Context; use diesel::{ BoolExpressionMethods, ExpressionMethods, QueryDsl, SelectableHelper, @@ -80,6 +82,8 @@ mod tests { use namada_sdk::token::Amount as NamadaAmount; use namada_sdk::uint::MAX_SIGNED_VALUE; use orm::balances::BalanceDb; + use orm::blocks::BlockInsertDb; + use orm::schema::blocks; use orm::views::balances; use shared::balance::{Amount, Balance}; use shared::id::Id; @@ -130,6 +134,8 @@ mod tests { insert_tokens(conn, vec![token.clone()])?; + seed_blocks_from_balances(conn, &[balance.clone()])?; + insert_balances(conn, vec![balance.clone()])?; let queried_balance = query_balance_by_address(conn, owner, token)?; @@ -175,6 +181,7 @@ mod tests { ..(balance.clone()) }; + seed_blocks_from_balances(conn, &[new_balance.clone()])?; insert_balances(conn, vec![new_balance])?; let queried_balance = @@ -376,6 +383,8 @@ mod tests { seed_tokens_from_balance(conn, fake_balances.clone())?; + seed_blocks_from_balances(conn, &fake_balances)?; + insert_balances(conn, fake_balances.clone())?; assert_eq!(query_all_balances(conn)?.len(), fake_balances.len()); @@ -410,6 +419,7 @@ mod tests { insert_tokens(conn, vec![token.clone()])?; + seed_blocks_from_balances(conn, &[balance.clone()])?; insert_balances(conn, vec![balance.clone()])?; let queried_balance = query_balance_by_address(conn, owner, token)?; @@ -442,6 +452,8 @@ mod tests { insert_tokens(conn, vec![token])?; + seed_blocks_from_balances(conn, &balances)?; + let res = insert_balances(conn, balances); assert!(res.is_ok()); @@ -475,6 +487,8 @@ mod tests { seed_tokens_from_balance(conn, balances.clone())?; + seed_blocks_from_balances(conn, &balances)?; + let res = insert_balances(conn, balances); assert!(res.is_ok()); @@ -500,12 +514,33 @@ mod tests { anyhow::Ok(()) } + fn seed_blocks_from_balances( + conn: &mut PgConnection, + balances: &[Balance], + ) -> anyhow::Result<()> { + for height in balances + .iter() + .map(|balance| balance.height as i32) + .collect::>() + { + diesel::insert_into(blocks::table) + .values::<&BlockInsertDb>(&BlockInsertDb::fake(height)) + .on_conflict_do_nothing() + .execute(conn) + .context("Failed to insert block in db")?; + } + + anyhow::Ok(()) + } + fn seed_balance( conn: &mut PgConnection, balances: Vec, ) -> anyhow::Result<()> { seed_tokens_from_balance(conn, balances.clone())?; + seed_blocks_from_balances(conn, &balances)?; + diesel::insert_into(balance_changes::table) .values::<&Vec>( &balances diff --git a/chain/src/repository/block.rs b/chain/src/repository/block.rs new file mode 100644 index 000000000..5420dd939 --- /dev/null +++ b/chain/src/repository/block.rs @@ -0,0 +1,32 @@ +use anyhow::Context; +use diesel::upsert::excluded; +use diesel::{ExpressionMethods, PgConnection, RunQueryDsl}; +use orm::blocks::BlockInsertDb; +use orm::schema::blocks; +use shared::block::Block; +use tendermint_rpc::endpoint::block::Response as TendermintBlockResponse; + +pub fn upsert_block( + transaction_conn: &mut PgConnection, + block: Block, + tm_block_response: TendermintBlockResponse, +) -> anyhow::Result<()> { + diesel::insert_into(blocks::table) + .values::<&BlockInsertDb>(&BlockInsertDb::from(( + block, + tm_block_response, + ))) + .on_conflict(blocks::height) + .do_update() + .set(( + blocks::hash.eq(excluded(blocks::hash)), + blocks::app_hash.eq(excluded(blocks::app_hash)), + blocks::timestamp.eq(excluded(blocks::timestamp)), + blocks::proposer.eq(excluded(blocks::proposer)), + blocks::epoch.eq(excluded(blocks::epoch)), + )) + .execute(transaction_conn) + .context("Failed to insert block in db")?; + + anyhow::Ok(()) +} diff --git a/chain/src/repository/mod.rs b/chain/src/repository/mod.rs index 296150acf..c33a98c69 100644 --- a/chain/src/repository/mod.rs +++ b/chain/src/repository/mod.rs @@ -1,4 +1,5 @@ pub mod balance; +pub mod block; pub mod crawler_state; pub mod gov; pub mod pgf; diff --git a/chain/src/repository/pos.rs b/chain/src/repository/pos.rs index 0960c229e..f85824e05 100644 --- a/chain/src/repository/pos.rs +++ b/chain/src/repository/pos.rs @@ -10,14 +10,17 @@ use orm::bond::BondInsertDb; use orm::schema::{bonds, pos_rewards, unbonds, validators}; use orm::unbond::UnbondInsertDb; use orm::validators::{ - ValidatorDb, ValidatorUpdateMetadataDb, ValidatorWithMetaInsertDb, + ValidatorDb, ValidatorStateDb, ValidatorUpdateMetadataDb, + ValidatorWithMetaInsertDb, }; use shared::block::Epoch; use shared::bond::Bonds; use shared::id::Id; use shared::tuple_len::TupleLen; use shared::unbond::{UnbondAddresses, Unbonds}; -use shared::validator::{ValidatorMetadataChange, ValidatorSet}; +use shared::validator::{ + ValidatorMetadataChange, ValidatorSet, ValidatorStateChange, +}; use super::utils::MAX_PARAM_SIZE; @@ -250,6 +253,30 @@ pub fn update_validator_metadata( anyhow::Ok(()) } +pub fn upsert_validator_state( + transaction_conn: &mut PgConnection, + validators_states: HashSet, +) -> anyhow::Result<()> { + for change in validators_states { + let state = ValidatorStateDb::from(change.state); + let validator_address = change.address.to_string(); + + diesel::update( + validators::table.filter( + validators::columns::namada_address.eq(validator_address), + ), + ) + .set(validators::columns::state.eq(state)) + .execute(transaction_conn) + .context(format!( + "Failed to update validator state for {}", + change.address + ))?; + } + + Ok(()) +} + pub fn upsert_validators( transaction_conn: &mut PgConnection, validators_set: ValidatorSet, diff --git a/chain/src/services/namada.rs b/chain/src/services/namada.rs index abc6a36d1..b4a48ddd5 100644 --- a/chain/src/services/namada.rs +++ b/chain/src/services/namada.rs @@ -110,7 +110,7 @@ pub async fn query_balance( }) }) .map(futures::future::ready) - .buffer_unordered(20) + .buffer_unordered(32) .collect::>() .await) } @@ -440,7 +440,7 @@ pub async fn query_bonds( Some(bonds) }) .map(futures::future::ready) - .buffer_unordered(20) + .buffer_unordered(32) .collect::>() .await; @@ -513,7 +513,7 @@ pub async fn query_unbonds( } }) .map(futures::future::ready) - .buffer_unordered(20) + .buffer_unordered(32) .collect::>() .await; @@ -530,6 +530,27 @@ pub async fn get_current_epoch(client: &HttpClient) -> anyhow::Result { Ok(epoch.0 as Epoch) } +pub async fn get_all_consensus_validators_addresses_at( + client: &HttpClient, + epoch: u32, + native_token: Id, +) -> anyhow::Result> { + let validators = + rpc::get_all_consensus_validators(client, (epoch as u64).into()) + .await + .context("Failed to query Namada's current epoch")? + .into_iter() + .map(|validator| { + BalanceChange::new( + Id::from(validator.address), + Token::Native(native_token.clone()), + ) + }) + .collect::>(); + + Ok(validators) +} + pub async fn query_tx_code_hash( client: &HttpClient, tx_code_path: &str, @@ -573,7 +594,7 @@ pub async fn query_tallies( Some((proposal, tally_type)) }) .map(futures::future::ready) - .buffer_unordered(20) + .buffer_unordered(32) .collect::>() .await; @@ -584,30 +605,57 @@ pub async fn query_all_votes( client: &HttpClient, proposals_ids: Vec, ) -> anyhow::Result> { - let votes: Vec> = - futures::stream::iter(proposals_ids) - .filter_map(|proposal_id| async move { - let votes = rpc::query_proposal_votes(client, proposal_id) - .await - .ok()?; + let votes = futures::stream::iter(proposals_ids) + .filter_map(|proposal_id| async move { + let votes = + rpc::query_proposal_votes(client, proposal_id).await.ok()?; + + let votes = votes + .into_iter() + .map(|vote| GovernanceVote { + proposal_id, + vote: ProposalVoteKind::from(vote.data), + address: Id::from(vote.delegator), + }) + .collect::>(); + + Some(votes) + }) + .map(futures::future::ready) + .buffer_unordered(32) + .collect::>() + .await; - let votes = votes - .into_iter() - .map(|vote| GovernanceVote { - proposal_id, - vote: ProposalVoteKind::from(vote.data), - address: Id::from(vote.delegator), - }) - .collect::>(); + let mut voter_count: HashMap<(Id, u64), u64> = HashMap::new(); + for vote in votes.iter().flatten() { + *voter_count + .entry((vote.address.clone(), vote.proposal_id)) + .or_insert(0) += 1; + } - Some(votes) + let mut seen_voters = HashSet::new(); + anyhow::Ok( + votes + .iter() + .flatten() + .filter(|&vote| { + seen_voters.insert((vote.address.clone(), vote.proposal_id)) }) - .map(futures::future::ready) - .buffer_unordered(20) - .collect::>() - .await; - - anyhow::Ok(votes.iter().flatten().cloned().collect()) + .cloned() + .map(|mut vote| { + if let Some(count) = + voter_count.get(&(vote.address.clone(), vote.proposal_id)) + { + if *count > 1_u64 { + vote.vote = ProposalVoteKind::Unknown; + } + vote + } else { + vote + } + }) + .collect(), + ) } pub async fn get_validator_set_at_epoch( @@ -686,13 +734,41 @@ pub async fn get_validator_set_at_epoch( state: validator_state }) }) - .buffer_unordered(100) + .buffer_unordered(32) .try_collect::>() .await?; Ok(ValidatorSet { validators, epoch }) } +pub async fn get_validator_namada_address( + client: &HttpClient, + tm_addr: &Id, +) -> anyhow::Result> { + let validator = RPC + .vp() + .pos() + .validator_by_tm_addr(client, &tm_addr.to_string().to_uppercase()) + .await?; + + Ok(validator.map(Id::from)) +} + +pub fn query_native_addresses_balance_change( + native_token: Token, +) -> HashSet { + [ + NamadaSdkAddress::Internal(InternalAddress::Governance), + NamadaSdkAddress::Internal(InternalAddress::PoS), + NamadaSdkAddress::Internal(InternalAddress::Masp), + NamadaSdkAddress::Internal(InternalAddress::Pgf), + NamadaSdkAddress::Internal(InternalAddress::Ibc), + ] + .into_iter() + .map(|address| BalanceChange::new(Id::from(address), native_token.clone())) + .collect::>() +} + pub async fn query_pipeline_length(client: &HttpClient) -> anyhow::Result { let pos_parameters = rpc::get_pos_params(client) .await diff --git a/governance/src/services/namada.rs b/governance/src/services/namada.rs index 8d1ccd281..ca09b292d 100644 --- a/governance/src/services/namada.rs +++ b/governance/src/services/namada.rs @@ -83,7 +83,7 @@ pub async fn get_governance_proposals_updates( } }) .map(futures::future::ready) - .buffer_unordered(20) + .buffer_unordered(32) .collect::>() .await) } diff --git a/orm/Cargo.toml b/orm/Cargo.toml index 892fbd590..3d0234cf1 100644 --- a/orm/Cargo.toml +++ b/orm/Cargo.toml @@ -24,3 +24,4 @@ shared.workspace = true bigdecimal.workspace = true chrono.workspace = true serde_json.workspace = true +tendermint-rpc.workspace = true diff --git a/orm/migrations/2024-06-09-102302_transactions/up.sql b/orm/migrations/2024-06-09-102302_transactions/up.sql index ab070c063..ef7abd0ba 100644 --- a/orm/migrations/2024-06-09-102302_transactions/up.sql +++ b/orm/migrations/2024-06-09-102302_transactions/up.sql @@ -44,4 +44,4 @@ CREATE TABLE inner_transactions ( ); CREATE INDEX index_wrapper_transactions_block_height ON wrapper_transactions (block_height); -CREATE INDEX index_inner_transactions_memo ON inner_transactions (memo); +CREATE INDEX index_inner_transactions_memo ON inner_transactions (memo); \ No newline at end of file diff --git a/orm/migrations/2024-07-04-103941_crawler_state/down.sql b/orm/migrations/2024-07-04-103941_crawler_state/down.sql index 9122f91e4..23de38c6d 100644 --- a/orm/migrations/2024-07-04-103941_crawler_state/down.sql +++ b/orm/migrations/2024-07-04-103941_crawler_state/down.sql @@ -1,4 +1,5 @@ -- This file should undo anything in `up.sql` DROP TABLE crawler_state; + DROP TYPE CRAWLER_NAME; diff --git a/orm/migrations/2024-12-01-170248_ibc_ack/down.sql b/orm/migrations/2024-12-01-170248_ibc_ack/down.sql new file mode 100644 index 000000000..14cc16f4c --- /dev/null +++ b/orm/migrations/2024-12-01-170248_ibc_ack/down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS ibc_ack; + +DROP TYPE IF EXISTS IBC_STATUS; diff --git a/orm/migrations/2024-12-01-170248_ibc_ack/up.sql b/orm/migrations/2024-12-01-170248_ibc_ack/up.sql new file mode 100644 index 000000000..652a3e23e --- /dev/null +++ b/orm/migrations/2024-12-01-170248_ibc_ack/up.sql @@ -0,0 +1,9 @@ +-- Your SQL goes here +CREATE TYPE IBC_STATUS AS ENUM ('fail', 'success', 'timeout', 'unknown'); + +CREATE TABLE ibc_ack ( + id VARCHAR PRIMARY KEY, + tx_hash VARCHAR NOT NULL, + timeout BIGINT NOT NULL, + status IBC_STATUS NOT NULL +); diff --git a/orm/migrations/2024-12-09-225148_init_blocks/down.sql b/orm/migrations/2024-12-09-225148_init_blocks/down.sql new file mode 100644 index 000000000..a57d19684 --- /dev/null +++ b/orm/migrations/2024-12-09-225148_init_blocks/down.sql @@ -0,0 +1,9 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE balance_changes + DROP CONSTRAINT fk_balance_changes_height; + +ALTER TABLE wrapper_transactions + DROP CONSTRAINT fk_wrapper_transactions_height; + +DROP TABLE IF EXISTS blocks; + diff --git a/orm/migrations/2024-12-09-225148_init_blocks/up.sql b/orm/migrations/2024-12-09-225148_init_blocks/up.sql new file mode 100644 index 000000000..2c519cce3 --- /dev/null +++ b/orm/migrations/2024-12-09-225148_init_blocks/up.sql @@ -0,0 +1,40 @@ +-- Your SQL goes here +CREATE TABLE blocks ( + height integer PRIMARY KEY, + hash VARCHAR(64), + app_hash varchar(64), + timestamp timestamp, + proposer varchar, + epoch int +); + +ALTER TABLE blocks + ADD UNIQUE (hash); + +CREATE INDEX index_blocks_epoch ON blocks (epoch); + +-- Populate null blocks for all existing wrapper_transactions and balance_changes to satisfy foreign key constraints +INSERT INTO blocks ( SELECT DISTINCT + height, + NULL::varchar AS hash, + NULL::varchar AS app_hash, + NULL::timestamp AS timestamp, + NULL::varchar AS proposer, + NULL::int AS epoch + FROM ( SELECT DISTINCT + block_height AS height + FROM + wrapper_transactions + UNION + SELECT DISTINCT + height + FROM + balance_changes)); + +-- Create foreign key constraints for wrapper_transactions and balance_changes +ALTER TABLE wrapper_transactions + ADD CONSTRAINT fk_wrapper_transactions_height FOREIGN KEY (block_height) REFERENCES blocks (height) ON DELETE RESTRICT; + +ALTER TABLE balance_changes + ADD CONSTRAINT fk_balance_changes_height FOREIGN KEY (height) REFERENCES blocks (height) ON DELETE RESTRICT; + diff --git a/orm/migrations/2024-12-10-104502_transaction_types/down.sql b/orm/migrations/2024-12-10-104502_transaction_types/down.sql new file mode 100644 index 000000000..b13ef0e56 --- /dev/null +++ b/orm/migrations/2024-12-10-104502_transaction_types/down.sql @@ -0,0 +1,35 @@ +-- This file should undo anything in `up.sql` + +-- Step 1: Rename the existing enum type +ALTER TYPE TRANSACTION_KIND RENAME TO TRANSACTION_KIND_OLD; + +-- Step 2: Create the new enum type without the added values +CREATE TYPE TRANSACTION_KIND AS ENUM ( + 'transparent_transfer', + 'shielded_transfer', + 'shielding_transfer', + 'unshielding_transfer', + 'ibc_msg_transfer', + 'bond', + 'redelegation', + 'unbond', + 'withdraw', + 'claim_rewards', + 'vote_proposal', + 'init_proposal', + 'change_metadata', + 'change_commission', + 'reveal_pk', + 'become_validator', + 'unknown' +); + +-- Step 3: Update all columns to use the new enum type +ALTER TABLE inner_transactions ALTER COLUMN kind TYPE TRANSACTION_KIND +USING kind::text::TRANSACTION_KIND; + +ALTER TABLE gas ALTER COLUMN tx_kind TYPE TRANSACTION_KIND +USING tx_kind::text::TRANSACTION_KIND; + +-- Step 4: Drop the old enum type +DROP TYPE TRANSACTION_KIND_OLD; diff --git a/orm/migrations/2024-12-10-104502_transaction_types/up.sql b/orm/migrations/2024-12-10-104502_transaction_types/up.sql new file mode 100644 index 000000000..6656ce277 --- /dev/null +++ b/orm/migrations/2024-12-10-104502_transaction_types/up.sql @@ -0,0 +1,4 @@ +-- Your SQL goes here +ALTER TYPE TRANSACTION_KIND ADD VALUE 'reactivate_validator'; +ALTER TYPE TRANSACTION_KIND ADD VALUE 'deactivate_validator'; +ALTER TYPE TRANSACTION_KIND ADD VALUE 'unjail_validator'; \ No newline at end of file diff --git a/orm/migrations/2024-12-10-110059_validator_states/down.sql b/orm/migrations/2024-12-10-110059_validator_states/down.sql new file mode 100644 index 000000000..2a3866c86 --- /dev/null +++ b/orm/migrations/2024-12-10-110059_validator_states/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +SELECT 1; diff --git a/orm/migrations/2024-12-10-110059_validator_states/up.sql b/orm/migrations/2024-12-10-110059_validator_states/up.sql new file mode 100644 index 000000000..ef893141f --- /dev/null +++ b/orm/migrations/2024-12-10-110059_validator_states/up.sql @@ -0,0 +1,4 @@ +-- Your SQL goes here +ALTER TYPE VALIDATOR_STATE ADD VALUE 'deactivating'; +ALTER TYPE VALIDATOR_STATE ADD VALUE 'reactivating'; +ALTER TYPE VALIDATOR_STATE ADD VALUE 'unjailing'; \ No newline at end of file diff --git a/orm/migrations/2024-12-17-095036_transaction_gas_used/down.sql b/orm/migrations/2024-12-17-095036_transaction_gas_used/down.sql new file mode 100644 index 000000000..0e2cb6fad --- /dev/null +++ b/orm/migrations/2024-12-17-095036_transaction_gas_used/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE wrapper_transactions DROP COLUMN gas_used; \ No newline at end of file diff --git a/orm/migrations/2024-12-17-095036_transaction_gas_used/up.sql b/orm/migrations/2024-12-17-095036_transaction_gas_used/up.sql new file mode 100644 index 000000000..2ace24652 --- /dev/null +++ b/orm/migrations/2024-12-17-095036_transaction_gas_used/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE wrapper_transactions ADD COLUMN gas_used VARCHAR; \ No newline at end of file diff --git a/orm/migrations/2024-12-17-103655_transaction_history/down.sql b/orm/migrations/2024-12-17-103655_transaction_history/down.sql new file mode 100644 index 000000000..c58298880 --- /dev/null +++ b/orm/migrations/2024-12-17-103655_transaction_history/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` +DROP TABLE transaction_history; + +DROP TYPE IF EXISTS HISTORY_KIND; diff --git a/orm/migrations/2024-12-17-103655_transaction_history/up.sql b/orm/migrations/2024-12-17-103655_transaction_history/up.sql new file mode 100644 index 000000000..87f7a2670 --- /dev/null +++ b/orm/migrations/2024-12-17-103655_transaction_history/up.sql @@ -0,0 +1,13 @@ +-- Your SQL goes here +CREATE TYPE HISTORY_KIND AS ENUM ('received', 'sent'); + +CREATE TABLE transaction_history ( + id SERIAL PRIMARY KEY, + inner_tx_id VARCHAR(64) NOT NULL, + target VARCHAR NOT NULL, + kind HISTORY_KIND NOT NULL, + CONSTRAINT fk_inner_tx_id FOREIGN KEY(inner_tx_id) REFERENCES inner_transactions(id) ON DELETE CASCADE +); + +CREATE UNIQUE INDEX index_transaction_history_target_inner_tx_id ON transaction_history(inner_tx_id, target, kind); +CREATE INDEX index_transaction_history_target ON transaction_history (target); diff --git a/orm/migrations/2024-12-20-092544_gas_eastimation/down.sql b/orm/migrations/2024-12-20-092544_gas_eastimation/down.sql new file mode 100644 index 000000000..b49868e77 --- /dev/null +++ b/orm/migrations/2024-12-20-092544_gas_eastimation/down.sql @@ -0,0 +1,5 @@ +-- This file should undo anything in `up.sql` +DROP TABLE IF EXISTS gas_estimations; + +DROP INDEX IF EXISTS wrapper_transactions_gas; +DROP INDEX IF EXISTS inner_transactions_kind; \ No newline at end of file diff --git a/orm/migrations/2024-12-20-092544_gas_eastimation/up.sql b/orm/migrations/2024-12-20-092544_gas_eastimation/up.sql new file mode 100644 index 000000000..6eb689f8d --- /dev/null +++ b/orm/migrations/2024-12-20-092544_gas_eastimation/up.sql @@ -0,0 +1,26 @@ +-- Your SQL goes here +CREATE TABLE gas_estimations ( + id SERIAL PRIMARY KEY, + wrapper_id VARCHAR(64) NOT NULL, + transparent_transfer INT NOT NULL, + shielded_transfer INT NOT NULL, + shielding_transfer INT NOT NULL, + unshielding_transfer INT NOT NULL, + ibc_msg_transfer INT NOT NULL, + bond INT NOT NULL, + redelegation INT NOT NULL, + unbond INT NOT NULL, + withdraw INT NOT NULL, + claim_rewards INT NOT NULL, + vote_proposal INT NOT NULL, + reveal_pk INT NOT NULL, + tx_size INT NOT NULL, + signatures INT NOT NULL, + CONSTRAINT fk_wrapper_id FOREIGN KEY(wrapper_id) REFERENCES wrapper_transactions(id) ON DELETE CASCADE +); + +ALTER TABLE wrapper_transactions ALTER COLUMN gas_used TYPE INTEGER USING (gas_used::integer) ; + +CREATE INDEX wrapper_transactions_gas ON wrapper_transactions (gas_used); + +CREATE INDEX inner_transactions_kind ON inner_transactions (kind); \ No newline at end of file diff --git a/orm/migrations/2025-01-09-135629_update_masp_txs_gas_limits/down.sql b/orm/migrations/2025-01-09-135629_update_masp_txs_gas_limits/down.sql new file mode 100644 index 000000000..c31cb9be1 --- /dev/null +++ b/orm/migrations/2025-01-09-135629_update_masp_txs_gas_limits/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` + +UPDATE gas SET gas_limit = 50_000 WHERE tx_kind = 'shielded_transfer'; +UPDATE gas SET gas_limit = 50_000 WHERE tx_kind = 'unshielding_transfer'; diff --git a/orm/migrations/2025-01-09-135629_update_masp_txs_gas_limits/up.sql b/orm/migrations/2025-01-09-135629_update_masp_txs_gas_limits/up.sql new file mode 100644 index 000000000..679a54bb6 --- /dev/null +++ b/orm/migrations/2025-01-09-135629_update_masp_txs_gas_limits/up.sql @@ -0,0 +1,4 @@ +-- Your SQL goes here + +UPDATE gas SET gas_limit = 60_000 WHERE tx_kind = 'shielded_transfer'; +UPDATE gas SET gas_limit = 60_000 WHERE tx_kind = 'unshielding_transfer'; diff --git a/orm/migrations/2025-01-13-115313_vote_unknown/down.sql b/orm/migrations/2025-01-13-115313_vote_unknown/down.sql new file mode 100644 index 000000000..c7c9cbeb4 --- /dev/null +++ b/orm/migrations/2025-01-13-115313_vote_unknown/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +SELECT 1; \ No newline at end of file diff --git a/orm/migrations/2025-01-13-115313_vote_unknown/up.sql b/orm/migrations/2025-01-13-115313_vote_unknown/up.sql new file mode 100644 index 000000000..fbda3ac6b --- /dev/null +++ b/orm/migrations/2025-01-13-115313_vote_unknown/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TYPE VOTE_KIND ADD VALUE 'unknown'; \ No newline at end of file diff --git a/orm/migrations/2025-01-15-130138_transaction_kind_mixed_transfer/down.sql b/orm/migrations/2025-01-15-130138_transaction_kind_mixed_transfer/down.sql new file mode 100644 index 000000000..c7c9cbeb4 --- /dev/null +++ b/orm/migrations/2025-01-15-130138_transaction_kind_mixed_transfer/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +SELECT 1; \ No newline at end of file diff --git a/orm/migrations/2025-01-15-130138_transaction_kind_mixed_transfer/up.sql b/orm/migrations/2025-01-15-130138_transaction_kind_mixed_transfer/up.sql new file mode 100644 index 000000000..76cfe3b6f --- /dev/null +++ b/orm/migrations/2025-01-15-130138_transaction_kind_mixed_transfer/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TYPE TRANSACTION_KIND ADD VALUE 'mixed_transfer'; \ No newline at end of file diff --git a/orm/migrations/2025-01-16-100856_transaction_amount_per_gas_unit/down.sql b/orm/migrations/2025-01-16-100856_transaction_amount_per_gas_unit/down.sql new file mode 100644 index 000000000..39cc7a5dc --- /dev/null +++ b/orm/migrations/2025-01-16-100856_transaction_amount_per_gas_unit/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE wrapper_transactions DROP COLUMN amount_per_gas_unit; \ No newline at end of file diff --git a/orm/migrations/2025-01-16-100856_transaction_amount_per_gas_unit/up.sql b/orm/migrations/2025-01-16-100856_transaction_amount_per_gas_unit/up.sql new file mode 100644 index 000000000..420e206dc --- /dev/null +++ b/orm/migrations/2025-01-16-100856_transaction_amount_per_gas_unit/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE wrapper_transactions ADD COLUMN amount_per_gas_unit VARCHAR; \ No newline at end of file diff --git a/orm/migrations/2025-01-16-131336_transaction_ibc_types/down.sql b/orm/migrations/2025-01-16-131336_transaction_ibc_types/down.sql new file mode 100644 index 000000000..c7c9cbeb4 --- /dev/null +++ b/orm/migrations/2025-01-16-131336_transaction_ibc_types/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +SELECT 1; \ No newline at end of file diff --git a/orm/migrations/2025-01-16-131336_transaction_ibc_types/up.sql b/orm/migrations/2025-01-16-131336_transaction_ibc_types/up.sql new file mode 100644 index 000000000..dd15f662a --- /dev/null +++ b/orm/migrations/2025-01-16-131336_transaction_ibc_types/up.sql @@ -0,0 +1,4 @@ +-- Your SQL goes here +ALTER TYPE TRANSACTION_KIND ADD VALUE 'ibc_transparent_transfer'; +ALTER TYPE TRANSACTION_KIND ADD VALUE 'ibc_shielding_transfer'; +ALTER TYPE TRANSACTION_KIND ADD VALUE 'ibc_unshielding_transfer'; \ No newline at end of file diff --git a/orm/src/blocks.rs b/orm/src/blocks.rs new file mode 100644 index 000000000..09a03a616 --- /dev/null +++ b/orm/src/blocks.rs @@ -0,0 +1,59 @@ +use diesel::{Insertable, Queryable, Selectable}; +use shared::block::Block; +use tendermint_rpc::endpoint::block::Response as TendermintBlockResponse; + +use crate::schema::blocks; + +#[derive(Insertable, Clone, Queryable, Selectable, Debug)] +#[diesel(table_name = blocks)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct BlockInsertDb { + pub height: i32, + pub hash: Option, + pub app_hash: Option, + pub timestamp: Option, + pub proposer: Option, + pub epoch: Option, +} + +pub type BlockDb = BlockInsertDb; + +impl From<(Block, TendermintBlockResponse)> for BlockInsertDb { + fn from( + (block, tm_block_response): (Block, TendermintBlockResponse), + ) -> Self { + let timestamp = chrono::DateTime::from_timestamp( + tm_block_response.block.header.time.unix_timestamp(), + 0, + ) + .expect("Invalid timestamp") + .naive_utc(); + + Self { + height: block.header.height as i32, + hash: Some(block.hash.to_string()), + app_hash: Some(block.header.app_hash.to_string()), + timestamp: Some(timestamp), + proposer: block.header.proposer_address_namada, + epoch: Some(block.epoch as i32), + } + } +} + +impl BlockInsertDb { + pub fn fake(height: i32) -> Self { + Self { + height, + hash: Some(height.to_string()), /* fake hash but ensures + * uniqueness + * with height */ + app_hash: Some("fake_app_hash".to_string()), /* doesn't require + * uniqueness */ + timestamp: Some( + chrono::DateTime::from_timestamp(0, 0).unwrap().naive_utc(), + ), + proposer: Some("fake_proposer".to_string()), + epoch: Some(0), + } + } +} diff --git a/orm/src/gas.rs b/orm/src/gas.rs index 26e88f2fe..a32257a35 100644 --- a/orm/src/gas.rs +++ b/orm/src/gas.rs @@ -2,9 +2,9 @@ use std::str::FromStr; use bigdecimal::BigDecimal; use diesel::{Insertable, Queryable, Selectable}; -use shared::gas::GasPrice; +use shared::gas::{GasEstimation, GasPrice}; -use crate::schema::{gas, gas_price}; +use crate::schema::{gas, gas_estimations, gas_price}; use crate::transactions::TransactionKindDb; #[derive(Clone, Queryable, Selectable)] @@ -32,3 +32,48 @@ impl From for GasPriceDb { } } } + +#[derive(Clone, Queryable, Selectable, Insertable)] +#[diesel(table_name = gas_estimations)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct GasEstimationDb { + pub wrapper_id: String, + pub transparent_transfer: i32, + pub shielded_transfer: i32, + pub shielding_transfer: i32, + pub unshielding_transfer: i32, + pub ibc_msg_transfer: i32, + pub bond: i32, + pub redelegation: i32, + pub unbond: i32, + pub withdraw: i32, + pub claim_rewards: i32, + pub vote_proposal: i32, + pub reveal_pk: i32, + pub signatures: i32, + pub tx_size: i32, +} + +pub type GasEstimationInsertDb = GasEstimationDb; + +impl From for GasEstimationInsertDb { + fn from(value: GasEstimation) -> Self { + Self { + wrapper_id: value.wrapper_id.to_string(), + transparent_transfer: value.transparent_transfer as i32, + shielded_transfer: value.shielded_transfer as i32, + shielding_transfer: value.shielding_transfer as i32, + unshielding_transfer: value.unshielding_transfer as i32, + ibc_msg_transfer: value.ibc_msg_transfer as i32, + bond: value.bond as i32, + redelegation: value.redelegation as i32, + unbond: value.unbond as i32, + withdraw: value.withdraw as i32, + claim_rewards: value.claim_rewards as i32, + vote_proposal: value.vote_proposal as i32, + reveal_pk: value.reveal_pk as i32, + signatures: value.signatures as i32, + tx_size: value.size as i32, + } + } +} diff --git a/orm/src/governance_votes.rs b/orm/src/governance_votes.rs index f76b7069c..86c23c3f0 100644 --- a/orm/src/governance_votes.rs +++ b/orm/src/governance_votes.rs @@ -10,6 +10,7 @@ pub enum GovernanceVoteKindDb { Nay, Yay, Abstain, + Unknown, } impl From for GovernanceVoteKindDb { @@ -18,6 +19,7 @@ impl From for GovernanceVoteKindDb { ProposalVoteKind::Nay => Self::Nay, ProposalVoteKind::Yay => Self::Yay, ProposalVoteKind::Abstain => Self::Abstain, + ProposalVoteKind::Unknown => Self::Unknown, } } } diff --git a/orm/src/ibc.rs b/orm/src/ibc.rs new file mode 100644 index 000000000..0583f47e5 --- /dev/null +++ b/orm/src/ibc.rs @@ -0,0 +1,56 @@ +use diesel::prelude::Queryable; +use diesel::{AsChangeset, Insertable, Selectable}; +use serde::{Deserialize, Serialize}; +use shared::transaction::{IbcAckStatus, IbcSequence}; + +use crate::schema::ibc_ack; + +#[derive(Debug, Clone, Serialize, Deserialize, diesel_derive_enum::DbEnum)] +#[ExistingTypePath = "crate::schema::sql_types::IbcStatus"] +pub enum IbcAckStatusDb { + Unknown, + Timeout, + Fail, + Success, +} + +impl From for IbcAckStatusDb { + fn from(value: IbcAckStatus) -> Self { + match value { + IbcAckStatus::Success => Self::Success, + IbcAckStatus::Fail => Self::Fail, + IbcAckStatus::Timeout => Self::Timeout, + IbcAckStatus::Unknown => Self::Unknown, + } + } +} + +#[derive(Serialize, Queryable, Insertable, Selectable, Clone, Debug)] +#[diesel(table_name = ibc_ack)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct IbcAckDb { + pub id: String, + pub tx_hash: String, + pub timeout: i64, + pub status: IbcAckStatusDb, +} + +pub type IbcAckInsertDb = IbcAckDb; + +impl From for IbcAckInsertDb { + fn from(value: IbcSequence) -> Self { + Self { + id: value.id(), + tx_hash: value.tx_id.to_string(), + timeout: value.timeout as i64, + status: IbcAckStatusDb::Unknown, + } + } +} + +#[derive(Serialize, AsChangeset, Clone)] +#[diesel(table_name = ibc_ack)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct IbcSequencekStatusUpdateDb { + pub status: IbcAckStatusDb, +} diff --git a/orm/src/lib.rs b/orm/src/lib.rs index 82aa2cdd3..2427200c7 100644 --- a/orm/src/lib.rs +++ b/orm/src/lib.rs @@ -1,4 +1,5 @@ pub mod balances; +pub mod blocks; pub mod bond; pub mod crawler_state; pub mod gas; @@ -6,6 +7,7 @@ pub mod governance_proposal; pub mod governance_votes; pub mod group_by_macros; pub mod helpers; +pub mod ibc; pub mod migrations; pub mod parameters; pub mod pgf; diff --git a/orm/src/schema.rs b/orm/src/schema.rs index 0314a7053..38a7d1217 100644 --- a/orm/src/schema.rs +++ b/orm/src/schema.rs @@ -49,6 +49,22 @@ pub mod sql_types { #[diesel(postgres_type(name = "payment_recurrence"))] pub struct PaymentRecurrence; + #[derive( + diesel::query_builder::QueryId, + std::fmt::Debug, + diesel::sql_types::SqlType, + )] + #[diesel(postgres_type(name = "history_kind"))] + pub struct HistoryKind; + + #[derive( + diesel::query_builder::QueryId, + std::fmt::Debug, + diesel::sql_types::SqlType, + )] + #[diesel(postgres_type(name = "ibc_status"))] + pub struct IbcStatus; + #[derive( diesel::query_builder::QueryId, std::fmt::Debug, @@ -101,6 +117,19 @@ diesel::table! { } } +diesel::table! { + blocks (height) { + height -> Int4, + #[max_length = 64] + hash -> Nullable, + #[max_length = 64] + app_hash -> Nullable, + timestamp -> Nullable, + proposer -> Nullable, + epoch -> Nullable, + } +} + diesel::table! { bonds (id) { id -> Int4, @@ -154,6 +183,28 @@ diesel::table! { } } +diesel::table! { + gas_estimations (id) { + id -> Int4, + #[max_length = 64] + wrapper_id -> Varchar, + transparent_transfer -> Int4, + shielded_transfer -> Int4, + shielding_transfer -> Int4, + unshielding_transfer -> Int4, + ibc_msg_transfer -> Int4, + bond -> Int4, + redelegation -> Int4, + unbond -> Int4, + withdraw -> Int4, + claim_rewards -> Int4, + vote_proposal -> Int4, + reveal_pk -> Int4, + tx_size -> Int4, + signatures -> Int4, + } +} + diesel::table! { gas_price (token) { token -> Varchar, @@ -196,6 +247,18 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::IbcStatus; + + ibc_ack (id) { + id -> Varchar, + tx_hash -> Varchar, + timeout -> Int8, + status -> IbcStatus, + } +} + diesel::table! { ibc_token (address) { #[max_length = 45] @@ -264,6 +327,19 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::HistoryKind; + + transaction_history (id) { + id -> Int4, + #[max_length = 64] + inner_tx_id -> Varchar, + target -> Varchar, + kind -> HistoryKind, + } +} + diesel::table! { unbonds (id) { id -> Int4, @@ -307,33 +383,43 @@ diesel::table! { block_height -> Int4, exit_code -> TransactionResult, atomic -> Bool, + gas_used -> Nullable, + amount_per_gas_unit -> Nullable, } } +diesel::joinable!(balance_changes -> blocks (height)); diesel::joinable!(balance_changes -> token (token)); diesel::joinable!(bonds -> validators (validator_id)); +diesel::joinable!(gas_estimations -> wrapper_transactions (wrapper_id)); diesel::joinable!(governance_votes -> governance_proposals (proposal_id)); 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!(transaction_history -> inner_transactions (inner_tx_id)); diesel::joinable!(unbonds -> validators (validator_id)); +diesel::joinable!(wrapper_transactions -> blocks (block_height)); diesel::allow_tables_to_appear_in_same_query!( balance_changes, + blocks, bonds, chain_parameters, crawler_state, gas, + gas_estimations, gas_price, governance_proposals, governance_votes, + ibc_ack, ibc_token, inner_transactions, pos_rewards, public_good_funding, revealed_pk, token, + transaction_history, unbonds, validators, wrapper_transactions, diff --git a/orm/src/transactions.rs b/orm/src/transactions.rs index b4f820fb3..d207be660 100644 --- a/orm/src/transactions.rs +++ b/orm/src/transactions.rs @@ -1,11 +1,13 @@ use diesel::{Insertable, Queryable, Selectable}; use serde::{Deserialize, Serialize}; use shared::transaction::{ - InnerTransaction, TransactionExitStatus, TransactionKind, - WrapperTransaction, + InnerTransaction, TransactionExitStatus, TransactionHistoryKind, + TransactionKind, TransactionTarget, WrapperTransaction, }; -use crate::schema::{inner_transactions, wrapper_transactions}; +use crate::schema::{ + inner_transactions, transaction_history, wrapper_transactions, +}; #[derive(Debug, Clone, Serialize, Deserialize, diesel_derive_enum::DbEnum)] #[ExistingTypePath = "crate::schema::sql_types::TransactionKind"] @@ -14,7 +16,11 @@ pub enum TransactionKindDb { ShieldedTransfer, ShieldingTransfer, UnshieldingTransfer, + MixedTransfer, IbcMsgTransfer, + IbcTransparentTransfer, + IbcShieldingTransfer, + IbcUnshieldingTransfer, Bond, Redelegation, Unbond, @@ -26,6 +32,9 @@ pub enum TransactionKindDb { ChangeCommission, RevealPk, BecomeValidator, + ReactivateValidator, + DeactivateValidator, + UnjailValidator, Unknown, } @@ -33,31 +42,42 @@ impl From for TransactionKindDb { fn from(value: TransactionKind) -> Self { match value { TransactionKind::TransparentTransfer(_) => { - TransactionKindDb::TransparentTransfer + Self::TransparentTransfer + } + TransactionKind::ShieldedTransfer(_) => Self::ShieldedTransfer, + TransactionKind::UnshieldingTransfer(_) => { + Self::UnshieldingTransfer } - TransactionKind::ShieldedTransfer(_) => { - TransactionKindDb::ShieldedTransfer + TransactionKind::ShieldingTransfer(_) => Self::ShieldingTransfer, + TransactionKind::MixedTransfer(_) => Self::MixedTransfer, + TransactionKind::IbcMsgTransfer(_) => Self::IbcMsgTransfer, + TransactionKind::IbcTrasparentTransfer(_) => { + Self::IbcTransparentTransfer } - TransactionKind::IbcMsgTransfer(_) => { - TransactionKindDb::IbcMsgTransfer + TransactionKind::IbcShieldingTransfer(_) => { + Self::IbcShieldingTransfer } - TransactionKind::Bond(_) => TransactionKindDb::Bond, - TransactionKind::Redelegation(_) => TransactionKindDb::Redelegation, - TransactionKind::Unbond(_) => TransactionKindDb::Unbond, - TransactionKind::Withdraw(_) => TransactionKindDb::Withdraw, - TransactionKind::ClaimRewards(_) => TransactionKindDb::ClaimRewards, - TransactionKind::ProposalVote(_) => TransactionKindDb::VoteProposal, - TransactionKind::InitProposal(_) => TransactionKindDb::InitProposal, - TransactionKind::MetadataChange(_) => { - TransactionKindDb::ChangeMetadata + TransactionKind::IbcUnshieldingTransfer(_) => { + Self::IbcUnshieldingTransfer } - TransactionKind::CommissionChange(_) => { - TransactionKindDb::ChangeCommission + TransactionKind::Bond(_) => Self::Bond, + TransactionKind::Redelegation(_) => Self::Redelegation, + TransactionKind::Unbond(_) => Self::Unbond, + TransactionKind::Withdraw(_) => Self::Withdraw, + TransactionKind::ClaimRewards(_) => Self::ClaimRewards, + TransactionKind::ProposalVote(_) => Self::VoteProposal, + TransactionKind::InitProposal(_) => Self::InitProposal, + TransactionKind::MetadataChange(_) => Self::ChangeMetadata, + TransactionKind::CommissionChange(_) => Self::ChangeCommission, + TransactionKind::DeactivateValidator(_) => { + Self::DeactivateValidator } - TransactionKind::RevealPk(_) => TransactionKindDb::RevealPk, - TransactionKind::BecomeValidator(_) => { - TransactionKindDb::BecomeValidator + TransactionKind::ReactivateValidator(_) => { + Self::ReactivateValidator } + TransactionKind::RevealPk(_) => Self::RevealPk, + TransactionKind::BecomeValidator(_) => Self::BecomeValidator, + TransactionKind::UnjailValidator(_) => Self::UnjailValidator, TransactionKind::Unknown(_) => TransactionKindDb::Unknown, } } @@ -109,17 +129,19 @@ impl InnerTransactionInsertDb { #[derive(Serialize, Queryable, Selectable, Insertable, Clone)] #[diesel(table_name = wrapper_transactions)] #[diesel(check_for_backend(diesel::pg::Pg))] -pub struct WrapperTransactionInsertDb { +pub struct WrapperTransactionDb { pub id: String, pub fee_payer: String, pub fee_token: String, pub gas_limit: String, + pub gas_used: Option, + pub amount_per_gas_unit: Option, pub block_height: i32, pub exit_code: TransactionResultDb, pub atomic: bool, } -pub type WrapperTransactionDb = WrapperTransactionInsertDb; +pub type WrapperTransactionInsertDb = WrapperTransactionDb; impl WrapperTransactionInsertDb { pub fn from(tx: WrapperTransaction) -> Self { @@ -128,9 +150,56 @@ impl WrapperTransactionInsertDb { fee_payer: tx.fee.gas_payer.to_string(), fee_token: tx.fee.gas_token.to_string(), gas_limit: tx.fee.gas, + gas_used: tx.fee.gas_used.map(|gas| gas as i32), + amount_per_gas_unit: Some(tx.fee.amount_per_gas_unit), block_height: tx.block_height as i32, exit_code: TransactionResultDb::from(tx.exit_code), atomic: tx.atomic, } } } + +#[derive(Debug, Clone, Serialize, Deserialize, diesel_derive_enum::DbEnum)] +#[ExistingTypePath = "crate::schema::sql_types::HistoryKind"] +pub enum TransactionHistoryKindDb { + Received, + Sent, +} + +impl From for TransactionHistoryKindDb { + fn from(value: TransactionHistoryKind) -> Self { + match value { + TransactionHistoryKind::Received => Self::Received, + TransactionHistoryKind::Sent => Self::Sent, + } + } +} + +#[derive(Serialize, Queryable, Selectable, Clone)] +#[diesel(table_name = transaction_history)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct TransactionHistoryDb { + pub id: i32, + pub inner_tx_id: String, + pub target: String, + pub kind: TransactionHistoryKindDb, +} + +#[derive(Serialize, Insertable, Clone)] +#[diesel(table_name = transaction_history)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct TransactionHistoryInsertDb { + pub inner_tx_id: String, + pub target: String, + pub kind: TransactionHistoryKindDb, +} + +impl TransactionHistoryInsertDb { + pub fn from(target: TransactionTarget) -> Self { + Self { + inner_tx_id: target.inner_tx.to_string(), + target: target.address, + kind: TransactionHistoryKindDb::from(target.kind), + } + } +} diff --git a/orm/src/validators.rs b/orm/src/validators.rs index 79027c0dc..395325a95 100644 --- a/orm/src/validators.rs +++ b/orm/src/validators.rs @@ -28,6 +28,9 @@ pub enum ValidatorStateDb { BelowThreshold, Inactive, Jailed, + Deactivating, + Reactivating, + Unjailing, Unknown, } @@ -39,6 +42,9 @@ impl From for ValidatorStateDb { ValidatorState::BelowThreshold => Self::BelowThreshold, ValidatorState::Inactive => Self::Inactive, ValidatorState::Jailed => Self::Jailed, + ValidatorState::Deactivating => Self::Deactivating, + ValidatorState::Reactivating => Self::Reactivating, + ValidatorState::Unjailing => Self::Unjailing, ValidatorState::Unknown => Self::Unknown, } } @@ -90,6 +96,14 @@ pub struct ValidatorWithMetaInsertDb { pub state: ValidatorStateDb, } +#[derive(Serialize, Insertable, Clone)] +#[diesel(table_name = validators)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct ValidatorStateChangeDb { + pub namada_address: String, + pub state: ValidatorStateDb, +} + #[derive(Serialize, AsChangeset, Clone)] #[diesel(table_name = validators)] #[diesel(check_for_backend(diesel::pg::Pg))] diff --git a/pos/src/repository/pos.rs b/pos/src/repository/pos.rs index 3efaddd37..2a644ac2c 100644 --- a/pos/src/repository/pos.rs +++ b/pos/src/repository/pos.rs @@ -19,6 +19,7 @@ pub fn upsert_validators( .eq(excluded(validators::columns::max_commission)), validators::columns::commission .eq(excluded(validators::columns::commission)), + validators::columns::state.eq(excluded(validators::columns::state)), )) .execute(transaction_conn) .context("Failed to update validators in db")?; diff --git a/pos/src/services/namada.rs b/pos/src/services/namada.rs index 9261648a8..fa18e9e4a 100644 --- a/pos/src/services/namada.rs +++ b/pos/src/services/namada.rs @@ -3,6 +3,7 @@ use std::collections::HashSet; use anyhow::Context; use futures::{StreamExt, TryStreamExt}; use namada_core::chain::Epoch as NamadaSdkEpoch; +use namada_sdk::address::Address; use namada_sdk::rpc; use shared::block::Epoch; use shared::id::Id; @@ -84,7 +85,65 @@ pub async fn get_validator_set_at_epoch( state: validator_state }) }) - .buffer_unordered(100) + .buffer_unordered(32) + .try_collect::>() + .await?; + + Ok(ValidatorSet { validators, epoch }) +} + +pub async fn get_validators_state( + client: &HttpClient, + validators: Vec, + epoch: Epoch, +) -> anyhow::Result { + let namada_epoch = to_epoch(epoch); + + let validators = futures::stream::iter(validators) + .map(|mut validator| async move { + let validator_address = Address::from(validator.address.clone()); + let validator_state = rpc::get_validator_state( + client, + &validator_address, + Some(namada_epoch), + ) + .await + .with_context(|| { + format!("Failed to query validator {validator_address} state") + })?; + + let validator_state = validator_state + .0 + .map(ValidatorState::from) + .unwrap_or(ValidatorState::Unknown); + + let from_unjailing_state = + validator.state.eq(&ValidatorState::Unjailing) + && !validator_state.eq(&ValidatorState::Jailed); + let from_deactivating_state = + validator.state.eq(&ValidatorState::Deactivating) + && validator_state.eq(&ValidatorState::Inactive); + let from_reactivating_state = + validator.state.eq(&ValidatorState::Reactivating) + && !validator_state.eq(&ValidatorState::Inactive); + let from_concrete_state = ![ + ValidatorState::Deactivating, + ValidatorState::Reactivating, + ValidatorState::Unjailing, + ] + .contains(&validator.state); + + if from_unjailing_state + || from_deactivating_state + || from_reactivating_state + || from_concrete_state + { + validator.state = validator_state; + } + + anyhow::Ok(validator) + }) + .buffer_unordered(32) .try_collect::>() .await?; diff --git a/rewards/src/main.rs b/rewards/src/main.rs index a28e09f54..74a108c0c 100644 --- a/rewards/src/main.rs +++ b/rewards/src/main.rs @@ -81,7 +81,7 @@ async fn crawling_fn( return Err(MainError::NoAction); } - tracing::info!("Starting to update proposals..."); + tracing::info!("Starting to update pos rewards..."); // TODO: change this by querying all the pairs in the database let delegations_pairs = namada_service::query_delegation_pairs(&client) diff --git a/rewards/src/repository/mod.rs b/rewards/src/repository/mod.rs index 9c4ae7b13..399a24fdb 100644 --- a/rewards/src/repository/mod.rs +++ b/rewards/src/repository/mod.rs @@ -1,2 +1,3 @@ pub mod crawler_state; pub mod pos_rewards; +mod utils; diff --git a/rewards/src/repository/pos_rewards.rs b/rewards/src/repository/pos_rewards.rs index 1987926f5..255715c44 100644 --- a/rewards/src/repository/pos_rewards.rs +++ b/rewards/src/repository/pos_rewards.rs @@ -1,12 +1,33 @@ +use anyhow::Context; use diesel::upsert::excluded; use diesel::{ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl}; use orm::pos_rewards::PosRewardInsertDb; use orm::schema::{pos_rewards, validators}; use shared::rewards::Reward; +use shared::tuple_len::TupleLen; + +use super::utils::MAX_PARAM_SIZE; pub fn upsert_rewards( transaction_conn: &mut PgConnection, rewards: Vec, +) -> anyhow::Result<()> { + let rewards_col_count = pos_rewards::all_columns.len() as i64; + + for chunk in rewards + .into_iter() + .collect::>() + .chunks((MAX_PARAM_SIZE as i64 / rewards_col_count) as usize) + { + upsert_rewards_chunk(transaction_conn, chunk.to_vec())?; + } + + anyhow::Ok(()) +} + +fn upsert_rewards_chunk( + transaction_conn: &mut PgConnection, + rewards: Vec, ) -> anyhow::Result<()> { diesel::insert_into(pos_rewards::table) .values::>( @@ -37,7 +58,8 @@ pub fn upsert_rewards( pos_rewards::columns::raw_amount .eq(excluded(pos_rewards::columns::raw_amount)), ) - .execute(transaction_conn)?; + .execute(transaction_conn) + .context("Failed to upsert rewards in db")?; Ok(()) } diff --git a/rewards/src/repository/utils.rs b/rewards/src/repository/utils.rs new file mode 100644 index 000000000..bd4b8ce6d --- /dev/null +++ b/rewards/src/repository/utils.rs @@ -0,0 +1,4 @@ +// Represents maximum number of parameters that we can insert into postgres in +// one go. To get the number of rows that we can insert in one chunk, we have to +// divide MAX_PARAM_SIZE by the number of columns in the given table. +pub const MAX_PARAM_SIZE: u16 = u16::MAX; diff --git a/rewards/src/services/namada.rs b/rewards/src/services/namada.rs index af020d3fd..03a73eade 100644 --- a/rewards/src/services/namada.rs +++ b/rewards/src/services/namada.rs @@ -1,4 +1,5 @@ use std::collections::HashSet; +use std::time::Duration; use anyhow::Context; use futures::StreamExt; @@ -24,9 +25,13 @@ pub async fn query_delegation_pairs( data.into_iter() .fold(HashSet::new(), |mut acc, (bond_id, _)| { acc.insert(DelegationPair { - validator_address: Id::from(bond_id.validator), + validator_address: Id::from(bond_id.validator.clone()), delegator_address: Id::from(bond_id.source), }); + acc.insert(DelegationPair { + validator_address: Id::from(bond_id.validator.clone()), + delegator_address: Id::from(bond_id.validator), + }); acc }); @@ -37,13 +42,97 @@ pub async fn query_rewards( client: &HttpClient, delegation_pairs: HashSet, ) -> anyhow::Result> { - Ok(futures::stream::iter(delegation_pairs) + let mut all_rewards: Vec = Vec::new(); + + let batches: Vec<(usize, Vec)> = delegation_pairs + .clone() + .into_iter() + .collect::>() + .chunks(32) + .enumerate() + .map(|(i, chunk)| (i, chunk.to_vec())) + .collect(); + + tracing::info!( + "Got {} batches with a total of {} rewards to query...", + batches.len(), + delegation_pairs.len() + ); + + let results = futures::stream::iter(batches) + .map(|batch| process_batch_with_retries(client, batch)) + .buffer_unordered(3) + .collect::>() + .await; + + tracing::info!("Done fetching rewards!"); + + for result in results { + match result { + Ok(mut rewards) => all_rewards.append(&mut rewards), + Err(err) => return Err(err), + } + } + + Ok(all_rewards) +} + +pub async fn get_current_epoch(client: &HttpClient) -> anyhow::Result { + let epoch = rpc::query_epoch(client) + .await + .context("Failed to query Namada's current epoch")?; + + Ok(epoch.0 as Epoch) +} + +async fn process_batch_with_retries( + client: &HttpClient, + batch: (usize, Vec), +) -> anyhow::Result> { + let mut retries = 0; + + tracing::info!("Processing batch {}", batch.0); + loop { + let result = process_batch(client, batch.1.clone()).await; + + match result { + Ok(rewards) => { + tracing::info!("Batch {} done!", batch.0); + return Ok(rewards); + } + Err(err) => { + retries += 1; + tracing::warn!( + "Batch reward failed (attempt {}/{}) - Error: {:?}", + retries, + 3, + err + ); + + if retries >= 3 { + tracing::error!( + "Batch reward failed after maximum retries." + ); + return Err(err); + } + tokio::time::sleep(Duration::from_secs(2)).await; + } + } + } +} + +async fn process_batch( + client: &HttpClient, + batch: Vec, +) -> anyhow::Result> { + Ok(futures::stream::iter(batch) .filter_map(|delegation| async move { - tracing::info!( + tracing::debug!( "Fetching rewards {} -> {} ...", delegation.validator_address, delegation.delegator_address ); + let reward = RPC .vp() .pos() @@ -55,7 +144,7 @@ pub async fn query_rewards( .await .ok()?; - tracing::info!( + tracing::debug!( "Done fetching reward for {} -> {}!", delegation.validator_address, delegation.delegator_address @@ -67,15 +156,7 @@ pub async fn query_rewards( }) }) .map(futures::future::ready) - .buffer_unordered(20) + .buffer_unordered(32) .collect::>() .await) } - -pub async fn get_current_epoch(client: &HttpClient) -> anyhow::Result { - let epoch = rpc::query_epoch(client) - .await - .context("Failed to query Namada's current epoch")?; - - Ok(epoch.0 as Epoch) -} diff --git a/shared/src/balance.rs b/shared/src/balance.rs index 9912f5f95..9c96d9ba9 100644 --- a/shared/src/balance.rs +++ b/shared/src/balance.rs @@ -110,7 +110,7 @@ impl Balance { owner: Id::Account(address.to_string()), token, amount: Amount::fake(), - height: (0..10000).fake::(), + height: 0, } } } diff --git a/shared/src/block.rs b/shared/src/block.rs index 7827455e2..a1dad616b 100644 --- a/shared/src/block.rs +++ b/shared/src/block.rs @@ -5,6 +5,7 @@ use namada_ibc::apps::transfer::types::packet::PacketData; use namada_ibc::core::channel::types::msgs::{MsgRecvPacket, PacketMsg}; use namada_ibc::core::handler::types::msgs::MsgEnvelope; use namada_ibc::IbcMessage; +use namada_sdk::address::{Address, InternalAddress}; use namada_sdk::borsh::BorshDeserialize; use namada_sdk::token::Transfer; use subtle_encoding::hex; @@ -20,11 +21,13 @@ use crate::public_key::PublicKey; use crate::token::{IbcToken, Token}; use crate::transaction::{ InnerTransaction, Transaction, TransactionExitStatus, TransactionKind, - WrapperTransaction, + TransactionTarget, WrapperTransaction, }; use crate::unbond::UnbondAddresses; use crate::utils::BalanceChange; -use crate::validator::{Validator, ValidatorMetadataChange, ValidatorState}; +use crate::validator::{ + Validator, ValidatorMetadataChange, ValidatorState, ValidatorStateChange, +}; use crate::vote::GovernanceVote; pub type Epoch = u32; @@ -107,12 +110,14 @@ pub struct Block { impl Block { pub fn from( - block_response: TendermintBlockResponse, + block_response: &TendermintBlockResponse, block_results: &BlockResult, + proposer_address_namada: &Option, checksums: Checksums, epoch: Epoch, block_height: BlockHeight, ) -> Self { + let masp_address = Address::Internal(InternalAddress::Masp); let transactions = block_response .block .data @@ -125,6 +130,7 @@ impl Block { block_height, checksums.clone(), block_results, + &masp_address, ) .map_err(|reason| { tracing::info!("Couldn't deserialize tx due to {}", reason); @@ -138,14 +144,17 @@ impl Block { header: BlockHeader { height: block_response.block.header.height.value() as BlockHeight, - proposer_address: block_response + proposer_address_tm: block_response .block .header .proposer_address .to_string() .to_lowercase(), + proposer_address_namada: proposer_address_namada + .as_ref() + .map(Id::to_string), timestamp: block_response.block.header.time.to_string(), - app_hash: Id::from(block_response.block.header.app_hash), + app_hash: Id::from(&block_response.block.header.app_hash), }, transactions, epoch, @@ -166,6 +175,569 @@ impl Block { .collect() } + pub fn sources(&self) -> HashSet { + self.inner_txs() + .into_iter() + .flat_map(|tx| match tx.kind { + TransactionKind::TransparentTransfer(transparent_transfer) => { + if let Some(data) = transparent_transfer { + let sources = data + .sources + .0 + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + let targets = data + .targets + .0 + .keys() + .map(|account| { + TransactionTarget::received( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + [sources, targets].concat() + } else { + vec![] + } + } + TransactionKind::MixedTransfer(transparent_transfer) => { + if let Some(data) = transparent_transfer { + let sources = data + .sources + .0 + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + let targets = data + .targets + .0 + .keys() + .map(|account| { + TransactionTarget::received( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + [sources, targets].concat() + } else { + vec![] + } + } + TransactionKind::ShieldedTransfer(transparent_transfer) => { + if let Some(data) = transparent_transfer { + let sources = data + .sources + .0 + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + let targets = data + .targets + .0 + .keys() + .map(|account| { + TransactionTarget::received( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + [sources, targets].concat() + } else { + vec![] + } + } + TransactionKind::UnshieldingTransfer(transparent_transfer) => { + if let Some(data) = transparent_transfer { + let sources = data + .sources + .0 + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + let targets = data + .targets + .0 + .keys() + .map(|account| { + TransactionTarget::received( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + [sources, targets].concat() + } else { + vec![] + } + } + TransactionKind::ShieldingTransfer(transparent_transfer) => { + if let Some(data) = transparent_transfer { + let sources = data + .sources + .0 + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + let targets = data + .targets + .0 + .keys() + .map(|account| { + TransactionTarget::received( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + [sources, targets].concat() + } else { + vec![] + } + } + TransactionKind::IbcMsgTransfer(ibc_message) => { + if let Some(data) = ibc_message { + match data.0 { + IbcMessage::Envelope(_) => vec![], + IbcMessage::Transfer(msg_transfer) => { + if let Some(transfer) = msg_transfer.transfer { + let sources = transfer + .sources + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + let targets = transfer + .targets + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + [sources, targets].concat() + } else { + vec![] + } + } + IbcMessage::NftTransfer(msg_nft_transfer) => { + if let Some(transfer) = + msg_nft_transfer.transfer + { + let sources = transfer + .sources + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + let targets = transfer + .targets + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + [sources, targets].concat() + } else { + vec![] + } + } + } + } else { + vec![] + } + } + TransactionKind::IbcTrasparentTransfer((ibc_message, _)) => { + if let Some(data) = ibc_message { + match data.0 { + IbcMessage::Transfer(transfer) => { + let sources = transfer + .clone() + .transfer + .unwrap_or_default() + .sources + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + let targets = transfer + .transfer + .unwrap_or_default() + .targets + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + [sources, targets].concat() + } + IbcMessage::NftTransfer(transfer) => { + let sources = transfer + .clone() + .transfer + .unwrap_or_default() + .sources + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + let targets = transfer + .transfer + .unwrap_or_default() + .targets + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + [sources, targets].concat() + } + _ => vec![], + } + } else { + vec![] + } + } + TransactionKind::IbcShieldingTransfer((ibc_message, _)) => { + if let Some(data) = ibc_message { + match data.0 { + IbcMessage::Transfer(transfer) => { + let sources = transfer + .clone() + .transfer + .unwrap_or_default() + .sources + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + let targets = transfer + .transfer + .unwrap_or_default() + .targets + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + [sources, targets].concat() + } + IbcMessage::NftTransfer(transfer) => { + let sources = transfer + .clone() + .transfer + .unwrap_or_default() + .sources + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + let targets = transfer + .transfer + .unwrap_or_default() + .targets + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + [sources, targets].concat() + } + _ => vec![], + } + } else { + vec![] + } + } + TransactionKind::IbcUnshieldingTransfer((ibc_message, _)) => { + if let Some(data) = ibc_message { + match data.0 { + IbcMessage::Transfer(transfer) => { + let sources = transfer + .clone() + .transfer + .unwrap_or_default() + .sources + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + let targets = transfer + .transfer + .unwrap_or_default() + .targets + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + [sources, targets].concat() + } + IbcMessage::NftTransfer(transfer) => { + let sources = transfer + .clone() + .transfer + .unwrap_or_default() + .sources + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + let targets = transfer + .transfer + .unwrap_or_default() + .targets + .keys() + .map(|account| { + TransactionTarget::sent( + tx.tx_id.clone(), + account.owner.to_string(), + ) + }) + .collect::>(); + [sources, targets].concat() + } + _ => vec![], + } + } else { + vec![] + } + } + TransactionKind::Bond(bond) => { + if let Some(data) = bond { + let source = + data.source.unwrap_or(data.validator.clone()); + vec![TransactionTarget::sent( + tx.tx_id.clone(), + source.to_string(), + )] + } else { + vec![] + } + } + TransactionKind::Redelegation(redelegation) => { + if let Some(data) = redelegation { + vec![TransactionTarget::sent( + tx.tx_id.clone(), + data.owner.to_string(), + )] + } else { + vec![] + } + } + TransactionKind::Unbond(unbond) => { + if let Some(data) = unbond { + let source = + data.source.unwrap_or(data.validator.clone()); + vec![TransactionTarget::sent( + tx.tx_id.clone(), + source.to_string(), + )] + } else { + vec![] + } + } + TransactionKind::Withdraw(withdraw) => { + if let Some(data) = withdraw { + let source = + data.source.unwrap_or(data.validator.clone()); + vec![TransactionTarget::sent( + tx.tx_id.clone(), + source.to_string(), + )] + } else { + vec![] + } + } + TransactionKind::ClaimRewards(claim_rewards) => { + if let Some(data) = claim_rewards { + let source = + data.source.unwrap_or(data.validator.clone()); + vec![TransactionTarget::sent( + tx.tx_id.clone(), + source.to_string(), + )] + } else { + vec![] + } + } + TransactionKind::ProposalVote(vote_proposal_data) => { + if let Some(data) = vote_proposal_data { + vec![TransactionTarget::sent( + tx.tx_id, + data.voter.to_string(), + )] + } else { + vec![] + } + } + TransactionKind::InitProposal(init_proposal_data) => { + if let Some(data) = init_proposal_data { + vec![TransactionTarget::sent( + tx.tx_id, + data.author.to_string(), + )] + } else { + vec![] + } + } + TransactionKind::MetadataChange(meta_data_change) => { + if let Some(data) = meta_data_change { + vec![TransactionTarget::sent( + tx.tx_id, + data.validator.to_string(), + )] + } else { + vec![] + } + } + TransactionKind::CommissionChange(commission_change) => { + if let Some(data) = commission_change { + vec![TransactionTarget::sent( + tx.tx_id, + data.validator.to_string(), + )] + } else { + vec![] + } + } + TransactionKind::RevealPk(reveal_pk_data) => { + if let Some(data) = reveal_pk_data { + let source = Address::from(&data.public_key); + vec![TransactionTarget::sent( + tx.tx_id, + source.to_string(), + )] + } else { + vec![] + } + } + TransactionKind::BecomeValidator(become_validator) => { + if let Some(data) = become_validator { + vec![TransactionTarget::sent( + tx.tx_id, + data.address.to_string(), + )] + } else { + vec![] + } + } + TransactionKind::ReactivateValidator(address) => { + if let Some(data) = address { + vec![TransactionTarget::sent( + tx.tx_id, + data.to_string(), + )] + } else { + vec![] + } + } + TransactionKind::DeactivateValidator(address) => { + if let Some(data) = address { + vec![TransactionTarget::sent( + tx.tx_id, + data.to_string(), + )] + } else { + vec![] + } + } + TransactionKind::UnjailValidator(address) => { + if let Some(data) = address { + vec![TransactionTarget::sent( + tx.tx_id, + data.to_string(), + )] + } else { + vec![] + } + } + TransactionKind::Unknown(_) => vec![], + }) + .collect::>() + } + pub fn governance_proposal( &self, mut next_proposal_id: u64, @@ -380,38 +952,36 @@ impl Block { let (msg, packet_data) = data?; let denom = packet_data.token.denom.to_string(); - // If the denom is the native token, we can just return the - // receiver - if denom.contains(&native_token.to_string()) { - vec![BalanceChange::new( - Id::Account(String::from( - packet_data.receiver.as_ref(), - )), - Token::Native(native_token.clone()), - )] - } else { - let ibc_trace = format!( - "{}/{}/{}", - msg.packet.port_id_on_b, - msg.packet.chan_id_on_b, - packet_data.token.denom - ); + let ibc_trace = format!( + "{}/{}/{}", + msg.packet.port_id_on_b, + msg.packet.chan_id_on_b, + packet_data.token.denom + ); - let trace = Id::IbcTrace(ibc_trace.clone()); - let address = - namada_ibc::trace::convert_to_address(ibc_trace) - .expect("Failed to convert IBC trace to address"); + let trace = Id::IbcTrace(ibc_trace.clone()); + let address = namada_ibc::trace::convert_to_address(ibc_trace) + .expect("Failed to convert IBC trace to address"); + + let mut balances = vec![BalanceChange::new( + Id::Account(String::from(packet_data.receiver.as_ref())), + Token::Ibc(IbcToken { + address: Id::from(address.clone()), + trace, + }), + )]; - vec![BalanceChange::new( + // If the denom contains the namada native token, try to fetch + // the balance + if denom.contains(&native_token.to_string()) { + balances.push(BalanceChange::new( Id::Account(String::from( packet_data.receiver.as_ref(), )), - Token::Ibc(IbcToken { - address: Id::from(address.clone()), - trace, - }), - )] + Token::Native(native_token.clone()), + )) } + balances } TransactionKind::TransparentTransfer(data) => { let data = data.as_ref()?; @@ -506,7 +1076,7 @@ impl Block { Some(recv_msg) } - pub fn validators(&self) -> HashSet { + pub fn new_validators(&self) -> HashSet { self.transactions .iter() .flat_map(|(_, txs)| txs) @@ -538,6 +1108,34 @@ impl Block { .collect() } + pub fn update_validators_state(&self) -> HashSet { + self.transactions + .iter() + .flat_map(|(_, txs)| txs) + .filter(|tx| { + tx.data.is_some() + && tx.exit_code == TransactionExitStatus::Applied + }) + .filter_map(|tx| match &tx.kind { + TransactionKind::DeactivateValidator(data) => { + let data = data.clone().unwrap(); + Some(ValidatorStateChange { + address: Id::from(data), + state: ValidatorState::Deactivating, + }) + } + TransactionKind::ReactivateValidator(data) => { + let data = data.clone().unwrap(); + Some(ValidatorStateChange { + address: Id::from(data), + state: ValidatorState::Reactivating, + }) + } + _ => None, + }) + .collect() + } + pub fn bond_addresses(&self) -> HashSet { self.transactions .iter() diff --git a/shared/src/block_result.rs b/shared/src/block_result.rs index d0e7f08d8..42d47989a 100644 --- a/shared/src/block_result.rs +++ b/shared/src/block_result.rs @@ -10,6 +10,7 @@ use crate::transaction::TransactionExitStatus; #[derive(Debug, Clone)] pub enum EventKind { Applied, + SendPacket, Unknown, } @@ -17,6 +18,7 @@ impl From<&String> for EventKind { fn from(value: &String) -> Self { match value.as_str() { "tx/applied" => Self::Applied, + "send_packet" => Self::SendPacket, _ => Self::Unknown, } } @@ -32,7 +34,7 @@ pub struct BlockResult { #[derive(Debug, Clone)] pub struct Event { pub kind: EventKind, - pub attributes: Option, + pub attributes: Option, } #[derive(Debug, Clone, Default, Copy)] @@ -107,7 +109,7 @@ impl BatchResults { } #[derive(Debug, Clone, Default)] -pub struct TxAttributes { +pub struct TxApplied { pub code: TxEventStatusCode, pub gas: u64, pub hash: Id, @@ -116,14 +118,57 @@ pub struct TxAttributes { pub info: String, } -impl TxAttributes { +#[derive(Debug, Clone, Default)] +pub struct SendPacket { + pub source_port: String, + pub dest_port: String, + pub source_channel: String, + pub dest_channel: String, + pub timeout_timestamp: u64, + pub sequence: String, +} + +#[derive(Debug, Clone)] +pub enum TxAttributesType { + TxApplied(TxApplied), + SendPacket(SendPacket), +} + +impl TxAttributesType { pub fn deserialize( event_kind: &EventKind, attributes: &BTreeMap, ) -> Option { match event_kind { EventKind::Unknown => None, - EventKind::Applied => Some(Self { + EventKind::SendPacket => { + let source_port = + attributes.get("packet_src_port").unwrap().to_owned(); + let dest_port = + attributes.get("packet_dst_port").unwrap().to_owned(); + let source_channel = + attributes.get("packet_src_channel").unwrap().to_owned(); + let dest_channel = + attributes.get("packet_dst_channel").unwrap().to_owned(); + let sequence = + attributes.get("packet_sequence").unwrap().to_owned(); + let timeout_timestamp = attributes + .get("packet_timeout_timestamp") + .unwrap_or(&"0".to_string()) + .parse::() + .unwrap_or_default() + .to_owned(); + + Some(Self::SendPacket(SendPacket { + source_port, + dest_port, + source_channel, + dest_channel, + timeout_timestamp, + sequence, + })) + } + EventKind::Applied => Some(Self::TxApplied(TxApplied { code: attributes .get("code") .map(|code| TxEventStatusCode::from(code.as_str())) @@ -153,7 +198,7 @@ impl TxAttributes { }) .unwrap(), info: attributes.get("info").unwrap().to_owned(), - }), + })), } } } @@ -177,7 +222,7 @@ impl From for BlockResult { }, ); let attributes = - TxAttributes::deserialize(&kind, &raw_attributes); + TxAttributesType::deserialize(&kind, &raw_attributes); Event { kind, attributes } }) .collect::>(); @@ -198,7 +243,7 @@ impl From for BlockResult { }, ); let attributes = - TxAttributes::deserialize(&kind, &raw_attributes); + TxAttributesType::deserialize(&kind, &raw_attributes); Event { kind, attributes } }) .collect::>(); @@ -221,7 +266,15 @@ impl BlockResult { let exit_status = self .end_events .iter() - .filter_map(|event| event.attributes.clone()) + .filter_map(|event| { + if let Some(TxAttributesType::TxApplied(data)) = + &event.attributes + { + Some(data.clone()) + } else { + None + } + }) .find(|attributes| attributes.hash.eq(tx_hash)) .map(|attributes| attributes.clone().code) .map(TransactionExitStatus::from); @@ -229,6 +282,22 @@ impl BlockResult { exit_status.unwrap_or(TransactionExitStatus::Rejected) } + pub fn gas_used(&self, tx_hash: &Id) -> Option { + self.end_events + .iter() + .filter_map(|event| { + if let Some(TxAttributesType::TxApplied(data)) = + &event.attributes + { + Some(data.clone()) + } else { + None + } + }) + .find(|attributes| attributes.hash.eq(tx_hash)) + .map(|attributes| attributes.gas.to_string()) + } + pub fn is_inner_tx_accepted( &self, wrapper_hash: &Id, @@ -237,7 +306,15 @@ impl BlockResult { let exit_status = self .end_events .iter() - .filter_map(|event| event.attributes.clone()) + .filter_map(|event| { + if let Some(TxAttributesType::TxApplied(data)) = + &event.attributes + { + Some(data.clone()) + } else { + None + } + }) .find(|attributes| attributes.hash.eq(wrapper_hash)) .map(|attributes| attributes.batch.is_successful(inner_hash)) .map(|successful| match successful { diff --git a/shared/src/gas.rs b/shared/src/gas.rs index 70386b7a4..6399b683e 100644 --- a/shared/src/gas.rs +++ b/shared/src/gas.rs @@ -1,7 +1,115 @@ use crate::balance::Amount; +use crate::id::Id; #[derive(Clone, Debug)] pub struct GasPrice { pub token: String, pub amount: Amount, } + +#[derive(Clone, Debug)] +pub struct GasEstimation { + pub wrapper_id: Id, + pub transparent_transfer: u64, + pub shielded_transfer: u64, + pub shielding_transfer: u64, + pub ibc_unshielding_transfer: u64, + pub ibc_shielding_transfer: u64, + pub unshielding_transfer: u64, + pub ibc_msg_transfer: u64, + pub mixed_transfer: u64, + pub bond: u64, + pub redelegation: u64, + pub unbond: u64, + pub withdraw: u64, + pub claim_rewards: u64, + pub vote_proposal: u64, + pub reveal_pk: u64, + pub size: u64, + pub signatures: u64, +} + +impl GasEstimation { + pub fn new(tx_id: Id) -> Self { + Self { + wrapper_id: tx_id, + transparent_transfer: 0, + shielded_transfer: 0, + shielding_transfer: 0, + unshielding_transfer: 0, + ibc_shielding_transfer: 0, + ibc_unshielding_transfer: 0, + ibc_msg_transfer: 0, + mixed_transfer: 0, + bond: 0, + redelegation: 0, + unbond: 0, + withdraw: 0, + claim_rewards: 0, + vote_proposal: 0, + reveal_pk: 0, + size: 0, + signatures: 0, + } + } + + pub fn increase_transparent_transfer(&mut self) { + self.transparent_transfer += 1 + } + + pub fn increase_shielded_transfer(&mut self) { + self.shielded_transfer += 1 + } + + pub fn increase_shielding_transfer(&mut self) { + self.shielding_transfer += 1 + } + + pub fn increase_unshielding_transfer(&mut self) { + self.unshielding_transfer += 1 + } + + pub fn increase_mixed_transfer(&mut self) { + self.mixed_transfer += 1 + } + + pub fn increase_ibc_shielding_transfer(&mut self) { + self.ibc_shielding_transfer += 1 + } + + pub fn increase_ibc_unshielding_transfer(&mut self) { + self.ibc_unshielding_transfer += 1 + } + + pub fn increase_ibc_msg_transfer(&mut self) { + self.ibc_msg_transfer += 1 + } + + pub fn increase_bond(&mut self) { + self.bond += 1 + } + + pub fn increase_redelegation(&mut self) { + self.redelegation += 1 + } + + pub fn increase_unbond(&mut self) { + self.unbond += 1 + } + + pub fn increase_withdraw(&mut self) { + self.withdraw += 1 + } + + pub fn increase_claim_rewards(&mut self) { + self.claim_rewards += 1 + } + + pub fn increase_vote(&mut self) { + self.vote_proposal += 1 + } + + pub fn increase_reveal_pk(&mut self) { + self.reveal_pk += 1 + } +} diff --git a/shared/src/header.rs b/shared/src/header.rs index 792618895..decf3a9f9 100644 --- a/shared/src/header.rs +++ b/shared/src/header.rs @@ -4,7 +4,8 @@ use crate::block::BlockHeight; #[derive(Debug, Clone, Default)] pub struct BlockHeader { pub height: BlockHeight, - pub proposer_address: String, + pub proposer_address_tm: String, + pub proposer_address_namada: Option, pub timestamp: String, pub app_hash: Id, } diff --git a/shared/src/id.rs b/shared/src/id.rs index 556cee83f..2a3532f7b 100644 --- a/shared/src/id.rs +++ b/shared/src/id.rs @@ -17,7 +17,6 @@ pub enum Id { IbcTrace(String), Hash(String), } - impl Default for Id { fn default() -> Self { Self::Hash("".to_owned()) @@ -46,8 +45,8 @@ impl From for Id { } } -impl From for Id { - fn from(value: TendermintAppHash) -> Self { +impl From<&TendermintAppHash> for Id { + fn from(value: &TendermintAppHash) -> Self { Self::Hash(value.to_string()) } } diff --git a/shared/src/ser.rs b/shared/src/ser.rs index a776b783f..27ef6588b 100644 --- a/shared/src/ser.rs +++ b/shared/src/ser.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use namada_core::address::Address; use namada_core::masp::MaspTxId; +use namada_sdk::borsh::BorshSerializeExt; use namada_sdk::ibc::IbcMessage as NamadaIbcMessage; use namada_sdk::token::{ Account as NamadaAccount, DenominatedAmount as NamadaDenominatedAmount, @@ -10,6 +11,7 @@ use namada_sdk::token::{ }; use serde::ser::SerializeStruct; use serde::{Deserialize, Serialize}; +use subtle_encoding::hex; #[derive(Debug, Clone)] pub struct AccountsMap(pub BTreeMap); @@ -66,7 +68,7 @@ impl<'de> Deserialize<'de> for AccountsMap { } #[derive(Deserialize, Serialize, Debug, Clone)] -pub struct TransparentTransfer { +pub struct TransferData { /// Sources of this transfer pub sources: AccountsMap, /// Targets of this transfer @@ -75,13 +77,13 @@ pub struct TransparentTransfer { pub shielded_section_hash: Option, } -impl From for TransparentTransfer { +impl From for TransferData { fn from(transfer: NamadaTransfer) -> Self { let sources = AccountsMap(transfer.sources); let targets = AccountsMap(transfer.targets); let shielded_section_hash = transfer.shielded_section_hash; - TransparentTransfer { + TransferData { sources, targets, shielded_section_hash, @@ -120,10 +122,19 @@ impl Serialize for IbcMessage { state.end() } - NamadaIbcMessage::Envelope(_) => { - let state = serializer.serialize_struct("IbcEnvelope", 0)?; + NamadaIbcMessage::Envelope(data) => { + let mut state = + serializer.serialize_struct("IbcEnvelope", 1)?; + + // todo: implement this bs :( - // TODO: serialize envelope message correctly + state.serialize_field( + "data", + &String::from_utf8_lossy(&hex::encode( + data.serialize_to_vec(), + )) + .into_owned(), + )?; state.end() } diff --git a/shared/src/transaction.rs b/shared/src/transaction.rs index 79cabbcb0..91d0a653f 100644 --- a/shared/src/transaction.rs +++ b/shared/src/transaction.rs @@ -2,9 +2,9 @@ use std::collections::HashMap; use std::fmt::Display; use namada_governance::{InitProposalData, VoteProposalData}; +use namada_sdk::address::Address; use namada_sdk::borsh::BorshDeserialize; use namada_sdk::key::common::PublicKey; -use namada_sdk::masp::ShieldedTransfer; use namada_sdk::token::Transfer; use namada_sdk::uint::Uint; use namada_tx::data::pos::{ @@ -20,7 +20,8 @@ use crate::block::BlockHeight; use crate::block_result::{BlockResult, TxEventStatusCode}; use crate::checksums::Checksums; use crate::id::Id; -use crate::ser::{IbcMessage, TransparentTransfer}; +use crate::ser::{IbcMessage, TransferData}; +use crate::utils::{self, transfer_to_ibc_tx_kind}; // We wrap public key in a struct so we serialize data as object instead of // string @@ -65,11 +66,15 @@ where #[derive(Serialize, Debug, Clone)] #[serde(untagged)] pub enum TransactionKind { - TransparentTransfer(Option), - // TODO: remove once ShieldedTransfer can be serialized - #[serde(skip)] - ShieldedTransfer(Option), + TransparentTransfer(Option), + ShieldedTransfer(Option), + ShieldingTransfer(Option), + UnshieldingTransfer(Option), + MixedTransfer(Option), IbcMsgTransfer(Option>), + IbcTrasparentTransfer((Option>, TransferData)), + IbcShieldingTransfer((Option>, TransferData)), + IbcUnshieldingTransfer((Option>, TransferData)), Bond(Option), Redelegation(Option), Unbond(Option), @@ -81,6 +86,9 @@ pub enum TransactionKind { CommissionChange(Option), RevealPk(Option), BecomeValidator(Option>), + ReactivateValidator(Option
), + DeactivateValidator(Option
), + UnjailValidator(Option
), Unknown(Option), } @@ -89,15 +97,23 @@ impl TransactionKind { serde_json::to_string(&self).ok() } - pub fn from(id: &str, tx_kind_name: &str, data: &[u8]) -> Self { + pub fn from( + id: &str, + tx_kind_name: &str, + data: &[u8], + masp_address: &Address, + ) -> Self { match tx_kind_name { "tx_transfer" => { - let data = if let Ok(data) = Transfer::try_from_slice(data) { - Some(TransparentTransfer::from(data)) + if let Ok(transfer) = Transfer::try_from_slice(data) { + utils::transfer_to_tx_kind(transfer, masp_address) } else { - None - }; - TransactionKind::TransparentTransfer(data) + TransactionKind::Unknown(Some(UnknownTransaction { + id: Some(id.to_string()), + name: Some(tx_kind_name.to_string()), + data: Some(data.to_vec()), + })) + } } "tx_bond" => { let data = if let Ok(data) = Bond::try_from_slice(data) { @@ -185,16 +201,63 @@ impl TransactionKind { }; TransactionKind::RevealPk(data) } + "tx_deactivate_validator" => { + let data = if let Ok(data) = Address::try_from_slice(data) { + Some(data) + } else { + None + }; + TransactionKind::DeactivateValidator(data) + } + "tx_reactivate_validator" => { + let data = if let Ok(data) = Address::try_from_slice(data) { + Some(data) + } else { + None + }; + TransactionKind::ReactivateValidator(data) + } "tx_ibc" => { - let data = if let Ok(data) = + if let Ok(ibc_data) = namada_ibc::decode_message::(data) { - Some(data) + match ibc_data.clone() { + namada_ibc::IbcMessage::Envelope(_msg_envelope) => { + TransactionKind::IbcMsgTransfer(Some(IbcMessage( + ibc_data, + ))) + } + namada_ibc::IbcMessage::Transfer(transfer) => { + if let Some(data) = transfer.transfer { + utils::transfer_to_tx_kind(data, masp_address) + } else { + TransactionKind::IbcMsgTransfer(None) + } + } + namada_ibc::IbcMessage::NftTransfer(transfer) => { + if let Some(data) = transfer.transfer { + transfer_to_ibc_tx_kind( + data, + masp_address, + ibc_data, + ) + } else { + TransactionKind::IbcMsgTransfer(None) + } + } + } } else { tracing::warn!("Cannot deserialize IBC transfer"); + TransactionKind::IbcMsgTransfer(None) + } + } + "tx_unjail_validator" => { + let data = if let Ok(data) = Address::try_from_slice(data) { + Some(data) + } else { None }; - TransactionKind::IbcMsgTransfer(data.map(IbcMessage)) + TransactionKind::UnjailValidator(data) } "tx_become_validator" => { let data = @@ -260,6 +323,8 @@ pub struct WrapperTransaction { pub atomic: bool, pub block_height: BlockHeight, pub exit_code: TransactionExitStatus, + pub total_signatures: u64, + pub size: u64, } #[derive(Debug, Clone)] @@ -278,11 +343,26 @@ impl InnerTransaction { pub fn get_section_data_by_id(&self, section_id: Id) -> Option> { self.extra_sections.get(§ion_id).cloned() } + + pub fn was_successful(&self) -> bool { + self.exit_code == TransactionExitStatus::Applied + } + + pub fn is_ibc(&self) -> bool { + matches!( + self.kind, + TransactionKind::IbcMsgTransfer(_) + | TransactionKind::IbcTrasparentTransfer(_) + | TransactionKind::IbcUnshieldingTransfer(_) + | TransactionKind::IbcShieldingTransfer(_) + ) + } } #[derive(Debug, Clone)] pub struct Fee { pub gas: String, + pub gas_used: Option, pub amount_per_gas_unit: String, pub gas_payer: Id, pub gas_token: Id, @@ -295,18 +375,30 @@ impl Transaction { block_height: BlockHeight, checksums: Checksums, block_results: &BlockResult, + masp_address: &Address, ) -> Result<(WrapperTransaction, Vec), String> { let transaction = Tx::try_from(raw_tx_bytes).map_err(|e| e.to_string())?; + let total_signatures = transaction + .clone() + .sections + .iter() + .filter(|section| section.signature().is_some()) + .count() as u64; + let tx_size = raw_tx_bytes.len() as u64; match transaction.header().tx_type { TxType::Wrapper(wrapper) => { let wrapper_tx_id = Id::from(transaction.header_hash()); let wrapper_tx_status = block_results.is_wrapper_tx_applied(&wrapper_tx_id); + let gas_used = block_results + .gas_used(&wrapper_tx_id) + .map(|gas| gas.parse::().unwrap()); let fee = Fee { gas: Uint::from(wrapper.gas_limit).to_string(), + gas_used, amount_per_gas_unit: wrapper .fee .amount_per_gas_unit @@ -324,6 +416,8 @@ impl Transaction { atomic, block_height, exit_code: wrapper_tx_status, + total_signatures, + size: tx_size, }; let mut inner_txs = vec![]; @@ -362,7 +456,12 @@ impl Transaction { if let Some(tx_kind_name) = checksums.get_name_by_id(&id) { - TransactionKind::from(&id, &tx_kind_name, &tx_data) + TransactionKind::from( + &id, + &tx_kind_name, + &tx_data, + masp_address, + ) } else { TransactionKind::Unknown(Some(UnknownTransaction { id: Some(id), @@ -434,3 +533,107 @@ impl Transaction { self.extra_sections.get(§ion_id).cloned() } } + +#[derive(Debug, Clone)] +pub struct IbcSequence { + pub sequence_number: String, + pub source_port: String, + pub dest_port: String, + pub source_channel: String, + pub dest_channel: String, + pub timeout: u64, + pub tx_id: Id, +} + +impl IbcSequence { + pub fn id(&self) -> String { + format!( + "{}/{}/{}/{}/{}", + self.dest_port, + self.dest_channel, + self.source_port, + self.source_channel, + self.sequence_number + ) + } +} + +#[derive(Debug, Clone)] +pub enum IbcAckStatus { + Success, + Fail, + Timeout, + Unknown, +} + +#[derive(Debug, Clone)] +pub struct IbcAck { + pub sequence_number: String, + pub source_port: String, + pub dest_port: String, + pub source_channel: String, + pub dest_channel: String, + pub status: IbcAckStatus, +} + +impl IbcAck { + pub fn id_source(&self) -> String { + format!( + "{}/{}/{}", + self.source_port, self.source_channel, self.sequence_number + ) + } + + pub fn id_dest(&self) -> String { + format!( + "{}/{}/{}", + self.dest_port, self.dest_channel, self.sequence_number + ) + } + + pub fn id(&self) -> String { + format!( + "{}/{}/{}/{}/{}", + self.dest_port, + self.dest_channel, + self.source_port, + self.source_channel, + self.sequence_number + ) + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum TransactionHistoryKind { + Received, + Sent, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct TransactionTarget { + pub inner_tx: Id, + pub address: String, + pub kind: TransactionHistoryKind, +} + +impl TransactionTarget { + pub fn new( + inner_tx: Id, + address: String, + kind: TransactionHistoryKind, + ) -> Self { + Self { + inner_tx, + address, + kind, + } + } + + pub fn sent(inner_tx: Id, address: String) -> Self { + Self::new(inner_tx, address, TransactionHistoryKind::Sent) + } + + pub fn received(inner_tx: Id, address: String) -> Self { + Self::new(inner_tx, address, TransactionHistoryKind::Received) + } +} diff --git a/shared/src/utils.rs b/shared/src/utils.rs index 4cd834ae5..5876113dc 100644 --- a/shared/src/utils.rs +++ b/shared/src/utils.rs @@ -1,5 +1,11 @@ +use namada_ibc::IbcMessage; +use namada_sdk::address::Address; +use namada_sdk::token::Transfer; + use crate::id::Id; +use crate::ser; use crate::token::Token; +use crate::transaction::TransactionKind; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct BalanceChange { @@ -25,3 +31,93 @@ pub struct DelegationPair { pub validator_address: Id, pub delegator_address: Id, } + +pub fn transfer_to_tx_kind( + data: Transfer, + masp_address: &Address, +) -> TransactionKind { + let has_shielded_section = data.shielded_section_hash.is_some(); + + let (all_sources_are_masp, any_sources_are_masp) = data + .sources + .iter() + .fold((true, false), |(all, any), (acc, _)| { + let is_masp = acc.owner.eq(masp_address); + (all && is_masp, any || is_masp) + }); + + let (all_targets_are_masp, any_targets_are_masp) = data + .targets + .iter() + .fold((true, false), |(all, any), (acc, _)| { + let is_masp = acc.owner.eq(masp_address); + (all && is_masp, any || is_masp) + }); + + match ( + all_sources_are_masp, + any_sources_are_masp, + all_targets_are_masp, + any_targets_are_masp, + has_shielded_section, + ) { + (true, _, true, _, true) => { + TransactionKind::ShieldedTransfer(Some(data.into())) + } + (true, _, _, false, true) => { + TransactionKind::UnshieldingTransfer(Some(data.into())) + } + (false, _, true, _, true) => { + TransactionKind::ShieldingTransfer(Some(data.into())) + } + (false, _, false, _, false) => { + TransactionKind::TransparentTransfer(Some(data.into())) + } + _ => TransactionKind::MixedTransfer(Some(data.into())), + } +} + +pub fn transfer_to_ibc_tx_kind( + data: Transfer, + masp_address: &Address, + ibc_data: IbcMessage, +) -> TransactionKind { + let has_shielded_section = data.shielded_section_hash.is_some(); + + let (all_sources_are_masp, any_sources_are_masp) = data + .sources + .iter() + .fold((true, false), |(all, any), (acc, _)| { + let is_masp = acc.owner.eq(masp_address); + (all && is_masp, any || is_masp) + }); + + let (all_targets_are_masp, any_targets_are_masp) = data + .targets + .iter() + .fold((true, false), |(all, any), (acc, _)| { + let is_masp = acc.owner.eq(masp_address); + (all && is_masp, any || is_masp) + }); + + match ( + all_sources_are_masp, + any_sources_are_masp, + all_targets_are_masp, + any_targets_are_masp, + has_shielded_section, + ) { + (true, _, _, false, true) => TransactionKind::IbcUnshieldingTransfer(( + Some(ser::IbcMessage(ibc_data)), + data.into(), + )), + (false, _, true, _, true) => TransactionKind::IbcShieldingTransfer(( + Some(ser::IbcMessage(ibc_data)), + data.into(), + )), + (false, _, false, _, false) => TransactionKind::IbcTrasparentTransfer( + (Some(ser::IbcMessage(ibc_data)), data.into()), + ), + _ => TransactionKind::MixedTransfer(Some(data.into())), + } +} diff --git a/shared/src/validator.rs b/shared/src/validator.rs index eead4e117..da5e6a7d9 100644 --- a/shared/src/validator.rs +++ b/shared/src/validator.rs @@ -18,6 +18,9 @@ pub enum ValidatorState { BelowThreshold, Inactive, Jailed, + Deactivating, + Reactivating, + Unjailing, Unknown, } @@ -43,6 +46,19 @@ pub struct ValidatorSet { pub epoch: Epoch, } +impl ValidatorSet { + pub fn union(&self, validator_set: &ValidatorSet) -> Self { + ValidatorSet { + validators: self + .validators + .union(&validator_set.validators) + .cloned() + .collect::>(), + epoch: self.epoch, + } + } +} + #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct Validator { pub address: Id, @@ -70,6 +86,12 @@ pub struct ValidatorMetadataChange { pub avatar: Option, } +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct ValidatorStateChange { + pub address: Id, + pub state: ValidatorState, +} + impl Validator { pub fn fake() -> Self { let address = diff --git a/shared/src/vote.rs b/shared/src/vote.rs index 92b67a32b..421823773 100644 --- a/shared/src/vote.rs +++ b/shared/src/vote.rs @@ -9,6 +9,7 @@ pub enum ProposalVoteKind { Nay, Yay, Abstain, + Unknown, } impl From for ProposalVoteKind { diff --git a/swagger.yml b/swagger.yml index f56f1c739..478570cbe 100644 --- a/swagger.yml +++ b/swagger.yml @@ -1,19 +1,30 @@ -openapi: '3.0.2' +openapi: "3.0.2" info: title: Namada Interface Indexer REST Api - version: '0.1' + version: "0.1" description: "Set of API to interact with a namada indexer." - contact: + contact: email: hello@heliax.dev url: https://github.com/anoma/namada-indexer servers: - url: http://localhost:5001 + - url: https://namada-rpc.mandragora.io + - url: https://indexer.namada.tududes.com:443 paths: /health: get: responses: - '200': + "200": description: Health check + content: + application/json: + schema: + type: object + properties: + commit: + type: string + version: + type: string /api/v1/pos/validator: get: summary: Get all validators, paginated @@ -29,7 +40,7 @@ paths: schema: type: array items: - $ref: '#/components/schemas/ValidatorStatus' + $ref: "#/components/schemas/ValidatorStatus" description: Validator status parameter - in: query name: sortField @@ -44,7 +55,7 @@ paths: enum: [asc, desc] description: Sort order - ignored if sortField is not provided responses: - '200': + "200": description: A list of validator. content: application/json: @@ -55,9 +66,9 @@ paths: results: type: array items: - $ref: '#/components/schemas/Validator' + $ref: "#/components/schemas/Validator" pagination: - $ref: '#/components/schemas/Pagination' + $ref: "#/components/schemas/Pagination" /api/v1/pos/validator/all: get: summary: Get all validators, non paginated @@ -67,17 +78,17 @@ paths: schema: type: array items: - $ref: '#/components/schemas/ValidatorStatus' + $ref: "#/components/schemas/ValidatorStatus" description: Validator status parameter responses: - '200': + "200": description: A list of validator. content: application/json: schema: type: array items: - $ref: '#/components/schemas/Validator' + $ref: "#/components/schemas/Validator" /api/v1/pos/reward/{address}: get: summary: Get all the rewards for an address @@ -89,14 +100,14 @@ paths: required: true description: The delegator address responses: - '200': + "200": description: A list of rewards. content: application/json: schema: type: array items: - $ref: '#/components/schemas/Reward' + $ref: "#/components/schemas/Reward" /api/v1/pos/bond/{address}: get: summary: Get all the bonds for an address @@ -113,8 +124,14 @@ paths: type: integer minimum: 1 description: Pagination parameter + - in: query + name: activeAt + schema: + type: integer + minimum: 0 + description: Get all bonds that are active at this epoch responses: - '200': + "200": description: A list of bonds. content: application/json: @@ -125,9 +142,9 @@ paths: results: type: array items: - $ref: '#/components/schemas/Bond' + $ref: "#/components/schemas/Bond" pagination: - $ref: '#/components/schemas/Pagination' + $ref: "#/components/schemas/Pagination" /api/v1/pos/merged-bonds/{address}: get: summary: Get all the bonds for an address, with merged amounts, regardless of the status @@ -145,7 +162,7 @@ paths: minimum: 1 description: Pagination parameter responses: - '200': + "200": description: A list of bonds. content: application/json: @@ -156,9 +173,9 @@ paths: results: type: array items: - $ref: '#/components/schemas/MergedBond' + $ref: "#/components/schemas/MergedBond" pagination: - $ref: '#/components/schemas/Pagination' + $ref: "#/components/schemas/Pagination" /api/v1/pos/unbond/{address}: get: summary: Get all the unbonds for an an address @@ -175,8 +192,14 @@ paths: type: integer minimum: 1 description: Pagination parameter + - in: query + name: activeAt + schema: + type: integer + minimum: 0 + description: Get all unbonds that are active at this epoch( < ) responses: - '200': + "200": description: A list of unbonds. content: application/json: @@ -187,9 +210,9 @@ paths: results: type: array items: - $ref: '#/components/schemas/Unbond' + $ref: "#/components/schemas/Unbond" pagination: - $ref: '#/components/schemas/Pagination' + $ref: "#/components/schemas/Pagination" /api/v1/pos/merged-unbonds/{address}: get: summary: Get all the unbonds for an an address with merged withdraw amounts @@ -207,7 +230,7 @@ paths: minimum: 1 description: Pagination parameter responses: - '200': + "200": description: A list of unbonds. content: application/json: @@ -218,9 +241,9 @@ paths: results: type: array items: - $ref: '#/components/schemas/Unbond' + $ref: "#/components/schemas/Unbond" pagination: - $ref: '#/components/schemas/Pagination' + $ref: "#/components/schemas/Pagination" /api/v1/pos/withdraw/{address}: get: summary: Get all the withdraws for an address at a specific epoch @@ -243,7 +266,7 @@ paths: minimum: 1 description: Pagination parameter responses: - '200': + "200": description: A list of withdraws. content: application/json: @@ -254,19 +277,19 @@ paths: results: type: array items: - $ref: '#/components/schemas/Withdraw' + $ref: "#/components/schemas/Withdraw" pagination: - $ref: '#/components/schemas/Pagination' + $ref: "#/components/schemas/Pagination" /api/v1/pos/voting-power: get: summary: Get the total voting power responses: - '200': + "200": description: The total voting power. content: application/json: schema: - $ref: '#/components/schemas/VotingPower' + $ref: "#/components/schemas/VotingPower" /api/v1/gov/proposal: get: summary: Get a list of governance proposals @@ -276,7 +299,7 @@ paths: schema: type: integer minimum: 1 - description: Pagination parameter + description: Pagination parameter - in: query name: status schema: @@ -295,7 +318,7 @@ paths: type: string description: The status of the proposal responses: - '200': + "200": description: A list of governance proposal. content: application/json: @@ -306,9 +329,9 @@ paths: results: type: array items: - $ref: '#/components/schemas/Proposal' + $ref: "#/components/schemas/Proposal" pagination: - $ref: '#/components/schemas/Pagination' + $ref: "#/components/schemas/Pagination" /api/v1/gov/proposal/all: get: summary: Get a list of all governance proposals @@ -331,14 +354,14 @@ paths: type: string description: The status of the proposal responses: - '200': + "200": description: A list of governance proposals. content: application/json: schema: type: array items: - $ref: '#/components/schemas/Proposal' + $ref: "#/components/schemas/Proposal" /api/v1/gov/proposal/{id}: get: summary: Get a governance proposal by proposal id @@ -351,12 +374,12 @@ paths: required: true description: Proposal id responses: - '200': + "200": description: A Governance proposal. content: application/json: schema: - $ref: '#/components/schemas/Proposal' + $ref: "#/components/schemas/Proposal" /api/v1/gov/proposal/{id}/data: get: summary: Get a governance proposal data by proposal id @@ -383,7 +406,7 @@ paths: required: true description: Proposal id responses: - '200': + "200": description: A list of votes for a governance proposal. content: application/json: @@ -394,9 +417,9 @@ paths: results: type: array items: - $ref: '#/components/schemas/Vote' + $ref: "#/components/schemas/Vote" pagination: - $ref: '#/components/schemas/Pagination' + $ref: "#/components/schemas/Pagination" /api/v1/gov/proposal/{id}/votes/{address}: get: summary: Get all the votes for a governance proposal from an address @@ -415,14 +438,14 @@ paths: required: true description: The voter address responses: - '200': + "200": description: A list of votes. content: application/json: schema: type: array items: - $ref: '#/components/schemas/Vote' + $ref: "#/components/schemas/Vote" /api/v1/gov/voter/{address}/votes: get: summary: Get all the votes from a voter @@ -434,14 +457,14 @@ paths: required: true description: The voter address responses: - '200': + "200": description: A list of votes. content: application/json: schema: type: array items: - $ref: '#/components/schemas/Vote' + $ref: "#/components/schemas/Vote" /api/v1/account/{address}: get: summary: Get the all the tokens balances of an address @@ -453,14 +476,14 @@ paths: required: true description: The address account responses: - '200': + "200": description: A List of balances. content: application/json: schema: type: array items: - $ref: '#/components/schemas/Balance' + $ref: "#/components/schemas/Balance" /api/v1/revealed-public-key/{address}: get: summary: Get revealed public key for an address if exists @@ -472,32 +495,32 @@ paths: required: true description: The address account responses: - '200': + "200": description: Revealed public key. content: application/json: schema: - $ref: '#/components/schemas/RevealedPk' + $ref: "#/components/schemas/RevealedPk" /api/v1/gas: get: summary: Get the gas limit per tx kind. responses: - '200': + "200": description: Gas limit table content: application/json: schema: - $ref: '#/components/schemas/GasLimitTable' + $ref: "#/components/schemas/GasLimitTable" /api/v1/gas-price: get: summary: Get all the gas prices responses: - '200': + "200": description: Gas price table content: application/json: schema: - $ref: '#/components/schemas/GasPriceTable' + $ref: "#/components/schemas/GasPriceTable" /api/v1/gas-price/{token}: get: parameters: @@ -506,20 +529,114 @@ paths: schema: type: string required: true - description: The gas token. + description: The gas token. summary: Get the gas price per token type responses: - '200': + "200": description: Gas price table content: application/json: schema: - $ref: '#/components/schemas/GasPriceTable' + $ref: "#/components/schemas/GasPriceTable" + /api/v1/gas/estimate: + get: + summary: Get an estimate for a transaction + parameters: + - in: query + name: bond + schema: + type: integer + minimum: 1 + maximum: 100 + - in: query + name: claim_rewards + schema: + type: integer + minimum: 1 + maximum: 100 + - in: query + name: unbond + schema: + type: integer + minimum: 1 + maximum: 100 + - in: query + name: transparent_transfer + schema: + type: integer + minimum: 1 + maximum: 100 + - in: query + name: shielded_transfer + schema: + type: integer + minimum: 1 + maximum: 100 + - in: query + name: shielding_transfer + schema: + type: integer + minimum: 1 + maximum: 100 + - in: query + name: unshielding_transfer + schema: + type: integer + minimum: 1 + maximum: 100 + - in: query + name: vote + schema: + type: integer + minimum: 1 + maximum: 100 + - in: query + name: ibc + schema: + type: integer + minimum: 1 + maximum: 100 + - in: query + name: withdraw + schema: + type: integer + minimum: 1 + maximum: 100 + - in: query + name: reveal_pk + schema: + type: integer + minimum: 1 + maximum: 100 + - in: query + name: redelegate + schema: + type: integer + minimum: 1 + maximum: 100 + - in: query + name: signatures + schema: + type: integer + minimum: 1 + maximum: 20 + - in: query + name: tx_size + schema: + type: integer + minimum: 1 + responses: + "200": + description: A gas estimate. + content: + application/json: + schema: + $ref: "#/components/schemas/GasEstimate" /api/v1/chain/token: get: summary: Get chain tokens responses: - '200': + "200": description: Chain tokens content: application/json: @@ -527,13 +644,13 @@ paths: type: array items: oneOf: - - $ref: '#/components/schemas/NativeToken' - - $ref: '#/components/schemas/IbcToken' + - $ref: "#/components/schemas/NativeToken" + - $ref: "#/components/schemas/IbcToken" examples: native: summary: An example of native token - value: - - address: tnam1qqg0jc68dx69d7klxg6n39qtcc6qnhc93senzthk] + value: + - address: tnam1qqg0jc68dx69d7klxg6n39qtcc6qnhc93senzthk] ibc: summary: An example of ibc token value: @@ -543,27 +660,27 @@ paths: get: summary: Get chain parameters responses: - '200': + "200": description: Chain parameters content: application/json: schema: - $ref: '#/components/schemas/Parameters' + $ref: "#/components/schemas/Parameters" /api/v1/chain/rpc-url: get: summary: Get rpc url that indexer connects to responses: - '200': + "200": description: Rpc url content: application/json: schema: - $ref: '#/components/schemas/RpcUrl' + $ref: "#/components/schemas/RpcUrl" /api/v1/chain/block/latest: get: summary: Get the latest block processed by the chain crawler responses: - '200': + "200": description: Block height content: application/json: @@ -577,7 +694,7 @@ paths: get: summary: Get the latest epoch processed by the chain crawler responses: - '200': + "200": description: Epoch content: application/json: @@ -596,14 +713,14 @@ paths: schema: type: string required: true - description: Tx id hash + description: Tx id hash responses: - '200': + "200": description: Wrapper transaction content: application/json: schema: - $ref: '#/components/schemas/WrapperTransaction' + $ref: "#/components/schemas/WrapperTransaction" /api/v1/chain/inner/{tx_id}: get: summary: Get the inner transaction by hash @@ -613,14 +730,69 @@ paths: schema: type: string required: true - description: Tx id hash + description: Tx id hash responses: - '200': + "200": description: Inner transaction content: application/json: schema: - $ref: '#/components/schemas/InnerTransaction' + $ref: "#/components/schemas/InnerTransaction" + /api/v1/ibc/{tx_id}/status: + get: + summary: Get the status of an IBC transfer by tx id + parameters: + - in: path + name: tx_id + schema: + type: string + required: true + description: Tx id hash + responses: + "200": + description: Status of the IBC transfer + content: + application/json: + schema: + type: object + properties: + status: + type: string + enum: [unknown, timeout, success, fail] + /api/v1/block/height/{value}: + get: + summary: Get the block by height + parameters: + - in: path + name: value + schema: + type: number + required: true + description: Block height + responses: + "200": + description: Block info + content: + application/json: + schema: + $ref: "#/components/schemas/Block" + /api/v1/block/timestamp/{value}: + get: + summary: Get the block by timestamp + parameters: + - in: path + name: value + schema: + type: number + required: true + description: Block timestamp + responses: + "200": + description: Block info + content: + application/json: + schema: + $ref: "#/components/schemas/Block" /api/v1/crawlers/timestamps: get: summary: Get timestamps of the last activity of the crawlers @@ -634,7 +806,7 @@ paths: enum: [chain, governance, parameters, pos, rewards, transactions] description: The crawler names responses: - '200': + "200": description: Inner transaction content: application/json: @@ -646,17 +818,61 @@ paths: properties: name: type: string - enum: [chain, governance, parameters, pos, rewards, transactions] + enum: + [ + chain, + governance, + parameters, + pos, + rewards, + transactions, + ] timestamp: type: number - last_processed_block: + last_processed_block_height: type: number - + /api/v1/chain/history: + get: + summary: Get a paginated list of transaction for a list of addresses + parameters: + - in: query + name: addresses + schema: + type: array + items: + type: string + minItems: 1 + maxItems: 10 + description: The list of address. Must contain at least 1 element + responses: + "200": + description: Pagined historic transaction list. + content: + application/json: + schema: + type: object + required: [results, pagination] + properties: + results: + type: array + items: + $ref: "#/components/schemas/TransactionHistory" + pagination: + $ref: "#/components/schemas/Pagination" components: schemas: Validator: type: object - required: [validatorId, address, name, votingPower, maxCommission, commission, state] + required: + [ + validatorId, + address, + name, + votingPower, + maxCommission, + commission, + state, + ] properties: validatorId: type: string @@ -683,13 +899,42 @@ components: avatar: type: string state: - $ref: '#/components/schemas/ValidatorStatus' + $ref: "#/components/schemas/ValidatorStatus" ValidatorStatus: type: string - enum: [consensus, belowCapacity, belowThreshold, inactive, jailed, unknown] + enum: + [ + consensus, + belowCapacity, + belowThreshold, + inactive, + jailed, + unknown, + unjailing, + deactivating, + reactivating, + ] Proposal: type: object - required: [id, content, type, author, startEpoch, endEpoch, activationEpoch, startTime, endTime, currentTime, activationTime, status, yayVotes, nayVotes, abstainVotes, tallyType] + required: + [ + id, + content, + type, + author, + startEpoch, + endEpoch, + activationEpoch, + startTime, + endTime, + currentTime, + activationTime, + status, + yayVotes, + nayVotes, + abstainVotes, + tallyType, + ] properties: id: type: string @@ -736,14 +981,14 @@ components: type: string vote: type: string - enum: [yay, nay, abstain] + enum: [yay, nay, abstain, unknown] voterAddress: type: string Reward: type: object properties: validator: - $ref: '#/components/schemas/Validator' + $ref: "#/components/schemas/Validator" minDenomAmount: type: string format: float @@ -753,7 +998,7 @@ components: required: [validator, minDenomAmount, status, startEpoch] properties: validator: - $ref: '#/components/schemas/Validator' + $ref: "#/components/schemas/Validator" minDenomAmount: type: string status: @@ -766,15 +1011,16 @@ components: required: [validator, minDenomAmount] properties: validator: - $ref: '#/components/schemas/Validator' + $ref: "#/components/schemas/Validator" minDenomAmount: type: string Unbond: type: object - required: [validator, minDenomAmount, withdrawEpoch, withdrawTime, canWithdraw] + required: + [validator, minDenomAmount, withdrawEpoch, withdrawTime, canWithdraw] properties: validator: - $ref: '#/components/schemas/Validator' + $ref: "#/components/schemas/Validator" minDenomAmount: type: string withdrawEpoch: @@ -788,7 +1034,7 @@ components: required: [minDenomAmount, withdrawEpoch] properties: validator: - $ref: '#/components/schemas/Validator' + $ref: "#/components/schemas/Validator" minDenomAmount: type: string format: float @@ -839,7 +1085,25 @@ components: type: number txKind: type: string - enum: [transparentTransfer, shieldedTransfer, shieldingTransfer, unshieldingTransfer, bond, redelegation, unbond, withdraw, claimRewards, voteProposal, initProposal, changeMetadata, changeCommission, revealPk, ibcMsgTransfer, unknown] + enum: + [ + transparentTransfer, + shieldedTransfer, + shieldingTransfer, + unshieldingTransfer, + bond, + redelegation, + unbond, + withdraw, + claimRewards, + voteProposal, + initProposal, + changeMetadata, + changeCommission, + revealPk, + ibcMsgTransfer, + unknown, + ] GasPriceTable: type: array items: @@ -850,6 +1114,18 @@ components: type: string minDenomAmount: type: string + GasEstimate: + type: object + required: [min, max, avg, totalEstimates] + properties: + min: + type: number + max: + type: number + avg: + type: number + totalEstimates: + type: number NativeToken: type: object required: [address] @@ -866,7 +1142,22 @@ components: type: string Parameters: type: object - required: [unbondingLength, pipelineLength, epochsPerYear, apr, nativeTokenAddress, chainId, genesisTime, minDuration, minNumOfBlocks, maxBlockTime, checksums, epochSwitchBlocksDelay, cubicSlashingWindowLength] + required: + [ + unbondingLength, + pipelineLength, + epochsPerYear, + apr, + nativeTokenAddress, + chainId, + genesisTime, + minDuration, + minNumOfBlocks, + maxBlockTime, + checksums, + epochSwitchBlocksDelay, + cubicSlashingWindowLength, + ] properties: unbondingLength: type: string @@ -904,7 +1195,17 @@ components: type: string WrapperTransaction: type: object - required: [txId, feePayer, feeToken, gasLimit, blockHeight, innerTransactions, exitCode, atomic] + required: + [ + txId, + feePayer, + feeToken, + gasLimit, + blockHeight, + innerTransactions, + exitCode, + atomic, + ] properties: txId: type: string @@ -913,7 +1214,12 @@ components: feeToken: type: string gasLimit: - type: string + type: number + amountPerGasUnit: + type: number + format: float + gasUsed: + type: number blockHeight: type: string innerTransactions: @@ -926,7 +1232,27 @@ components: type: string kind: type: string - enum: ["transparentTransfer", "shieldedTransfer", "shieldingTransfer", "unshieldingTransfer", "bond", "redelegation", "unbond", "withdraw", "claimRewards", "voteProposal", "initProposal", "changeMetadata", "changeCommission", "revealPk", "unknown"] + enum: + [ + "transparentTransfer", + "shieldedTransfer", + "shieldingTransfer", + "unshieldingTransfer", + "bond", + "redelegation", + "unbond", + "withdraw", + "claimRewards", + "voteProposal", + "initProposal", + "changeMetadata", + "changeCommission", + "revealPk", + "deactivateValidator", + "reactivateValidator", + "unjailValidator", + "unknown", + ] exitCode: type: string enum: [applied, rejected] @@ -949,7 +1275,24 @@ components: type: string kind: type: string - enum: ["transparentTransfer", "shieldedTransfer", "shieldingTransfer", "unshieldingTransfer", "bond", "redelegation", "unbond", "withdraw", "claimRewards", "voteProposal", "initProposal", "changeMetadata", "changeCommission", "revealPk", "unknown"] + enum: + [ + "transparentTransfer", + "shieldedTransfer", + "shieldingTransfer", + "unshieldingTransfer", + "bond", + "redelegation", + "unbond", + "withdraw", + "claimRewards", + "voteProposal", + "initProposal", + "changeMetadata", + "changeCommission", + "revealPk", + "unknown", + ] exitCode: type: string enum: [applied, rejected] @@ -957,3 +1300,71 @@ components: type: string data: type: string + Block: + type: object + required: [height] + properties: + height: + type: string + hash: + type: string + appHash: + type: string + timestamp: + type: string + proposer: + type: string + transactions: + type: array + items: + type: string + parentHash: + type: string + epoch: + type: string + TransactionHistory: + type: object + required: [txId, kind, wrapperId, exitCode] + properties: + tx: + type: object + required: [txId, kind, wrapperId, exitCode] + properties: + txId: + type: string + wrapperId: + type: string + kind: + type: string + enum: + [ + "transparentTransfer", + "shieldedTransfer", + "shieldingTransfer", + "unshieldingTransfer", + "bond", + "redelegation", + "unbond", + "withdraw", + "claimRewards", + "voteProposal", + "initProposal", + "changeMetadata", + "changeCommission", + "revealPk", + "unknown", + ] + exitCode: + type: string + enum: [applied, rejected] + memo: + type: string + data: + type: string + target: + type: string + kind: + type: string + enum: [received, sent] + block_height: + type: string diff --git a/transactions/Cargo.toml b/transactions/Cargo.toml index 57ff1f28d..7967dc86b 100644 --- a/transactions/Cargo.toml +++ b/transactions/Cargo.toml @@ -27,6 +27,8 @@ deadpool-diesel.workspace = true diesel.workspace = true diesel_migrations.workspace = true orm.workspace = true +clap-verbosity-flag.workspace = true +serde_json.workspace = true [build-dependencies] vergen = { version = "8.0.0", features = ["build", "git", "gitcl"] } diff --git a/transactions/src/main.rs b/transactions/src/main.rs index f6bbfc65e..adb125816 100644 --- a/transactions/src/main.rs +++ b/transactions/src/main.rs @@ -12,13 +12,16 @@ use shared::checksums::Checksums; use shared::crawler::crawl; use shared::crawler_state::BlockCrawlerState; use shared::error::{AsDbError, AsRpcError, ContextDbInteractError, MainError}; +use shared::id::Id; use tendermint_rpc::HttpClient; use transactions::app_state::AppState; use transactions::config::AppConfig; -use transactions::repository::transactions as transaction_repo; +use transactions::repository::{ + block as block_repo, transactions as transaction_repo, +}; use transactions::services::{ db as db_service, namada as namada_service, - tendermint as tendermint_service, + tendermint as tendermint_service, tx as tx_service, }; #[tokio::main] @@ -114,22 +117,45 @@ async fn crawling_fn( .into_rpc_error()?; let block_results = BlockResult::from(tm_block_results_response); + let proposer_address_namada = namada_service::get_validator_namada_address( + &client, + &Id::from(&tm_block_response.block.header.proposer_address), + ) + .await + .into_rpc_error()?; + + tracing::debug!( + block = block_height, + tm_address = tm_block_response.block.header.proposer_address.to_string(), + namada_address = ?proposer_address_namada, + "Got block proposer address" + ); + let block = Block::from( - tm_block_response.clone(), + &tm_block_response, &block_results, + &proposer_address_namada, checksums, - 1_u32, + 1_u32, // placeholder, we dont need the epoch here block_height, ); let inner_txs = block.inner_txs(); let wrapper_txs = block.wrapper_txs(); + let transaction_sources = block.sources(); + let gas_estimates = tx_service::get_gas_estimates(&inner_txs, &wrapper_txs); - tracing::debug!( - block = block_height, - txs = inner_txs.len(), - "Deserialized {} txs...", - wrapper_txs.len() + inner_txs.len() + let ibc_sequence_packet = + tx_service::get_ibc_packets(&block_results, &inner_txs); + let ibc_ack_packet = tx_service::get_ibc_ack_packet(&inner_txs); + + tracing::info!( + "Deserialized {} wrappers, {} inners, {} ibc sequence numbers and {} \ + ibc acks events...", + wrapper_txs.len(), + inner_txs.len(), + ibc_sequence_packet.len(), + ibc_ack_packet.len() ); // Because transaction crawler starts from block 1 we read timestamp from @@ -151,6 +177,11 @@ async fn crawling_fn( conn.build_transaction() .read_write() .run(|transaction_conn| { + block_repo::upsert_block( + transaction_conn, + block, + tm_block_response, + )?; transaction_repo::insert_wrapper_transactions( transaction_conn, wrapper_txs, @@ -164,6 +195,26 @@ async fn crawling_fn( crawler_state, )?; + transaction_repo::insert_ibc_sequence( + transaction_conn, + ibc_sequence_packet, + )?; + + transaction_repo::update_ibc_sequence( + transaction_conn, + ibc_ack_packet, + )?; + + transaction_repo::insert_transactions_history( + transaction_conn, + transaction_sources, + )?; + + transaction_repo::insert_gas_estimates( + transaction_conn, + gas_estimates, + )?; + anyhow::Ok(()) }) }) diff --git a/transactions/src/repository/block.rs b/transactions/src/repository/block.rs new file mode 100644 index 000000000..5420dd939 --- /dev/null +++ b/transactions/src/repository/block.rs @@ -0,0 +1,32 @@ +use anyhow::Context; +use diesel::upsert::excluded; +use diesel::{ExpressionMethods, PgConnection, RunQueryDsl}; +use orm::blocks::BlockInsertDb; +use orm::schema::blocks; +use shared::block::Block; +use tendermint_rpc::endpoint::block::Response as TendermintBlockResponse; + +pub fn upsert_block( + transaction_conn: &mut PgConnection, + block: Block, + tm_block_response: TendermintBlockResponse, +) -> anyhow::Result<()> { + diesel::insert_into(blocks::table) + .values::<&BlockInsertDb>(&BlockInsertDb::from(( + block, + tm_block_response, + ))) + .on_conflict(blocks::height) + .do_update() + .set(( + blocks::hash.eq(excluded(blocks::hash)), + blocks::app_hash.eq(excluded(blocks::app_hash)), + blocks::timestamp.eq(excluded(blocks::timestamp)), + blocks::proposer.eq(excluded(blocks::proposer)), + blocks::epoch.eq(excluded(blocks::epoch)), + )) + .execute(transaction_conn) + .context("Failed to insert block in db")?; + + anyhow::Ok(()) +} diff --git a/transactions/src/repository/mod.rs b/transactions/src/repository/mod.rs index 0824d7a9c..5ae69f54d 100644 --- a/transactions/src/repository/mod.rs +++ b/transactions/src/repository/mod.rs @@ -1 +1,2 @@ +pub mod block; pub mod transactions; diff --git a/transactions/src/repository/transactions.rs b/transactions/src/repository/transactions.rs index 5a97e60c8..b0f7ef9b3 100644 --- a/transactions/src/repository/transactions.rs +++ b/transactions/src/repository/transactions.rs @@ -1,12 +1,29 @@ +use std::collections::HashSet; + use anyhow::Context; use chrono::NaiveDateTime; use diesel::upsert::excluded; -use diesel::{ExpressionMethods, PgConnection, RunQueryDsl}; +use diesel::{ + ExpressionMethods, OptionalEmptyChangesetExtension, PgConnection, + RunQueryDsl, +}; use orm::crawler_state::{BlockStateInsertDb, CrawlerNameDb}; -use orm::schema::{crawler_state, inner_transactions, wrapper_transactions}; -use orm::transactions::{InnerTransactionInsertDb, WrapperTransactionInsertDb}; +use orm::gas::GasEstimationInsertDb; +use orm::ibc::{IbcAckInsertDb, IbcAckStatusDb, IbcSequencekStatusUpdateDb}; +use orm::schema::{ + crawler_state, gas_estimations, ibc_ack, inner_transactions, + transaction_history, wrapper_transactions, +}; +use orm::transactions::{ + InnerTransactionInsertDb, TransactionHistoryInsertDb, + WrapperTransactionInsertDb, +}; use shared::crawler_state::{BlockCrawlerState, CrawlerName}; -use shared::transaction::{InnerTransaction, WrapperTransaction}; +use shared::gas::GasEstimation; +use shared::transaction::{ + IbcAck, IbcSequence, InnerTransaction, TransactionTarget, + WrapperTransaction, +}; pub fn insert_inner_transactions( transaction_conn: &mut PgConnection, @@ -87,3 +104,73 @@ pub fn update_crawler_timestamp( anyhow::Ok(()) } + +pub fn insert_ibc_sequence( + transaction_conn: &mut PgConnection, + ibc_sequences: Vec, +) -> anyhow::Result<()> { + diesel::insert_into(ibc_ack::table) + .values::>( + ibc_sequences + .into_iter() + .map(IbcAckInsertDb::from) + .collect(), + ) + .execute(transaction_conn) + .context("Failed to update crawler state in db")?; + + anyhow::Ok(()) +} + +pub fn update_ibc_sequence( + transaction_conn: &mut PgConnection, + ibc_acks: Vec, +) -> anyhow::Result<()> { + for ack in ibc_acks { + let ack_update = IbcSequencekStatusUpdateDb { + status: IbcAckStatusDb::from(ack.status.clone()), + }; + diesel::update(ibc_ack::table) + .set(ack_update) + .filter(ibc_ack::dsl::id.eq(ack.id())) + .execute(transaction_conn) + .optional_empty_changeset() + .context("Failed to update validator metadata in db")?; + } + anyhow::Ok(()) +} + +pub fn insert_transactions_history( + transaction_conn: &mut PgConnection, + txs: HashSet, +) -> anyhow::Result<()> { + diesel::insert_into(transaction_history::table) + .values::<&Vec>( + &txs.into_iter() + .map(TransactionHistoryInsertDb::from) + .collect::>(), + ) + .on_conflict_do_nothing() + .execute(transaction_conn) + .context("Failed to insert transaction history in db")?; + + anyhow::Ok(()) +} + +pub fn insert_gas_estimates( + transaction_conn: &mut PgConnection, + gas_estimates: Vec, +) -> anyhow::Result<()> { + diesel::insert_into(gas_estimations::table) + .values::>( + gas_estimates + .into_iter() + .map(GasEstimationInsertDb::from) + .collect(), + ) + .on_conflict_do_nothing() + .execute(transaction_conn) + .context("Failed to update gas estimates in db")?; + + anyhow::Ok(()) +} diff --git a/transactions/src/services/mod.rs b/transactions/src/services/mod.rs index a9dfa39f9..233652f55 100644 --- a/transactions/src/services/mod.rs +++ b/transactions/src/services/mod.rs @@ -1,3 +1,4 @@ pub mod db; pub mod namada; pub mod tendermint; +pub mod tx; diff --git a/transactions/src/services/namada.rs b/transactions/src/services/namada.rs index 51d412202..cde1d642e 100644 --- a/transactions/src/services/namada.rs +++ b/transactions/src/services/namada.rs @@ -55,3 +55,16 @@ pub async fn query_tx_code_hash( None } } + +pub async fn get_validator_namada_address( + client: &HttpClient, + tm_addr: &Id, +) -> anyhow::Result> { + let validator = RPC + .vp() + .pos() + .validator_by_tm_addr(client, &tm_addr.to_string().to_uppercase()) + .await?; + + Ok(validator.map(Id::from)) +} diff --git a/transactions/src/services/tx.rs b/transactions/src/services/tx.rs new file mode 100644 index 000000000..2c534a2dc --- /dev/null +++ b/transactions/src/services/tx.rs @@ -0,0 +1,174 @@ +use namada_sdk::ibc::core::channel::types::acknowledgement::AcknowledgementStatus; +use namada_sdk::ibc::core::channel::types::msgs::PacketMsg; +use namada_sdk::ibc::core::handler::types::msgs::MsgEnvelope; +use shared::block_result::{BlockResult, TxAttributesType}; +use shared::gas::GasEstimation; +use shared::transaction::{ + IbcAck, IbcAckStatus, IbcSequence, InnerTransaction, TransactionKind, + WrapperTransaction, +}; + +pub fn get_ibc_packets( + block_results: &BlockResult, + inner_txs: &[InnerTransaction], +) -> Vec { + let mut ibc_txs = inner_txs + .iter() + .filter_map(|tx| { + if tx.is_ibc() && tx.was_successful() { + Some(tx.tx_id.clone()) + } else { + None + } + }) + .collect::>(); + + ibc_txs.reverse(); + + block_results + .end_events + .iter() + .filter_map(|event| { + if let Some(attributes) = &event.attributes { + match attributes { + TxAttributesType::SendPacket(packet) => Some(IbcSequence { + sequence_number: packet.sequence.clone(), + source_port: packet.source_port.clone(), + dest_port: packet.dest_port.clone(), + source_channel: packet.source_channel.clone(), + dest_channel: packet.dest_channel.clone(), + timeout: packet.timeout_timestamp, + tx_id: ibc_txs.pop().unwrap(), + }), + _ => None, + } + } else { + None + } + }) + .collect::>() +} + +pub fn get_ibc_ack_packet(inner_txs: &[InnerTransaction]) -> Vec { + inner_txs.iter().filter_map(|tx| match tx.kind.clone() { + TransactionKind::IbcMsgTransfer(Some(ibc_message)) => match ibc_message.0 { + namada_sdk::ibc::IbcMessage::Envelope(msg_envelope) => { + match *msg_envelope { + MsgEnvelope::Packet(packet_msg) => match packet_msg { + PacketMsg::Recv(_) => None, + PacketMsg::Ack(msg) => { + let ack = match serde_json::from_slice::< + AcknowledgementStatus, + >( + msg.acknowledgement.as_bytes() + ) { + Ok(status) => IbcAck { + sequence_number: msg.packet.seq_on_a.to_string(), + source_port: msg.packet.port_id_on_a.to_string(), + dest_port: msg.packet.port_id_on_b.to_string(), + source_channel: msg.packet.chan_id_on_a.to_string(), + dest_channel: msg.packet.chan_id_on_b.to_string(), + status: match status { + AcknowledgementStatus::Success(_) => IbcAckStatus::Success, + AcknowledgementStatus::Error(_) => IbcAckStatus::Fail, + }, + }, + Err(_) => IbcAck { + sequence_number: msg.packet.seq_on_a.to_string(), + source_port: msg.packet.port_id_on_a.to_string(), + dest_port: msg.packet.port_id_on_b.to_string(), + source_channel: msg.packet.chan_id_on_a.to_string(), + dest_channel: msg.packet.chan_id_on_b.to_string(), + status: IbcAckStatus::Unknown, + }, + }; + Some(ack) + } + PacketMsg::Timeout(msg) => Some(IbcAck { + sequence_number: msg.packet.seq_on_a.to_string(), + source_port: msg.packet.port_id_on_a.to_string(), + dest_port: msg.packet.port_id_on_b.to_string(), + source_channel: msg.packet.chan_id_on_a.to_string(), + dest_channel: msg.packet.chan_id_on_b.to_string(), + status: IbcAckStatus::Timeout, + }), + PacketMsg::TimeoutOnClose(msg) => Some(IbcAck { + sequence_number: msg.packet.seq_on_a.to_string(), + source_port: msg.packet.port_id_on_a.to_string(), + dest_port: msg.packet.port_id_on_b.to_string(), + source_channel: msg.packet.chan_id_on_a.to_string(), + dest_channel: msg.packet.chan_id_on_b.to_string(), + status: IbcAckStatus::Timeout, + }), + }, + _ => None, + } + }, + _ => None + }, + _ => None, + }).collect() +} + +pub fn get_gas_estimates( + inner_txs: &[InnerTransaction], + wrapper_txs: &[WrapperTransaction], +) -> Vec { + wrapper_txs + .iter() + .map(|wrapper_tx| { + let mut gas_estimate = GasEstimation::new(wrapper_tx.tx_id.clone()); + gas_estimate.signatures = wrapper_tx.total_signatures; + gas_estimate.size = wrapper_tx.size; + + inner_txs + .iter() + .filter(|inner_tx| { + inner_tx.was_successful() + && inner_tx.wrapper_id.eq(&wrapper_tx.tx_id) + }) + .for_each(|tx| match tx.kind { + TransactionKind::TransparentTransfer(_) + | TransactionKind::MixedTransfer(_) => { + gas_estimate.increase_mixed_transfer() + } + TransactionKind::IbcMsgTransfer(_) => { + gas_estimate.increase_ibc_msg_transfer() + } + TransactionKind::Bond(_) => gas_estimate.increase_bond(), + TransactionKind::Redelegation(_) => { + gas_estimate.increase_redelegation() + } + TransactionKind::Unbond(_) => { + gas_estimate.increase_unbond() + } + TransactionKind::Withdraw(_) => { + gas_estimate.increase_withdraw() + } + TransactionKind::ClaimRewards(_) => { + gas_estimate.increase_claim_rewards() + } + TransactionKind::ProposalVote(_) => { + gas_estimate.increase_vote() + } + TransactionKind::RevealPk(_) => { + gas_estimate.increase_reveal_pk() + } + TransactionKind::ShieldedTransfer(_) => { + gas_estimate.increase_shielded_transfer() + } + TransactionKind::ShieldingTransfer(_) => { + gas_estimate.increase_shielding_transfer() + } + TransactionKind::UnshieldingTransfer(_) => { + gas_estimate.increase_ibc_unshielding_transfer() + } + TransactionKind::IbcShieldingTransfer(_) => { + gas_estimate.increase_ibc_shielding_transfer() + } + _ => (), + }); + gas_estimate + }) + .collect() +} diff --git a/webserver/Cargo.toml b/webserver/Cargo.toml index f019ff58a..ecd86ee16 100644 --- a/webserver/Cargo.toml +++ b/webserver/Cargo.toml @@ -22,6 +22,7 @@ production = [] [dependencies] axum.workspace = true +chrono.workspace = true tokio.workspace = true tower.workspace = true tower-http.workspace = true diff --git a/webserver/src/app.rs b/webserver/src/app.rs index fcb26cd3d..9f1692712 100644 --- a/webserver/src/app.rs +++ b/webserver/src/app.rs @@ -19,10 +19,11 @@ use tower_http::trace::TraceLayer; use crate::appstate::AppState; use crate::config::AppConfig; use crate::handler::{ - balance as balance_handlers, chain as chain_handlers, - crawler_state as crawler_state_handlers, gas as gas_handlers, - governance as gov_handlers, pgf as pgf_service, pk as pk_handlers, - pos as pos_handlers, transaction as transaction_handlers, + balance as balance_handlers, block as block_handlers, + chain as chain_handlers, crawler_state as crawler_state_handlers, + gas as gas_handlers, governance as gov_handlers, ibc as ibc_handler, + pgf as pgf_service, pk as pk_handlers, pos as pos_handlers, + transaction as transaction_handlers, }; use crate::state::common::CommonState; @@ -109,6 +110,7 @@ impl ApplicationServer { get(pk_handlers::get_revealed_pk), ) .route("/gas", get(gas_handlers::get_gas)) + .route("/gas/estimate", get(gas_handlers::get_gas_estimate)) .route( "/gas-price/:token", get(gas_handlers::get_gas_price_by_token), @@ -122,6 +124,10 @@ impl ApplicationServer { "/chain/inner/:id", get(transaction_handlers::get_inner_tx), ) + .route( + "/chain/history", + get(transaction_handlers::get_transaction_history), + ) .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)) @@ -133,6 +139,7 @@ impl ApplicationServer { "/chain/epoch/latest", get(chain_handlers::get_last_processed_epoch), ) + .route("/ibc/:tx_id/status", get(ibc_handler::get_ibc_status)) .route( "/pgf/payments", get(pgf_service::get_pgf_continuous_payments), @@ -147,6 +154,14 @@ impl ApplicationServer { ) // Server sent events endpoints .route("/chain/status", get(chain_handlers::chain_status)) + .route( + "/block/height/:value", + get(block_handlers::get_block_by_height), + ) + .route( + "/block/timestamp/:value", + get(block_handlers::get_block_by_timestamp), + ) .route( "/metrics", get(|| async move { metric_handle.render() }), @@ -163,7 +178,7 @@ impl ApplicationServer { .nest("/api/v1", routes) .merge(Router::new().route( "/health", - get(|| async { env!("VERGEN_GIT_SHA").to_string() }), + get(|| async { json!({"commit": env!("VERGEN_GIT_SHA").to_string(), "version": env!("CARGO_PKG_VERSION") }).to_string() }), )) .layer( ServiceBuilder::new() diff --git a/webserver/src/dto/gas.rs b/webserver/src/dto/gas.rs new file mode 100644 index 000000000..24e75770e --- /dev/null +++ b/webserver/src/dto/gas.rs @@ -0,0 +1,63 @@ +use serde::{Deserialize, Serialize}; +use validator::Validate; + +use crate::error::gas::GasError; + +#[derive(Clone, Serialize, Deserialize, Validate)] +pub struct GasEstimateQuery { + #[validate(range(min = 1, max = 100))] + pub bond: Option, + #[validate(range(min = 1, max = 100))] + pub claim_rewards: Option, + #[validate(range(min = 1, max = 100))] + pub unbond: Option, + #[validate(range(min = 1, max = 100))] + pub transparent_transfer: Option, + #[validate(range(min = 1, max = 100))] + pub shielded_transfer: Option, + #[validate(range(min = 1, max = 100))] + pub shielding_transfer: Option, + #[validate(range(min = 1, max = 100))] + pub unshielding_transfer: Option, + #[validate(range(min = 1, max = 100))] + pub vote: Option, + #[validate(range(min = 1, max = 100))] + pub ibc: Option, + #[validate(range(min = 1, max = 100))] + pub withdraw: Option, + #[validate(range(min = 1, max = 100))] + pub reveal_pk: Option, + #[validate(range(min = 1, max = 100))] + pub redelegate: Option, + #[validate(range(min = 1, max = 20))] + pub signatures: Option, + #[validate(range(min = 1, max = 100000))] + pub tx_size: Option, +} + +impl GasEstimateQuery { + pub fn is_valid(&self) -> Result<(), GasError> { + let res = [ + self.bond, + self.claim_rewards, + self.unbond, + self.transparent_transfer, + self.shielded_transfer, + self.shielding_transfer, + self.unshielding_transfer, + self.vote, + self.withdraw, + self.ibc, + self.reveal_pk, + self.redelegate, + ] + .iter() + .any(|field| field.is_some()); + + if res { + Ok(()) + } else { + Err(GasError::InvalidQueryParams) + } + } +} diff --git a/webserver/src/dto/mod.rs b/webserver/src/dto/mod.rs index 23ea9f818..207e96440 100644 --- a/webserver/src/dto/mod.rs +++ b/webserver/src/dto/mod.rs @@ -1,4 +1,6 @@ pub mod crawler_state; +pub mod gas; pub mod governance; pub mod pgf; pub mod pos; +pub mod transaction; diff --git a/webserver/src/dto/pos.rs b/webserver/src/dto/pos.rs index 9a72bec3e..4a1a06978 100644 --- a/webserver/src/dto/pos.rs +++ b/webserver/src/dto/pos.rs @@ -64,15 +64,21 @@ pub enum MyValidatorKindDto { } #[derive(Clone, Serialize, Deserialize, Validate)] +#[serde(rename_all = "camelCase")] pub struct BondsDto { #[validate(range(min = 1, max = 10000))] pub page: Option, + #[validate(range(min = 0))] + pub active_at: Option, } #[derive(Clone, Serialize, Deserialize, Validate)] +#[serde(rename_all = "camelCase")] pub struct UnbondsDto { #[validate(range(min = 1, max = 10000))] pub page: Option, + #[validate(range(min = 0))] + pub active_at: Option, } #[derive(Clone, Serialize, Deserialize, Validate)] diff --git a/webserver/src/dto/transaction.rs b/webserver/src/dto/transaction.rs new file mode 100644 index 000000000..5f14eda21 --- /dev/null +++ b/webserver/src/dto/transaction.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; +use validator::Validate; + +#[derive(Clone, Serialize, Deserialize, Validate)] +#[serde(rename_all = "camelCase")] +pub struct TransactionHistoryQueryParams { + #[validate(range(min = 1, max = 10000))] + pub page: Option, + #[validate(length(min = 1, max = 10))] + pub addresses: Vec, +} diff --git a/webserver/src/error/api.rs b/webserver/src/error/api.rs index 86335a524..e18d55e12 100644 --- a/webserver/src/error/api.rs +++ b/webserver/src/error/api.rs @@ -2,10 +2,12 @@ use axum::response::{IntoResponse, Response}; use thiserror::Error; use super::balance::BalanceError; +use super::block::BlockError; use super::chain::ChainError; use super::crawler_state::CrawlerStateError; use super::gas::GasError; use super::governance::GovernanceError; +use super::ibc::IbcError; use super::pgf::PgfError; use super::pos::PoSError; use super::revealed_pk::RevealedPkError; @@ -13,6 +15,8 @@ use super::transaction::TransactionError; #[derive(Error, Debug)] pub enum ApiError { + #[error(transparent)] + BlockError(#[from] BlockError), #[error(transparent)] TransactionError(#[from] TransactionError), #[error(transparent)] @@ -28,6 +32,8 @@ pub enum ApiError { #[error(transparent)] GasError(#[from] GasError), #[error(transparent)] + IbcError(#[from] IbcError), + #[error(transparent)] PgfError(#[from] PgfError), #[error(transparent)] CrawlerStateError(#[from] CrawlerStateError), @@ -36,6 +42,7 @@ pub enum ApiError { impl IntoResponse for ApiError { fn into_response(self) -> Response { match self { + ApiError::BlockError(error) => error.into_response(), ApiError::TransactionError(error) => error.into_response(), ApiError::ChainError(error) => error.into_response(), ApiError::PoSError(error) => error.into_response(), @@ -43,6 +50,7 @@ impl IntoResponse for ApiError { ApiError::GovernanceError(error) => error.into_response(), ApiError::RevealedPkError(error) => error.into_response(), ApiError::GasError(error) => error.into_response(), + ApiError::IbcError(error) => error.into_response(), ApiError::PgfError(error) => error.into_response(), ApiError::CrawlerStateError(error) => error.into_response(), } diff --git a/webserver/src/error/block.rs b/webserver/src/error/block.rs new file mode 100644 index 000000000..43648a432 --- /dev/null +++ b/webserver/src/error/block.rs @@ -0,0 +1,28 @@ +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use thiserror::Error; + +use crate::response::api::ApiErrorResponse; + +#[derive(Error, Debug)] +pub enum BlockError { + #[error("Block not found error at {0}: {1}")] + NotFound(String, String), + #[error("Database error: {0}")] + Database(String), + #[error("Unknown error: {0}")] + Unknown(String), +} + +impl IntoResponse for BlockError { + fn into_response(self) -> Response { + let status_code = match self { + BlockError::Unknown(_) | BlockError::Database(_) => { + StatusCode::INTERNAL_SERVER_ERROR + } + BlockError::NotFound(_, _) => StatusCode::NOT_FOUND, + }; + + ApiErrorResponse::send(status_code.as_u16(), Some(self.to_string())) + } +} diff --git a/webserver/src/error/gas.rs b/webserver/src/error/gas.rs index 5e303b9be..52e0bdbb1 100644 --- a/webserver/src/error/gas.rs +++ b/webserver/src/error/gas.rs @@ -6,6 +6,8 @@ use crate::response::api::ApiErrorResponse; #[derive(Error, Debug)] pub enum GasError { + #[error("Invalid query parameters")] + InvalidQueryParams, #[error("Database error: {0}")] Database(String), #[error("Unknown error: {0}")] @@ -15,6 +17,7 @@ pub enum GasError { impl IntoResponse for GasError { fn into_response(self) -> Response { let status_code = match self { + GasError::InvalidQueryParams => StatusCode::BAD_GATEWAY, GasError::Unknown(_) | GasError::Database(_) => { StatusCode::INTERNAL_SERVER_ERROR } diff --git a/webserver/src/error/ibc.rs b/webserver/src/error/ibc.rs new file mode 100644 index 000000000..869d738ce --- /dev/null +++ b/webserver/src/error/ibc.rs @@ -0,0 +1,28 @@ +use axum::http::StatusCode; +use axum::response::IntoResponse; +use thiserror::Error; + +use crate::response::api::ApiErrorResponse; + +#[derive(Error, Debug)] +pub enum IbcError { + #[error("Revealed public key {0} not found")] + NotFound(u64), + #[error("Database error: {0}")] + Database(String), + #[error("Unknown error: {0}")] + Unknown(String), +} + +impl IntoResponse for IbcError { + fn into_response(self) -> axum::response::Response { + let status_code = match self { + IbcError::NotFound(_) => StatusCode::NOT_FOUND, + IbcError::Unknown(_) | IbcError::Database(_) => { + StatusCode::INTERNAL_SERVER_ERROR + } + }; + + ApiErrorResponse::send(status_code.as_u16(), Some(self.to_string())) + } +} diff --git a/webserver/src/error/mod.rs b/webserver/src/error/mod.rs index 092b8e379..9b21bf09a 100644 --- a/webserver/src/error/mod.rs +++ b/webserver/src/error/mod.rs @@ -1,9 +1,11 @@ pub mod api; pub mod balance; +pub mod block; pub mod chain; pub mod crawler_state; pub mod gas; pub mod governance; +pub mod ibc; pub mod pgf; pub mod pos; pub mod revealed_pk; diff --git a/webserver/src/handler/block.rs b/webserver/src/handler/block.rs new file mode 100644 index 000000000..ed4bb01b3 --- /dev/null +++ b/webserver/src/handler/block.rs @@ -0,0 +1,30 @@ +use axum::extract::{Path, State}; +use axum::http::HeaderMap; +use axum::Json; +use axum_macros::debug_handler; + +use crate::error::api::ApiError; +use crate::response::block::Block; +use crate::state::common::CommonState; + +#[debug_handler] +pub async fn get_block_by_height( + _headers: HeaderMap, + Path(value): Path, + State(state): State, +) -> Result, ApiError> { + let block = state.block_service.get_block_by_height(value).await?; + + Ok(Json(block)) +} + +#[debug_handler] +pub async fn get_block_by_timestamp( + _headers: HeaderMap, + Path(value): Path, + State(state): State, +) -> Result, ApiError> { + let block = state.block_service.get_block_by_timestamp(value).await?; + + Ok(Json(block)) +} diff --git a/webserver/src/handler/crawler_state.rs b/webserver/src/handler/crawler_state.rs index ab4667bb6..6475b65d7 100644 --- a/webserver/src/handler/crawler_state.rs +++ b/webserver/src/handler/crawler_state.rs @@ -41,7 +41,7 @@ pub async fn get_crawlers_timestamps( || CrawlersTimestamps { name: variant.to_string(), timestamp: 0, - last_processed_block: None, + last_processed_block_height: None, }, |ct| ct.clone(), ) diff --git a/webserver/src/handler/gas.rs b/webserver/src/handler/gas.rs index bd009950c..5b559792d 100644 --- a/webserver/src/handler/gas.rs +++ b/webserver/src/handler/gas.rs @@ -1,10 +1,11 @@ -use axum::extract::{Path, State}; +use axum::extract::{Path, Query, State}; use axum::http::HeaderMap; use axum::Json; use axum_macros::debug_handler; +use crate::dto::gas::GasEstimateQuery; use crate::error::api::ApiError; -use crate::response::gas::{Gas, GasPrice}; +use crate::response::gas::{Gas, GasEstimate, GasPrice}; use crate::state::common::CommonState; #[debug_handler] @@ -37,3 +38,34 @@ pub async fn get_all_gas_prices( Ok(Json(gas_price)) } + +#[debug_handler] +pub async fn get_gas_estimate( + _headers: HeaderMap, + Query(query): Query, + State(state): State, +) -> Result, ApiError> { + query.is_valid()?; + + let gas = state + .gas_service + .estimate_gas( + query.bond.unwrap_or(0), + query.redelegate.unwrap_or(0), + query.claim_rewards.unwrap_or(0), + query.unbond.unwrap_or(0), + query.transparent_transfer.unwrap_or(0), + query.shielded_transfer.unwrap_or(0), + query.shielding_transfer.unwrap_or(0), + query.unshielding_transfer.unwrap_or(0), + query.vote.unwrap_or(0), + query.ibc.unwrap_or(0), + query.withdraw.unwrap_or(0), + query.reveal_pk.unwrap_or(0), + query.signatures.unwrap_or(2), + query.tx_size.unwrap_or(0), + ) + .await?; + + Ok(Json(gas)) +} diff --git a/webserver/src/handler/ibc.rs b/webserver/src/handler/ibc.rs new file mode 100644 index 000000000..ecf8ff83d --- /dev/null +++ b/webserver/src/handler/ibc.rs @@ -0,0 +1,19 @@ +use axum::extract::{Path, State}; +use axum::http::HeaderMap; +use axum::Json; +use axum_macros::debug_handler; + +use crate::error::api::ApiError; +use crate::response::ibc::IbcAck; +use crate::state::common::CommonState; + +#[debug_handler] +pub async fn get_ibc_status( + _headers: HeaderMap, + Path(tx_id): Path, + State(state): State, +) -> Result, ApiError> { + let ibc_ack_status = state.ibc_service.get_ack_by_tx_id(tx_id).await?; + + Ok(Json(ibc_ack_status)) +} diff --git a/webserver/src/handler/mod.rs b/webserver/src/handler/mod.rs index 9b0dd7931..79aef8369 100644 --- a/webserver/src/handler/mod.rs +++ b/webserver/src/handler/mod.rs @@ -1,8 +1,10 @@ pub mod balance; +pub mod block; pub mod chain; pub mod crawler_state; pub mod gas; pub mod governance; +pub mod ibc; pub mod pgf; pub mod pk; pub mod pos; diff --git a/webserver/src/handler/pos.rs b/webserver/src/handler/pos.rs index d589c8ed1..a6b8e4c37 100644 --- a/webserver/src/handler/pos.rs +++ b/webserver/src/handler/pos.rs @@ -57,7 +57,7 @@ pub async fn get_bonds( let (bonds, total_pages, total_bonds) = state .pos_service - .get_bonds_by_address(address, page) + .get_bonds_by_address(address, page, query.active_at) .await?; let response = @@ -97,7 +97,7 @@ pub async fn get_unbonds( let (unbonds, total_pages, total_unbonds) = state .pos_service - .get_unbonds_by_address(address, page) + .get_unbonds_by_address(address, page, query.active_at) .await?; let response = diff --git a/webserver/src/handler/transaction.rs b/webserver/src/handler/transaction.rs index 32a303e5f..6ff02c081 100644 --- a/webserver/src/handler/transaction.rs +++ b/webserver/src/handler/transaction.rs @@ -1,11 +1,16 @@ use axum::extract::{Path, State}; use axum::http::HeaderMap; use axum::Json; +use axum_extra::extract::Query; use axum_macros::debug_handler; +use crate::dto::transaction::TransactionHistoryQueryParams; use crate::error::api::ApiError; use crate::error::transaction::TransactionError; -use crate::response::transaction::{InnerTransaction, WrapperTransaction}; +use crate::response::transaction::{ + InnerTransaction, TransactionHistory, WrapperTransaction, +}; +use crate::response::utils::PaginatedResponse; use crate::state::common::CommonState; #[debug_handler] @@ -15,6 +20,7 @@ pub async fn get_wrapper_tx( State(state): State, ) -> Result>, ApiError> { is_valid_hash(&tx_id)?; + let tx_id = tx_id.to_lowercase(); let wrapper_tx = state .transaction_service @@ -44,6 +50,7 @@ pub async fn get_inner_tx( State(state): State, ) -> Result>, ApiError> { is_valid_hash(&tx_id)?; + let tx_id = tx_id.to_lowercase(); let inner_tx = state .transaction_service @@ -57,6 +64,25 @@ pub async fn get_inner_tx( Ok(Json(inner_tx)) } +#[debug_handler] +pub async fn get_transaction_history( + _headers: HeaderMap, + Query(query): Query, + State(state): State, +) -> Result>>, ApiError> { + let page = query.page.unwrap_or(1); + + let (transactions, total_pages, total_items) = state + .transaction_service + .get_addresses_history(query.addresses, page) + .await?; + + let response = + PaginatedResponse::new(transactions, page, total_pages, total_items); + + Ok(Json(response)) +} + fn is_valid_hash(hash: &str) -> Result<(), TransactionError> { if hash.len().eq(&64) { Ok(()) diff --git a/webserver/src/repository/block.rs b/webserver/src/repository/block.rs new file mode 100644 index 000000000..cb13f4ee1 --- /dev/null +++ b/webserver/src/repository/block.rs @@ -0,0 +1,72 @@ +use axum::async_trait; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper}; +use orm::blocks::BlockDb; +use orm::schema::blocks; + +use crate::appstate::AppState; + +#[derive(Clone)] +pub struct BlockRepository { + pub(crate) app_state: AppState, +} + +#[async_trait] +pub trait BlockRepositoryTrait { + fn new(app_state: AppState) -> Self; + + async fn find_block_by_height( + &self, + height: i32, + ) -> Result, String>; + + async fn find_block_by_timestamp( + &self, + timestamp: i64, + ) -> Result, String>; +} + +#[async_trait] +impl BlockRepositoryTrait for BlockRepository { + fn new(app_state: AppState) -> Self { + Self { app_state } + } + + async fn find_block_by_height( + &self, + height: i32, + ) -> Result, String> { + let conn = self.app_state.get_db_connection().await; + + conn.interact(move |conn| { + blocks::table + .filter(blocks::dsl::height.eq(height)) + .select(BlockDb::as_select()) + .first(conn) + .ok() + }) + .await + .map_err(|e| e.to_string()) + } + + /// Gets the last block preceeding the given timestamp + async fn find_block_by_timestamp( + &self, + timestamp: i64, + ) -> Result, String> { + let conn = self.app_state.get_db_connection().await; + let timestamp = chrono::DateTime::from_timestamp(timestamp, 0) + .expect("Invalid timestamp") + .naive_utc(); + + conn.interact(move |conn| { + blocks::table + .filter(blocks::timestamp.le(timestamp)) + .order(blocks::timestamp.desc()) + .select(BlockDb::as_select()) + .first(conn) + .ok() + }) + .await + .map_err(|e| e.to_string()) + } +} diff --git a/webserver/src/repository/gas.rs b/webserver/src/repository/gas.rs index da3ea9b1d..6ef41d6fb 100644 --- a/webserver/src/repository/gas.rs +++ b/webserver/src/repository/gas.rs @@ -1,7 +1,13 @@ use axum::async_trait; -use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper}; +use bigdecimal::BigDecimal; +use diesel::dsl::{avg, count, max, min}; +use diesel::sql_types::{BigInt, Integer, Nullable, Numeric}; +use diesel::{ + ExpressionMethods, IntoSql, JoinOnDsl, QueryDsl, RunQueryDsl, + SelectableHelper, +}; use orm::gas::{GasDb, GasPriceDb}; -use orm::schema::{gas, gas_price}; +use orm::schema::{gas, gas_estimations, gas_price, wrapper_transactions}; use crate::appstate::AppState; @@ -22,6 +28,25 @@ pub trait GasRepositoryTrait { ) -> Result, String>; async fn find_all_gas_prices(&self) -> Result, String>; + + #[allow(clippy::too_many_arguments)] + async fn find_gas_estimates( + &self, + bond: u64, + redelegate: u64, + claim_rewards: u64, + unbond: u64, + transparent_transfer: u64, + shielded_transfer: u64, + shielding_transfer: u64, + unshielding_transfer: u64, + vote: u64, + ibc: u64, + withdraw: u64, + reveal_pk: u64, + signatures: u64, + tx_size: u64, + ) -> Result<(Option, Option, Option, i64), String>; } #[async_trait] @@ -70,4 +95,83 @@ impl GasRepositoryTrait for GasRepository { .map_err(|e| e.to_string())? .map_err(|e| e.to_string()) } + + #[allow(clippy::too_many_arguments)] + async fn find_gas_estimates( + &self, + bond: u64, + redelegate: u64, + claim_rewards: u64, + unbond: u64, + transparent_transfer: u64, + shielded_transfer: u64, + shielding_transfer: u64, + unshielding_transfer: u64, + vote: u64, + ibc: u64, + withdraw: u64, + reveal_pk: u64, + signatures: u64, + tx_size: u64, + ) -> Result<(Option, Option, Option, i64), String> + { + let conn = self.app_state.get_db_connection().await; + + conn.interact(move |conn| { + gas_estimations::table + .filter(gas_estimations::dsl::bond.eq(bond as i32)) + .filter( + gas_estimations::dsl::redelegation.eq(redelegate as i32), + ) + .filter( + gas_estimations::dsl::claim_rewards + .eq(claim_rewards as i32), + ) + .filter(gas_estimations::dsl::unbond.eq(unbond as i32)) + .filter( + gas_estimations::dsl::transparent_transfer + .eq(transparent_transfer as i32), + ) + .filter( + gas_estimations::dsl::shielded_transfer + .eq(shielded_transfer as i32), + ) + .filter( + gas_estimations::dsl::shielding_transfer + .eq(shielding_transfer as i32), + ) + .filter( + gas_estimations::dsl::unshielding_transfer + .eq(unshielding_transfer as i32), + ) + .filter(gas_estimations::dsl::vote_proposal.eq(vote as i32)) + .filter(gas_estimations::dsl::ibc_msg_transfer.eq(ibc as i32)) + .filter(gas_estimations::dsl::withdraw.eq(withdraw as i32)) + .filter(gas_estimations::dsl::reveal_pk.eq(reveal_pk as i32)) + .filter(gas_estimations::dsl::signatures.ge(signatures as i32)) + .filter(gas_estimations::dsl::tx_size.ge(tx_size as i32)) + .inner_join( + wrapper_transactions::table + .on(gas_estimations::dsl::wrapper_id + .eq(wrapper_transactions::dsl::id)), + ) + .limit(100) + .select(( + min(wrapper_transactions::dsl::gas_used) + .into_sql::>(), + max(wrapper_transactions::dsl::gas_used) + .into_sql::>(), + avg(wrapper_transactions::dsl::gas_used) + .into_sql::>(), + count(wrapper_transactions::dsl::gas_used) + .into_sql::(), + )) + .get_result::<(Option, Option, Option, i64)>( + conn, + ) + }) + .await + .map_err(|e| e.to_string())? + .map_err(|e| e.to_string()) + } } diff --git a/webserver/src/repository/ibc.rs b/webserver/src/repository/ibc.rs new file mode 100644 index 000000000..e55ef4a60 --- /dev/null +++ b/webserver/src/repository/ibc.rs @@ -0,0 +1,45 @@ +use axum::async_trait; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper}; +use orm::ibc::IbcAckDb; +use orm::schema::ibc_ack; + +use crate::appstate::AppState; + +#[derive(Clone)] +pub struct IbcRepository { + pub(crate) app_state: AppState, +} + +#[async_trait] +pub trait IbcRepositoryTrait { + fn new(app_state: AppState) -> Self; + + async fn find_ibc_ack( + &self, + id: String, + ) -> Result, String>; +} + +#[async_trait] +impl IbcRepositoryTrait for IbcRepository { + fn new(app_state: AppState) -> Self { + Self { app_state } + } + + async fn find_ibc_ack( + &self, + id: String, + ) -> Result, String> { + let conn = self.app_state.get_db_connection().await; + + conn.interact(move |conn| { + ibc_ack::table + .filter(ibc_ack::dsl::tx_hash.eq(id)) + .select(IbcAckDb::as_select()) + .first(conn) + .ok() + }) + .await + .map_err(|e| e.to_string()) + } +} diff --git a/webserver/src/repository/mod.rs b/webserver/src/repository/mod.rs index 8153b5ddd..5d6ace094 100644 --- a/webserver/src/repository/mod.rs +++ b/webserver/src/repository/mod.rs @@ -1,7 +1,9 @@ pub mod balance; +pub mod block; pub mod chain; pub mod gas; pub mod governance; +pub mod ibc; pub mod pgf; pub mod pos; pub mod revealed_pk; diff --git a/webserver/src/repository/pos.rs b/webserver/src/repository/pos.rs index 327592369..e623b045f 100644 --- a/webserver/src/repository/pos.rs +++ b/webserver/src/repository/pos.rs @@ -60,12 +60,14 @@ pub trait PosRepositoryTrait { &self, address: String, page: i64, + active_at: Option, ) -> Result, String>; async fn find_unbonds_by_address( &self, address: String, page: i64, + active_at: Option, ) -> Result, String>; async fn find_merged_unbonds_by_address( @@ -183,12 +185,19 @@ impl PosRepositoryTrait for PosRepository { &self, address: String, page: i64, + active_at: Option, ) -> Result, String> { let conn = self.app_state.get_db_connection().await; conn.interact(move |conn| { - validators::table - .inner_join(bonds::table) + let mut query = + validators::table.inner_join(bonds::table).into_boxed(); + + if let Some(at) = active_at { + query = query.filter(bonds::dsl::start.le(at)); + } + + query .filter(bonds::dsl::address.eq(address)) .select((validators::all_columns, bonds::all_columns)) .paginate(page) @@ -235,12 +244,19 @@ impl PosRepositoryTrait for PosRepository { &self, address: String, page: i64, + active_at: Option, ) -> Result, String> { let conn = self.app_state.get_db_connection().await; conn.interact(move |conn| { - validators::table - .inner_join(unbonds::table) + let mut query = + validators::table.inner_join(unbonds::table).into_boxed(); + + if let Some(at) = active_at { + query = query.filter(unbonds::dsl::withdraw_epoch.lt(at)); + } + + query .filter(unbonds::dsl::address.eq(address)) .select((validators::all_columns, unbonds::all_columns)) .paginate(page) diff --git a/webserver/src/repository/tranasaction.rs b/webserver/src/repository/tranasaction.rs index ade21154b..a82a5cc39 100644 --- a/webserver/src/repository/tranasaction.rs +++ b/webserver/src/repository/tranasaction.rs @@ -1,8 +1,15 @@ use axum::async_trait; -use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper}; -use orm::schema::{inner_transactions, wrapper_transactions}; -use orm::transactions::{InnerTransactionDb, WrapperTransactionDb}; +use diesel::{ + ExpressionMethods, JoinOnDsl, QueryDsl, RunQueryDsl, SelectableHelper, +}; +use orm::schema::{ + inner_transactions, transaction_history, wrapper_transactions, +}; +use orm::transactions::{ + InnerTransactionDb, TransactionHistoryDb, WrapperTransactionDb, +}; +use super::utils::{Paginate, PaginatedResponseDb}; use crate::appstate::AppState; #[derive(Clone)] @@ -26,6 +33,18 @@ pub trait TransactionRepositoryTrait { &self, id: String, ) -> Result, String>; + async fn find_addresses_history( + &self, + addresses: Vec, + page: i64, + ) -> Result< + PaginatedResponseDb<(TransactionHistoryDb, InnerTransactionDb, i32)>, + String, + >; + async fn find_txs_by_block_height( + &self, + block_height: i32, + ) -> Result, String>; } #[async_trait] @@ -84,4 +103,48 @@ impl TransactionRepositoryTrait for TransactionRepository { .await .map_err(|e| e.to_string()) } + + async fn find_addresses_history( + &self, + addresses: Vec, + page: i64, + ) -> Result< + PaginatedResponseDb<(TransactionHistoryDb, InnerTransactionDb, i32)>, + String, + > { + let conn = self.app_state.get_db_connection().await; + + conn.interact(move |conn| { + transaction_history::table + .filter(transaction_history::dsl::target.eq_any(addresses)) + .inner_join(inner_transactions::table.on(transaction_history::dsl::inner_tx_id.eq(inner_transactions::dsl::id))) + .inner_join(wrapper_transactions::table.on(inner_transactions::dsl::wrapper_id.eq(wrapper_transactions::dsl::id))) + .order(wrapper_transactions::dsl::block_height.desc()) + .select((transaction_history::all_columns, inner_transactions::all_columns, wrapper_transactions::dsl::block_height)) + .paginate(page) + .load_and_count_pages::<(TransactionHistoryDb, InnerTransactionDb, i32)>(conn) + }) + .await + .map_err(|e| e.to_string())? + .map_err(|e| e.to_string()) + } + + async fn find_txs_by_block_height( + &self, + block_height: i32, + ) -> Result, String> { + let conn = self.app_state.get_db_connection().await; + + conn.interact(move |conn| { + wrapper_transactions::table + .filter( + wrapper_transactions::dsl::block_height.eq(block_height), + ) + .select(WrapperTransactionDb::as_select()) + .get_results(conn) + }) + .await + .map_err(|e| e.to_string())? + .map_err(|e| e.to_string()) + } } diff --git a/webserver/src/response/block.rs b/webserver/src/response/block.rs new file mode 100644 index 000000000..4d9b2c15c --- /dev/null +++ b/webserver/src/response/block.rs @@ -0,0 +1,42 @@ +use orm::blocks::BlockDb; +use orm::transactions::WrapperTransactionDb; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Block { + pub height: i32, + pub hash: Option, + pub app_hash: Option, + pub timestamp: Option, + pub proposer: Option, + pub transactions: Vec, + pub parent_hash: Option, + pub epoch: Option, +} + +impl Block { + pub fn from( + block_db: BlockDb, + prev_block_db: Option, + transactions: Vec, + ) -> Self { + Self { + height: block_db.height, + hash: block_db.hash, + app_hash: block_db.app_hash, + timestamp: block_db + .timestamp + .map(|t| t.and_utc().timestamp().to_string()), + proposer: block_db.proposer, + transactions: transactions + .into_iter() + .map(|wrapper| wrapper.id.to_lowercase()) + .collect(), + parent_hash: prev_block_db + .map(|block| block.app_hash) + .unwrap_or(None), + epoch: block_db.epoch.map(|e| e.to_string()), + } + } +} diff --git a/webserver/src/response/crawler_state.rs b/webserver/src/response/crawler_state.rs index cf2f0d0d7..9ff756d76 100644 --- a/webserver/src/response/crawler_state.rs +++ b/webserver/src/response/crawler_state.rs @@ -5,5 +5,5 @@ use serde::{Deserialize, Serialize}; pub struct CrawlersTimestamps { pub name: String, pub timestamp: i64, - pub last_processed_block: Option, + pub last_processed_block_height: Option, } diff --git a/webserver/src/response/gas.rs b/webserver/src/response/gas.rs index b79cf7c48..6f9ce3264 100644 --- a/webserver/src/response/gas.rs +++ b/webserver/src/response/gas.rs @@ -34,3 +34,12 @@ impl From for GasPrice { } } } + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GasEstimate { + pub min: u64, + pub max: u64, + pub avg: u64, + pub total_estimates: u64, +} diff --git a/webserver/src/response/governance.rs b/webserver/src/response/governance.rs index 4b5847976..4162d1f9d 100644 --- a/webserver/src/response/governance.rs +++ b/webserver/src/response/governance.rs @@ -60,6 +60,7 @@ pub enum VoteType { Yay, Nay, Abstain, + Unknown, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -229,6 +230,7 @@ impl From for ProposalVote { GovernanceVoteKindDb::Nay => VoteType::Nay, GovernanceVoteKindDb::Yay => VoteType::Yay, GovernanceVoteKindDb::Abstain => VoteType::Abstain, + GovernanceVoteKindDb::Unknown => VoteType::Unknown, }, voter_address: value.voter_address, } diff --git a/webserver/src/response/ibc.rs b/webserver/src/response/ibc.rs new file mode 100644 index 000000000..c215b789e --- /dev/null +++ b/webserver/src/response/ibc.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum IbcAckStatus { + Success, + Fail, + Timeout, + Unknown, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IbcAck { + pub status: IbcAckStatus, + pub timeout: Option, +} diff --git a/webserver/src/response/mod.rs b/webserver/src/response/mod.rs index e00cfb59d..8215c6410 100644 --- a/webserver/src/response/mod.rs +++ b/webserver/src/response/mod.rs @@ -1,9 +1,11 @@ pub mod api; pub mod balance; +pub mod block; pub mod chain; pub mod crawler_state; pub mod gas; pub mod governance; +pub mod ibc; pub mod pgf; pub mod pos; pub mod revealed_pk; diff --git a/webserver/src/response/pos.rs b/webserver/src/response/pos.rs index 6073ad177..a07eb8d53 100644 --- a/webserver/src/response/pos.rs +++ b/webserver/src/response/pos.rs @@ -16,6 +16,9 @@ pub enum ValidatorState { BelowThreshold, Inactive, Jailed, + Deactivating, + Reactivating, + Unjailing, Unknown, } @@ -27,6 +30,9 @@ impl From for ValidatorState { ValidatorStateDb::BelowThreshold => Self::BelowThreshold, ValidatorStateDb::Inactive => Self::Inactive, ValidatorStateDb::Jailed => Self::Jailed, + ValidatorStateDb::Deactivating => Self::Deactivating, + ValidatorStateDb::Reactivating => Self::Reactivating, + ValidatorStateDb::Unjailing => Self::Unjailing, ValidatorStateDb::Unknown => Self::Unknown, } } diff --git a/webserver/src/response/transaction.rs b/webserver/src/response/transaction.rs index 56d9b9b01..b011e8d17 100644 --- a/webserver/src/response/transaction.rs +++ b/webserver/src/response/transaction.rs @@ -1,22 +1,24 @@ use orm::transactions::{ - InnerTransactionDb, TransactionKindDb, TransactionResultDb, - WrapperTransactionDb, + InnerTransactionDb, TransactionHistoryDb, TransactionHistoryKindDb, + TransactionKindDb, TransactionResultDb, WrapperTransactionDb, }; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] pub enum TransactionResult { Applied, Rejected, } -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Hash)] #[serde(rename_all = "camelCase")] pub enum TransactionKind { TransparentTransfer, ShieldedTransfer, ShieldingTransfer, UnshieldingTransfer, + MixedTransfer, Bond, Redelegation, Unbond, @@ -28,7 +30,13 @@ pub enum TransactionKind { ChangeCommission, RevealPk, IbcMsgTransfer, + IbcTransparentTransfer, + IbcShieldingTransfer, + IbcUnshieldingTransfer, BecomeValidator, + DeactivateValidator, + ReactivateValidator, + UnjailValidator, Unknown, } @@ -38,7 +46,9 @@ pub struct WrapperTransaction { pub tx_id: String, pub fee_payer: String, pub fee_token: String, - pub gas_limit: String, + pub gas_limit: u64, + pub gas_used: Option, + pub amount_per_gas_unit: Option, pub block_height: u64, pub inner_transactions: Vec, pub exit_code: TransactionResult, @@ -90,39 +100,36 @@ impl From for TransactionResult { impl From for TransactionKind { fn from(value: TransactionKindDb) -> Self { match value { - TransactionKindDb::TransparentTransfer => { - TransactionKind::TransparentTransfer - } - TransactionKindDb::ShieldedTransfer => { - TransactionKind::ShieldedTransfer - } - TransactionKindDb::ShieldingTransfer => { - TransactionKind::ShieldingTransfer - } - TransactionKindDb::UnshieldingTransfer => { - TransactionKind::UnshieldingTransfer + TransactionKindDb::TransparentTransfer => Self::TransparentTransfer, + TransactionKindDb::ShieldedTransfer => Self::ShieldedTransfer, + TransactionKindDb::ShieldingTransfer => Self::ShieldingTransfer, + TransactionKindDb::UnshieldingTransfer => Self::UnshieldingTransfer, + TransactionKindDb::MixedTransfer => Self::MixedTransfer, + TransactionKindDb::Bond => Self::Bond, + TransactionKindDb::Redelegation => Self::Redelegation, + TransactionKindDb::Unbond => Self::Unbond, + TransactionKindDb::Withdraw => Self::Withdraw, + TransactionKindDb::ClaimRewards => Self::ClaimRewards, + TransactionKindDb::VoteProposal => Self::VoteProposal, + TransactionKindDb::InitProposal => Self::InitProposal, + TransactionKindDb::ChangeMetadata => Self::ChangeMetadata, + TransactionKindDb::ChangeCommission => Self::ChangeCommission, + TransactionKindDb::RevealPk => Self::RevealPk, + TransactionKindDb::Unknown => Self::Unknown, + TransactionKindDb::IbcMsgTransfer => Self::IbcMsgTransfer, + TransactionKindDb::IbcTransparentTransfer => { + Self::IbcTransparentTransfer } - TransactionKindDb::Bond => TransactionKind::Bond, - TransactionKindDb::Redelegation => TransactionKind::Redelegation, - TransactionKindDb::Unbond => TransactionKind::Unbond, - TransactionKindDb::Withdraw => TransactionKind::Withdraw, - TransactionKindDb::ClaimRewards => TransactionKind::ClaimRewards, - TransactionKindDb::VoteProposal => TransactionKind::VoteProposal, - TransactionKindDb::InitProposal => TransactionKind::InitProposal, - TransactionKindDb::ChangeMetadata => { - TransactionKind::ChangeMetadata + TransactionKindDb::IbcShieldingTransfer => { + Self::IbcShieldingTransfer } - TransactionKindDb::ChangeCommission => { - TransactionKind::ChangeCommission - } - TransactionKindDb::RevealPk => TransactionKind::RevealPk, - TransactionKindDb::Unknown => TransactionKind::Unknown, - TransactionKindDb::IbcMsgTransfer => { - TransactionKind::IbcMsgTransfer - } - TransactionKindDb::BecomeValidator => { - TransactionKind::BecomeValidator + TransactionKindDb::IbcUnshieldingTransfer => { + Self::IbcUnshieldingTransfer } + TransactionKindDb::BecomeValidator => Self::BecomeValidator, + TransactionKindDb::ReactivateValidator => Self::ReactivateValidator, + TransactionKindDb::DeactivateValidator => Self::DeactivateValidator, + TransactionKindDb::UnjailValidator => Self::UnjailValidator, } } } @@ -133,7 +140,12 @@ impl From for WrapperTransaction { tx_id: value.id, fee_payer: value.fee_payer, fee_token: value.fee_token, - gas_limit: value.gas_limit, + gas_limit: value.gas_limit.parse::().unwrap_or(0), + gas_used: value.gas_used.map(|gas| gas as u64), + amount_per_gas_unit: value + .amount_per_gas_unit + .map(|gas| gas.parse::().ok()) + .unwrap_or(None), block_height: value.block_height as u64, inner_transactions: vec![], exit_code: TransactionResult::from(value.exit_code), @@ -154,3 +166,39 @@ impl From for InnerTransaction { } } } + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum TrasactionHistoryKind { + Received, + Sent, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionHistory { + pub tx: InnerTransaction, + pub target: String, + pub kind: TrasactionHistoryKind, + pub block_height: i32, +} + +impl TransactionHistory { + pub fn from( + transaction_history_db: TransactionHistoryDb, + inner_tx_db: InnerTransactionDb, + block_height: i32, + ) -> Self { + Self { + tx: InnerTransaction::from(inner_tx_db), + target: transaction_history_db.target, + kind: match transaction_history_db.kind { + TransactionHistoryKindDb::Received => { + TrasactionHistoryKind::Received + } + TransactionHistoryKindDb::Sent => TrasactionHistoryKind::Sent, + }, + block_height, + } + } +} diff --git a/webserver/src/service/block.rs b/webserver/src/service/block.rs new file mode 100644 index 000000000..1bc02def9 --- /dev/null +++ b/webserver/src/service/block.rs @@ -0,0 +1,87 @@ +use crate::appstate::AppState; +use crate::error::block::BlockError; +use crate::repository::block::{BlockRepository, BlockRepositoryTrait}; +use crate::repository::tranasaction::{ + TransactionRepository, TransactionRepositoryTrait, +}; +use crate::response::block::Block; + +#[derive(Clone)] +pub struct BlockService { + block_repo: BlockRepository, + transaction_repo: TransactionRepository, +} + +impl BlockService { + pub fn new(app_state: AppState) -> Self { + Self { + block_repo: BlockRepository::new(app_state.clone()), + transaction_repo: TransactionRepository::new(app_state), + } + } + + pub async fn get_block_by_height( + &self, + height: i32, + ) -> Result { + let block = self + .block_repo + .find_block_by_height(height) + .await + .map_err(BlockError::Database)?; + let block = block.ok_or(BlockError::NotFound( + "height".to_string(), + height.to_string(), + ))?; + let prev_block = if let Some(block_height) = block.height.checked_sub(1) + { + self.block_repo + .find_block_by_height(block_height) + .await + .map_err(BlockError::Database)? + } else { + None + }; + + let transactions = self + .transaction_repo + .find_txs_by_block_height(block.height) + .await + .map_err(BlockError::Database)?; + + Ok(Block::from(block, prev_block, transactions)) + } + + pub async fn get_block_by_timestamp( + &self, + timestamp: i64, + ) -> Result { + let block = self + .block_repo + .find_block_by_timestamp(timestamp) + .await + .map_err(BlockError::Database)?; + + let block = block.ok_or(BlockError::NotFound( + "timestamp".to_string(), + timestamp.to_string(), + ))?; + let prev_block = if let Some(block_height) = block.height.checked_sub(1) + { + self.block_repo + .find_block_by_height(block_height) + .await + .map_err(BlockError::Database)? + } else { + None + }; + + let transactions = self + .transaction_repo + .find_txs_by_block_height(block.height) + .await + .map_err(BlockError::Database)?; + + Ok(Block::from(block, prev_block, transactions)) + } +} diff --git a/webserver/src/service/crawler_state.rs b/webserver/src/service/crawler_state.rs index 1e88c9568..7fd74df57 100644 --- a/webserver/src/service/crawler_state.rs +++ b/webserver/src/service/crawler_state.rs @@ -49,7 +49,7 @@ impl CrawlerStateService { .map(|crawler| CrawlersTimestamps { name: crawler.name.to_string(), timestamp: crawler.timestamp.and_utc().timestamp(), - last_processed_block: crawler.last_processed_block, + last_processed_block_height: crawler.last_processed_block, }) .collect::>() }) diff --git a/webserver/src/service/gas.rs b/webserver/src/service/gas.rs index 8077e8e01..82b197d65 100644 --- a/webserver/src/service/gas.rs +++ b/webserver/src/service/gas.rs @@ -1,7 +1,12 @@ +use std::collections::HashMap; + +use bigdecimal::ToPrimitive; + use crate::appstate::AppState; use crate::error::gas::GasError; use crate::repository::gas::{GasRepository, GasRepositoryTrait}; -use crate::response::gas::{Gas, GasPrice}; +use crate::response::gas::{Gas, GasEstimate, GasPrice}; +use crate::response::transaction::TransactionKind; #[derive(Clone)] pub struct GasService { @@ -43,4 +48,100 @@ impl GasService { .map_err(GasError::Database) .map(|r| r.iter().cloned().map(GasPrice::from).collect()) } + + #[allow(clippy::too_many_arguments)] + pub async fn estimate_gas( + &self, + bond: u64, + redelegate: u64, + claim_rewards: u64, + unbond: u64, + transparent_transfer: u64, + shielded_transfer: u64, + shielding_transfer: u64, + unshielding_transfer: u64, + vote: u64, + ibc: u64, + withdraw: u64, + reveal_pk: u64, + signatures: u64, + tx_size: u64, + ) -> Result { + let (min, max, avg, count) = self + .gas_repo + .find_gas_estimates( + bond, + redelegate, + claim_rewards, + unbond, + transparent_transfer, + shielded_transfer, + shielding_transfer, + unshielding_transfer, + vote, + ibc, + withdraw, + reveal_pk, + signatures, + tx_size, + ) + .await + .map_err(GasError::Database) + .map(|(min, max, avg, count)| { + let min = min.map(|gas| gas as u64); + let max = max.map(|gas| gas as u64); + let avg = avg.map(|gas| gas.to_i64().unwrap() as u64); + let count = count as u64; + (min, max, avg, count) + })?; + + if let (Some(min), Some(max), Some(avg), count) = (min, max, avg, count) + { + Ok(GasEstimate { + min, + max, + avg, + total_estimates: count, + }) + } else { + let gas = self + .gas_repo + .get_gas() + .await + .unwrap_or_default() + .into_iter() + .map(Gas::from) + .fold(HashMap::new(), |mut acc, gas| { + acc.insert(gas.tx_kind, gas.gas_limit); + acc + }); + + let mut estimate = 0; + estimate += bond * gas.get(&TransactionKind::Bond).unwrap(); + estimate += claim_rewards + * gas.get(&TransactionKind::ClaimRewards).unwrap(); + estimate += unbond * gas.get(&TransactionKind::Unbond).unwrap(); + estimate += transparent_transfer + * gas.get(&TransactionKind::TransparentTransfer).unwrap(); + estimate += shielded_transfer + * gas.get(&TransactionKind::ShieldedTransfer).unwrap(); + estimate += shielding_transfer + * gas.get(&TransactionKind::ShieldingTransfer).unwrap(); + estimate += unshielding_transfer + * gas.get(&TransactionKind::UnshieldingTransfer).unwrap(); + estimate += vote * gas.get(&TransactionKind::VoteProposal).unwrap(); + estimate += + ibc * gas.get(&TransactionKind::IbcMsgTransfer).unwrap(); + estimate += withdraw * gas.get(&TransactionKind::Withdraw).unwrap(); + estimate += + reveal_pk * gas.get(&TransactionKind::RevealPk).unwrap(); + + Ok(GasEstimate { + min: estimate, + max: estimate, + avg: estimate, + total_estimates: 0, + }) + } + } } diff --git a/webserver/src/service/ibc.rs b/webserver/src/service/ibc.rs new file mode 100644 index 000000000..4bb3021d3 --- /dev/null +++ b/webserver/src/service/ibc.rs @@ -0,0 +1,44 @@ +use orm::ibc::IbcAckStatusDb; + +use crate::appstate::AppState; +use crate::error::ibc::IbcError; +use crate::repository::ibc::{IbcRepository, IbcRepositoryTrait}; +use crate::response::ibc::{IbcAck, IbcAckStatus}; + +#[derive(Clone)] +pub struct IbcService { + pub ibc_repo: IbcRepository, +} + +impl IbcService { + pub fn new(app_state: AppState) -> Self { + Self { + ibc_repo: IbcRepository::new(app_state), + } + } + + pub async fn get_ack_by_tx_id( + &self, + tx_id: String, + ) -> Result { + self.ibc_repo + .find_ibc_ack(tx_id) + .await + .map_err(IbcError::Database) + .map(|ack| match ack { + Some(ack) => IbcAck { + status: match ack.status { + IbcAckStatusDb::Unknown => IbcAckStatus::Unknown, + IbcAckStatusDb::Timeout => IbcAckStatus::Timeout, + IbcAckStatusDb::Fail => IbcAckStatus::Fail, + IbcAckStatusDb::Success => IbcAckStatus::Success, + }, + timeout: Some(ack.timeout), + }, + None => IbcAck { + status: IbcAckStatus::Unknown, + timeout: None, + }, + }) + } +} diff --git a/webserver/src/service/mod.rs b/webserver/src/service/mod.rs index bc433827f..6e61b6107 100644 --- a/webserver/src/service/mod.rs +++ b/webserver/src/service/mod.rs @@ -1,8 +1,10 @@ pub mod balance; +pub mod block; pub mod chain; pub mod crawler_state; pub mod gas; pub mod governance; +pub mod ibc; pub mod pgf; pub mod pos; pub mod revealed_pk; diff --git a/webserver/src/service/pos.rs b/webserver/src/service/pos.rs index 636dad679..a2c30cbad 100644 --- a/webserver/src/service/pos.rs +++ b/webserver/src/service/pos.rs @@ -102,6 +102,7 @@ impl PosService { &self, address: String, page: u64, + active_at: Option, ) -> Result<(Vec, u64, u64), PoSError> { let pos_state = self .pos_repo @@ -111,7 +112,7 @@ impl PosService { let (db_bonds, total_pages, total_items) = self .pos_repo - .find_bonds_by_address(address, page as i64) + .find_bonds_by_address(address, page as i64, active_at) .await .map_err(PoSError::Database)?; @@ -154,10 +155,11 @@ impl PosService { &self, address: String, page: u64, + active_at: Option, ) -> Result<(Vec, u64, u64), PoSError> { let (db_unbonds, total_pages, total_items) = self .pos_repo - .find_unbonds_by_address(address, page as i64) + .find_unbonds_by_address(address, page as i64, active_at) .await .map_err(PoSError::Database)?; diff --git a/webserver/src/service/transaction.rs b/webserver/src/service/transaction.rs index c67967ff3..75f2cab1c 100644 --- a/webserver/src/service/transaction.rs +++ b/webserver/src/service/transaction.rs @@ -3,7 +3,9 @@ use crate::error::transaction::TransactionError; use crate::repository::tranasaction::{ TransactionRepository, TransactionRepositoryTrait, }; -use crate::response::transaction::{InnerTransaction, WrapperTransaction}; +use crate::response::transaction::{ + InnerTransaction, TransactionHistory, WrapperTransaction, +}; #[derive(Clone)] pub struct TransactionService { @@ -55,4 +57,24 @@ impl TransactionService { Ok(inner_txs.into_iter().map(InnerTransaction::from).collect()) } + + pub async fn get_addresses_history( + &self, + addresses: Vec, + page: u64, + ) -> Result<(Vec, u64, u64), TransactionError> { + let (txs, total_pages, total_items) = self + .transaction_repo + .find_addresses_history(addresses, page as i64) + .await + .map_err(TransactionError::Database)?; + + Ok(( + txs.into_iter() + .map(|(h, t, bh)| TransactionHistory::from(h, t, bh)) + .collect(), + total_pages as u64, + total_items as u64, + )) + } } diff --git a/webserver/src/state/common.rs b/webserver/src/state/common.rs index 7580d706b..01a24bb48 100644 --- a/webserver/src/state/common.rs +++ b/webserver/src/state/common.rs @@ -3,10 +3,12 @@ use namada_sdk::tendermint_rpc::HttpClient; use crate::appstate::AppState; use crate::config::AppConfig; use crate::service::balance::BalanceService; +use crate::service::block::BlockService; use crate::service::chain::ChainService; use crate::service::crawler_state::CrawlerStateService; use crate::service::gas::GasService; use crate::service::governance::GovernanceService; +use crate::service::ibc::IbcService; use crate::service::pgf::PgfService; use crate::service::pos::PosService; use crate::service::revealed_pk::RevealedPkService; @@ -15,6 +17,7 @@ use crate::service::transaction::TransactionService; #[derive(Clone)] pub struct CommonState { pub pos_service: PosService, + pub block_service: BlockService, pub gov_service: GovernanceService, pub balance_service: BalanceService, pub chain_service: ChainService, @@ -23,6 +26,7 @@ pub struct CommonState { pub transaction_service: TransactionService, pub pgf_service: PgfService, pub crawler_state_service: CrawlerStateService, + pub ibc_service: IbcService, pub client: HttpClient, pub config: AppConfig, } @@ -30,6 +34,7 @@ pub struct CommonState { impl CommonState { pub fn new(client: HttpClient, config: AppConfig, data: AppState) -> Self { Self { + block_service: BlockService::new(data.clone()), pos_service: PosService::new(data.clone()), gov_service: GovernanceService::new(data.clone()), balance_service: BalanceService::new(data.clone()), @@ -39,6 +44,7 @@ impl CommonState { pgf_service: PgfService::new(data.clone()), transaction_service: TransactionService::new(data.clone()), crawler_state_service: CrawlerStateService::new(data.clone()), + ibc_service: IbcService::new(data.clone()), client, config, }