diff --git a/rs/ethereum/cketh/minter/BUILD.bazel b/rs/ethereum/cketh/minter/BUILD.bazel index a1ff214df31..4793b5f7a8b 100644 --- a/rs/ethereum/cketh/minter/BUILD.bazel +++ b/rs/ethereum/cketh/minter/BUILD.bazel @@ -133,6 +133,7 @@ rust_test( srcs = glob(["src/**/*.rs"]), compile_data = [ "templates/dashboard.html", + "templates/pagination.html", "templates/principal_to_bytes.js", ], crate_features = features, diff --git a/rs/ethereum/cketh/minter/src/dashboard.rs b/rs/ethereum/cketh/minter/src/dashboard.rs index 632f9108529..a6f17d04c7a 100644 --- a/rs/ethereum/cketh/minter/src/dashboard.rs +++ b/rs/ethereum/cketh/minter/src/dashboard.rs @@ -3,6 +3,7 @@ mod tests; use askama::Template; use candid::{Nat, Principal}; +use ic_canisters_http_types::HttpRequest; use ic_cketh_minter::endpoints::{EthTransaction, RetrieveEthStatus}; use ic_cketh_minter::erc20::CkTokenSymbol; use ic_cketh_minter::eth_logs::{EventSource, ReceivedEvent}; @@ -22,6 +23,7 @@ use ic_ethereum_types::Address; use icrc_ledger_types::icrc1::account::Account; use std::cmp::Reverse; use std::collections::{BTreeMap, BTreeSet}; +use std::str::FromStr; mod filters { pub fn timestamp_to_datetime(timestamp: T) -> askama::Result { @@ -145,6 +147,123 @@ impl DashboardPendingDeposit { } } +// Number of entries per page in dashboard tables (e.g. minted events, finalized transactions). +const DEFAULT_PAGE_SIZE: usize = 100; + +#[derive(Default, Clone)] +pub struct DashboardPaginationParameters { + minted_events_start: usize, + finalized_transactions_start: usize, + reimbursed_transactions_start: usize, +} + +impl DashboardPaginationParameters { + pub(crate) fn from_query_params( + req: &HttpRequest, + ) -> Result { + fn parse_query_param(req: &HttpRequest, param_name: &str) -> Result { + Ok(match req.raw_query_param(param_name) { + Some(arg) => usize::from_str(arg) + .map_err(|_| format!("failed to parse the '{}' parameter", param_name))?, + None => 0, + }) + } + + Ok(Self { + minted_events_start: parse_query_param(req, "minted_events_start")?, + finalized_transactions_start: parse_query_param(req, "finalized_transactions_start")?, + reimbursed_transactions_start: parse_query_param(req, "reimbursed_transactions_start")?, + }) + } +} + +#[derive(Clone)] +pub struct DashboardPaginatedTable { + current_page: Vec, + pagination: DashboardTablePagination, +} + +impl DashboardPaginatedTable { + pub fn from_items( + items: &[T], + current_page_offset: usize, + page_size: usize, + num_cols: usize, + table_reference: &str, + page_offset_query_param: &str, + ) -> Self { + Self { + current_page: items + .iter() + .skip(current_page_offset) + .take(page_size) + .cloned() + .collect(), + pagination: DashboardTablePagination::pagination( + items.len(), + current_page_offset, + page_size, + num_cols, + table_reference, + page_offset_query_param, + ), + } + } + + pub fn is_empty(&self) -> bool { + self.current_page.is_empty() + } + + pub fn has_more_than_one_page(&self) -> bool { + self.pagination.pages.len() > 1 + } +} + +#[derive(Clone)] +pub struct DashboardTablePage { + index: usize, + offset: usize, +} + +#[derive(Template)] +#[template(path = "pagination.html")] +#[derive(Clone)] +pub struct DashboardTablePagination { + pub table_id: String, + pub table_width: usize, + pub page_offset_query_param: String, + pub current_page_index: usize, + pub pages: Vec, +} + +impl DashboardTablePagination { + fn pagination( + num_items: usize, + current_offset: usize, + page_size: usize, + table_width: usize, + table_reference: &str, + page_offset_query_param: &str, + ) -> Self { + let pages = (0..num_items) + .step_by(page_size) + .enumerate() + .map(|(index, offset)| DashboardTablePage { + index: index + 1, + offset, + }) + .collect(); + let current_page_index = current_offset / page_size + 1; + Self { + table_id: String::from(table_reference), + page_offset_query_param: String::from(page_offset_query_param), + table_width, + current_page_index, + pages, + } + } +} + #[derive(Template)] #[template(path = "dashboard.html")] #[derive(Clone)] @@ -158,26 +277,35 @@ pub struct DashboardTemplate { pub first_synced_block: BlockNumber, pub last_observed_block: Option, pub cketh_ledger_id: Principal, - pub minted_events: Vec, + pub minted_events_table: DashboardPaginatedTable, pub pending_deposits: Vec, pub invalid_events: BTreeMap, pub withdrawal_requests: Vec, pub pending_transactions: Vec, - pub finalized_transactions: Vec, - pub reimbursed_transactions: Vec, + pub finalized_transactions_table: DashboardPaginatedTable, + pub reimbursed_transactions_table: DashboardPaginatedTable, pub eth_balance: EthBalance, pub skipped_blocks: BTreeMap>, pub supported_ckerc20_tokens: Vec, } impl DashboardTemplate { - pub fn from_state(state: &State) -> Self { + pub fn from_state(state: &State, pagination_parameters: DashboardPaginationParameters) -> Self { let mut minted_events: Vec<_> = state.minted_events.values().cloned().collect(); minted_events.sort_unstable_by_key(|event| { let deposit_event = &event.deposit_event; Reverse((deposit_event.block_number(), deposit_event.log_index())) }); + let minted_events_table = DashboardPaginatedTable::from_items( + &minted_events, + pagination_parameters.minted_events_start, + DEFAULT_PAGE_SIZE, + 7, + "minted-events", + "minted_events_start", + ); + let mut supported_ckerc20_tokens: Vec<_> = state .supported_ck_erc20_tokens() .map(|ckerc20| DashboardCkErc20Token { @@ -279,6 +407,15 @@ impl DashboardTemplate { .collect(); finalized_transactions.sort_unstable_by_key(|tx| Reverse(tx.ledger_burn_index)); + let finalized_transactions_table = DashboardPaginatedTable::from_items( + &finalized_transactions, + pagination_parameters.finalized_transactions_start, + DEFAULT_PAGE_SIZE, + 8, + "finalized-transactions", + "finalized_transactions_start", + ); + let mut reimbursed_transactions: Vec<_> = state .eth_transactions .reimbursed_transactions_iter() @@ -321,6 +458,15 @@ impl DashboardTemplate { reimbursed_transactions .sort_unstable_by_key(|reimbursed_tx| Reverse(reimbursed_tx.cketh_ledger_burn_index())); + let reimbursed_transactions_table = DashboardPaginatedTable::from_items( + &reimbursed_transactions, + pagination_parameters.reimbursed_transactions_start, + DEFAULT_PAGE_SIZE, + 6, + "reimbursed-transactions", + "reimbursed_transactions_start", + ); + DashboardTemplate { ethereum_network: state.ethereum_network, ecdsa_key_name: state.ecdsa_key_name.clone(), @@ -334,13 +480,13 @@ impl DashboardTemplate { minimum_withdrawal_amount: state.cketh_minimum_withdrawal_amount, first_synced_block: state.first_scraped_block_number, last_observed_block: state.last_observed_block_number, - minted_events, + minted_events_table, pending_deposits, invalid_events: state.invalid_events.clone(), withdrawal_requests, pending_transactions, - finalized_transactions, - reimbursed_transactions, + finalized_transactions_table, + reimbursed_transactions_table, eth_balance: state.eth_balance.clone(), skipped_blocks: state .skipped_blocks diff --git a/rs/ethereum/cketh/minter/src/dashboard/tests.rs b/rs/ethereum/cketh/minter/src/dashboard/tests.rs index c69193e6c8e..6826161f9cc 100644 --- a/rs/ethereum/cketh/minter/src/dashboard/tests.rs +++ b/rs/ethereum/cketh/minter/src/dashboard/tests.rs @@ -1,5 +1,5 @@ use crate::dashboard::tests::assertions::DashboardAssert; -use crate::dashboard::DashboardTemplate; +use crate::dashboard::{DashboardPaginationParameters, DashboardTemplate}; use crate::erc20::CkErc20Token; use candid::{Nat, Principal}; use ic_cketh_minter::eth_logs::{ @@ -17,7 +17,7 @@ use ic_cketh_minter::state::transactions::{ create_transaction, Erc20WithdrawalRequest, EthWithdrawalRequest, ReimbursementIndex, WithdrawalRequest, }; -use ic_cketh_minter::state::State; +use ic_cketh_minter::state::{MintedEvent, State}; use ic_cketh_minter::tx::{ Eip1559Signature, Eip1559TransactionRequest, GasFeeEstimate, SignedEip1559TransactionRequest, TransactionPrice, @@ -164,7 +164,7 @@ fn should_display_supported_erc20_tokens() { state.ethereum_network = EthereumNetwork::Mainnet; state.record_add_ckerc20_token(usdc.clone()); state.record_add_ckerc20_token(usdt.clone()); - DashboardTemplate::from_state(&state) + DashboardTemplate::from_state(&state, DashboardPaginationParameters::default()) }; DashboardAssert::assert_that(dashboard) @@ -213,7 +213,7 @@ fn should_display_pending_deposits_sorted_by_decreasing_block_number() { apply_state_transition(&mut state, &EventType::AcceptedDeposit(event_1)); apply_state_transition(&mut state, &EventType::AcceptedDeposit(event_2)); apply_state_transition(&mut state, &EventType::AcceptedErc20Deposit(event_3)); - DashboardTemplate::from_state(&state) + DashboardTemplate::from_state(&state, DashboardPaginationParameters::default()) }; DashboardAssert::assert_that(dashboard) @@ -311,7 +311,7 @@ fn should_display_minted_events_sorted_by_decreasing_mint_block_index() { mint_block_index: LedgerMintIndex::new(44), }, ); - DashboardTemplate::from_state(&state) + DashboardTemplate::from_state(&state, DashboardPaginationParameters::default()) }; DashboardAssert::assert_that(dashboard) @@ -386,7 +386,7 @@ fn should_display_rejected_deposits() { reason: "failed to decode principal".to_string(), }, ); - DashboardTemplate::from_state(&state) + DashboardTemplate::from_state(&state, DashboardPaginationParameters::default()) }; DashboardAssert::assert_that(dashboard) @@ -423,7 +423,7 @@ fn should_display_correct_cketh_token_symbol_based_on_network() { LedgerBurnIndex::new(15), )), ); - DashboardTemplate::from_state(&state) + DashboardTemplate::from_state(&state, DashboardPaginationParameters::default()) }; DashboardAssert::assert_that(dashboard).has_withdrawal_requests( 1, @@ -467,7 +467,7 @@ fn should_display_withdrawal_requests_sorted_by_decreasing_cketh_ledger_burn_ind ..cketh_withdrawal_request_with_index(LedgerBurnIndex::new(17)) }), ); - DashboardTemplate::from_state(&state) + DashboardTemplate::from_state(&state, DashboardPaginationParameters::default()) }; DashboardAssert::assert_that(dashboard) @@ -567,7 +567,7 @@ fn should_display_pending_transactions_sorted_by_decreasing_cketh_ledger_burn_in ); } - DashboardTemplate::from_state(&state) + DashboardTemplate::from_state(&state, DashboardPaginationParameters::default()) }; DashboardAssert::assert_that(dashboard) @@ -688,7 +688,7 @@ fn should_display_finalized_transactions_sorted_by_decreasing_cketh_ledger_burn_ ); } - DashboardTemplate::from_state(&state) + DashboardTemplate::from_state(&state, DashboardPaginationParameters::default()) }; DashboardAssert::assert_that(dashboard) @@ -895,7 +895,7 @@ fn should_display_reimbursed_requests() { } } } - DashboardTemplate::from_state(&state) + DashboardTemplate::from_state(&state, DashboardPaginationParameters::default()) }; // Check that we show latest first. @@ -1001,8 +1001,134 @@ fn should_display_reimbursed_requests() { ); } +#[test] +fn should_display_minted_events_pagination() { + let dashboard = { + let mut state = initial_state(); + add_minted_events(&mut state, 300); + let paging_parameters = DashboardPaginationParameters { + minted_events_start: 100, // Second page. + ..DashboardPaginationParameters::default() + }; + DashboardTemplate::from_state(&state, paging_parameters) + }; + + // Events are displayed in order of decreasing log index. Page 2 should therefore have events 200 to 101. + DashboardAssert::assert_that(dashboard) + .has_minted_events_with_log_index(1, "200") + .has_minted_events_with_log_index(100, "101") + .has_minted_events_last_row_text(&vec!["Pages:", "1", "2", "3"]) + .has_minted_events_last_row_links(&vec![ + "?minted_events_start=0#minted-events", + "?minted_events_start=200#minted-events", + ]); +} + +#[test] +fn should_not_display_minted_events_pagination() { + let dashboard = { + let mut state = initial_state(); + add_minted_events(&mut state, 75); // less than 1 full page + DashboardTemplate::from_state(&state, DashboardPaginationParameters::default()) + }; + + DashboardAssert::assert_that(dashboard).has_minted_events_last_row_text(&vec![ + "0xf1ac37d920fa57d9caeebc7136fea591191250309ffca95ae0e8a7739de89cc2", + "1", + "0xdd2851Cdd40aE6536831558DD46db62fAc7A844d", + "ckETH", + "10_000_000_000_000_000", + "k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae", + "1", + ]); +} + +#[test] +fn should_not_display_finalized_transactions_pagination() { + let dashboard = { + let mut state = initial_state(); + add_finalized_transactions(&mut state, 75); // less than 1 full page + DashboardTemplate::from_state(&state, DashboardPaginationParameters::default()) + }; + + DashboardAssert::assert_that(dashboard).has_finalized_transactions_last_row_text(&vec![ + "1", + "0xb44B5e756A894775FC32EDdf3314Bb1B1944dC34", + "ckSepoliaETH", + "1_058_000_000_000_000", + "21_000_000_000_000", + "4190269", + "0xdea6b45f0978fea7f38fe6957db7ee11dd0e351a6f24fe54598d8aec9c8a1527", + "Success", + ]); +} + +#[test] +fn should_display_finalized_transactions_pagination() { + let dashboard = { + let mut state = initial_state(); + add_finalized_transactions(&mut state, 300); + let paging_parameters = DashboardPaginationParameters { + finalized_transactions_start: 100, // Second page. + ..DashboardPaginationParameters::default() + }; + DashboardTemplate::from_state(&state, paging_parameters) + }; + + // Transactions are displayed in order of decreasing ledger burn index. Page 2 should therefore have transactions 200 to 101. + DashboardAssert::assert_that(dashboard) + .has_finalized_transactions_with_ledger_burn_index(1, "200") + .has_finalized_transactions_with_ledger_burn_index(100, "101") + .has_finalized_transactions_last_row_text(&vec!["Pages:", "1", "2", "3"]) + .has_finalized_transactions_last_row_links(&vec![ + "?finalized_transactions_start=0#finalized-transactions", + "?finalized_transactions_start=200#finalized-transactions", + ]); +} + +#[test] +fn should_not_display_reimbursed_transactions_pagination() { + let dashboard = { + let mut state = initial_state(); + add_reimbursed_transactions(&mut state, 75); // less than 1 full page + DashboardTemplate::from_state(&state, DashboardPaginationParameters::default()) + }; + + DashboardAssert::assert_that(dashboard).has_reimbursed_transactions_last_row_text(&vec![ + "1", + "123", + "ckSepoliaETH", + "1_058_000_000_000_000", + "0xdea6b45f0978fea7f38fe6957db7ee11dd0e351a6f24fe54598d8aec9c8a1527", + "Reimbursed", + ]); +} + +#[test] +fn should_display_reimbursed_transactions_pagination() { + let dashboard = { + let mut state = initial_state(); + add_reimbursed_transactions(&mut state, 300); + let paging_parameters = DashboardPaginationParameters { + reimbursed_transactions_start: 100, // Second page. + ..DashboardPaginationParameters::default() + }; + DashboardTemplate::from_state(&state, paging_parameters) + }; + + // Transactions are displayed in order of decreasing ledger burn index. Page 2 should therefore have transactions 200 to 101. + DashboardAssert::assert_that(dashboard) + .has_reimbursed_transactions_with_ledger_burn_index(1, "200") + .has_reimbursed_transactions_with_ledger_burn_index(100, "101") + .has_reimbursed_transactions_last_row_text(&vec!["Pages:", "1", "2", "3"]) + .has_reimbursed_transactions_last_row_links(&vec![ + "?reimbursed_transactions_start=0#reimbursed-transactions", + "?reimbursed_transactions_start=200#reimbursed-transactions", + ]); +} + fn initial_dashboard() -> DashboardTemplate { - DashboardTemplate::from_state(&initial_state()) + DashboardTemplate::from_state(&initial_state(), DashboardPaginationParameters::default()) } const INITIAL_LAST_SCRAPED_BLOCK_NUMBER: u32 = 3_956_206_u32; @@ -1030,6 +1156,142 @@ fn initial_state_with_usdc_support() -> State { state } +fn add_minted_events(state: &mut State, num_events: u128) { + (1..=num_events).for_each(|index| { + state + .minted_events + .insert(event_source(index), minted_event(index)); + }); +} + +fn minted_event(index: u128) -> MintedEvent { + MintedEvent { + deposit_event: ReceivedEthEvent { + log_index: LogIndex::new(index), + ..received_eth_event() + } + .into(), + mint_block_index: LedgerMintIndex::new(1u64), + token_symbol: "ckETH".to_string(), + erc20_contract_address: None, + } +} + +fn event_source(index: u128) -> EventSource { + EventSource { + transaction_hash: "0x05c6ec45699c9a6a4b1a4ea2058b0cee852ea2f19b18fb8313c04bf8156efde4" + .parse() + .unwrap(), + log_index: LogIndex::new(index), + } +} + +fn add_finalized_transactions(state: &mut State, num_transactions: u64) { + let deposit = ReceivedEthEvent { + //enough for withdrawals + value: Wei::from(1_000_000_000_000_000_000_u128), + ..received_eth_event() + }; + apply_state_transition(state, &EventType::AcceptedDeposit(deposit.clone())); + apply_state_transition( + state, + &EventType::MintedCkEth { + event_source: deposit.source(), + mint_block_index: LedgerMintIndex::new(42), + }, + ); + for index in 0..num_transactions { + let (req, tx, signed_tx, receipt) = cketh_withdrawal_flow( + LedgerBurnIndex::new(index + 1), + TransactionNonce::from(index), + TransactionStatus::Success, + ); + let id = req.cketh_ledger_burn_index(); + apply_state_transition(state, &req.into_accepted_withdrawal_request_event()); + apply_state_transition( + state, + &EventType::CreatedTransaction { + withdrawal_id: id, + transaction: tx, + }, + ); + apply_state_transition( + state, + &EventType::SignedTransaction { + withdrawal_id: id, + transaction: signed_tx, + }, + ); + apply_state_transition( + state, + &EventType::FinalizedTransaction { + withdrawal_id: id, + transaction_receipt: receipt, + }, + ); + } +} + +fn add_reimbursed_transactions(state: &mut State, num_transactions: u64) { + use ic_cketh_minter::state::transactions::Reimbursed; + + let reimbursed_in_block = LedgerMintIndex::new(123); + let reimbursed_amount = CkTokenAmount::new(100_102); + + let deposit = ReceivedEthEvent { + //enough for withdrawals + value: Wei::from(1_000_000_000_000_000_000_u128), + ..received_eth_event() + }; + apply_state_transition(state, &EventType::AcceptedDeposit(deposit.clone())); + apply_state_transition( + state, + &EventType::MintedCkEth { + event_source: deposit.source(), + mint_block_index: LedgerMintIndex::new(42), + }, + ); + for index in 0..num_transactions { + let (req, tx, signed_tx, receipt) = cketh_withdrawal_flow( + LedgerBurnIndex::new(index + 1), + TransactionNonce::from(index), + TransactionStatus::Failure, + ); + let id = req.cketh_ledger_burn_index(); + apply_state_transition(state, &req.into_accepted_withdrawal_request_event()); + apply_state_transition( + state, + &EventType::CreatedTransaction { + withdrawal_id: id, + transaction: tx, + }, + ); + apply_state_transition( + state, + &EventType::SignedTransaction { + withdrawal_id: id, + transaction: signed_tx, + }, + ); + apply_state_transition( + state, + &EventType::FinalizedTransaction { + withdrawal_id: id, + transaction_receipt: receipt.clone(), + }, + ); + apply_state_transition( + state, + &EventType::ReimbursedEthWithdrawal(Reimbursed { + transaction_hash: Some(receipt.transaction_hash), + burn_in_block: id, + reimbursed_in_block, + reimbursed_amount, + }), + ); + } +} + fn received_eth_event() -> ReceivedEthEvent { ReceivedEthEvent { transaction_hash: "0xf1ac37d920fa57d9caeebc7136fea591191250309ffca95ae0e8a7739de89cc2" @@ -1483,6 +1745,35 @@ mod assertions { ) } + pub fn has_minted_events_with_log_index( + &self, + row_index: u8, + expected_value: &str, + ) -> &Self { + self.has_table_row_string_value_in_column( + &format!("#minted-events + table > tbody > tr:nth-child({row_index})"), + 1, + expected_value, + "minted-events", + ) + } + + pub fn has_minted_events_last_row_text(&self, expected_value: &Vec<&str>) -> &Self { + self.has_table_row_string_value( + "#minted-events + table > tbody > tr:last-child", + expected_value, + "minted-events", + ) + } + + pub fn has_minted_events_last_row_links(&self, expected_value: &Vec<&str>) -> &Self { + self.has_table_row_links( + "#minted-events + table > tbody > tr:last-child", + expected_value, + "minted-events", + ) + } + pub fn has_rejected_deposits(&self, row_index: u8, expected_value: &Vec<&str>) -> &Self { self.has_table_row_string_value( &format!("#rejected-deposits + table > tbody > tr:nth-child({row_index})"), @@ -1519,6 +1810,41 @@ mod assertions { ) } + pub fn has_finalized_transactions_with_ledger_burn_index( + &self, + row_index: u8, + expected_value: &str, + ) -> &Self { + self.has_table_row_string_value_in_column( + &format!("#finalized-transactions + table > tbody > tr:nth-child({row_index})"), + 0, + expected_value, + "finalized-transactions", + ) + } + + pub fn has_finalized_transactions_last_row_text( + &self, + expected_value: &Vec<&str>, + ) -> &Self { + self.has_table_row_string_value( + "#finalized-transactions + table > tbody > tr:last-child", + expected_value, + "finalized-transactions", + ) + } + + pub fn has_finalized_transactions_last_row_links( + &self, + expected_value: &Vec<&str>, + ) -> &Self { + self.has_table_row_links( + "#finalized-transactions + table > tbody > tr:last-child", + expected_value, + "finalized-transactions", + ) + } + pub fn has_reimbursed_transactions( &self, row_index: u8, @@ -1531,6 +1857,41 @@ mod assertions { ) } + pub fn has_reimbursed_transactions_with_ledger_burn_index( + &self, + row_index: u8, + expected_value: &str, + ) -> &Self { + self.has_table_row_string_value_in_column( + &format!("#reimbursed-transactions + table > tbody > tr:nth-child({row_index})"), + 0, + expected_value, + "reimbursed-transactions", + ) + } + + pub fn has_reimbursed_transactions_last_row_text( + &self, + expected_value: &Vec<&str>, + ) -> &Self { + self.has_table_row_string_value( + "#reimbursed-transactions + table > tbody > tr:last-child", + expected_value, + "reimbursed-transactions", + ) + } + + pub fn has_reimbursed_transactions_last_row_links( + &self, + expected_value: &Vec<&str>, + ) -> &Self { + self.has_table_row_links( + "#reimbursed-transactions + table > tbody > tr:last-child", + expected_value, + "reimbursed-transactions", + ) + } + fn has_table_row_string_value( &self, selector: &str, @@ -1551,6 +1912,50 @@ mod assertions { self } + fn has_table_row_string_value_in_column( + &self, + selector: &str, + column_index: usize, + expected_value: &str, + error_msg: &str, + ) -> &Self { + let actual_value = self.select_only_one(selector); + let column_values = actual_value + .text() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .collect::>(); + assert_eq!( + column_values + .get(column_index) + .expect("column index out of bounds"), + &expected_value, + "{}. Rendered html: {}", + error_msg, + self.rendered_html + ); + self + } + + fn has_table_row_links( + &self, + selector: &str, + expected_value: &Vec<&str>, + error_msg: &str, + ) -> &Self { + let links = self + .select_only_one(selector) + .select(&Selector::parse("a").unwrap()) + .map(|link| link.value().attr("href").expect("href not found")) + .collect::>(); + assert_eq!( + &links, expected_value, + "{}. Rendered html: {}", + error_msg, self.rendered_html + ); + self + } + fn has_string_value(&self, selector: &str, expected_value: &str, error_msg: &str) -> &Self { let actual_value = self.select_only_one(selector); let string_value = actual_value.text().collect::(); diff --git a/rs/ethereum/cketh/minter/src/main.rs b/rs/ethereum/cketh/minter/src/main.rs index 32bb99986f9..c50853dfb72 100644 --- a/rs/ethereum/cketh/minter/src/main.rs +++ b/rs/ethereum/cketh/minter/src/main.rs @@ -1,4 +1,6 @@ +use crate::dashboard::DashboardPaginationParameters; use candid::Nat; +use dashboard::DashboardTemplate; use ic_canister_log::log; use ic_canisters_http_types::{HttpRequest, HttpResponse, HttpResponseBuilder}; use ic_cdk_macros::{init, post_upgrade, pre_upgrade, query, update}; @@ -1032,7 +1034,16 @@ fn http_request(req: HttpRequest) -> HttpResponse { } } else if req.path() == "/dashboard" { use askama::Template; - let dashboard = read_state(dashboard::DashboardTemplate::from_state); + + let paging_parameters = match DashboardPaginationParameters::from_query_params(&req) { + Ok(args) => args, + Err(error) => { + return HttpResponseBuilder::bad_request() + .with_body_and_content_length(error) + .build() + } + }; + let dashboard = read_state(|state| DashboardTemplate::from_state(state, paging_parameters)); HttpResponseBuilder::ok() .header("Content-Type", "text/html; charset=utf-8") .with_body_and_content_length(dashboard.render().unwrap()) diff --git a/rs/ethereum/cketh/minter/templates/dashboard.html b/rs/ethereum/cketh/minter/templates/dashboard.html index 3a9c8df3abb..5ec2ca14864 100644 --- a/rs/ethereum/cketh/minter/templates/dashboard.html +++ b/rs/ethereum/cketh/minter/templates/dashboard.html @@ -254,7 +254,7 @@

Pending deposit events

{% endif %} - {% if !minted_events.is_empty() %} + {% if !minted_events_table.is_empty() %}

Minted events

@@ -269,7 +269,7 @@

Minted events

- {% for event in minted_events %} + {% for event in minted_events_table.current_page %} @@ -280,6 +280,7 @@

Minted events

{% endfor %} + {% if minted_events_table.has_more_than_one_page() %}{{ minted_events_table.pagination|safe }}{% endif %}
{% call etherscan_tx_link(event.deposit_event.transaction_hash().to_string()) %} {{ event.deposit_event.log_index() }} {{ event.mint_block_index }}
{% endif %} @@ -358,7 +359,7 @@

Pending Transactions ckETH → ETH and ckERC20 → {% endif %} - {% if !finalized_transactions.is_empty() %} + {% if !finalized_transactions_table.is_empty() %}

Finalized Transactions ckETH → ETH and ckERC20 → ERC20

@@ -374,7 +375,7 @@

Finalized Transactions ckETH → ETH and ckERC20

- {% for tx in finalized_transactions %} + {% for tx in finalized_transactions_table.current_page %} @@ -386,11 +387,12 @@

Finalized Transactions ckETH → ETH and ckERC20

{% endfor %} + {% if finalized_transactions_table.has_more_than_one_page() %}{{ finalized_transactions_table.pagination|safe }}{% endif %}
{{ tx.ledger_burn_index }} {% call etherscan_address_link(tx.destination) %}{{ tx.status }}
{% endif %} - {% if !reimbursed_transactions.is_empty() %} + {% if !reimbursed_transactions_table.is_empty() %}

Reimbursed Transactions

@@ -404,7 +406,7 @@

Reimbursed Transactions

- {% for r in reimbursed_transactions %} + {% for r in reimbursed_transactions_table.current_page %} {% match r %} {% when DashboardReimbursedTransaction::Reimbursed with {cketh_ledger_burn_index, reimbursed_in_block, reimbursed_amount, token_symbol, transaction_hash} %} @@ -426,6 +428,7 @@

Reimbursed Transactions

{% endmatch %} {% endfor %} + {% if reimbursed_transactions_table.has_more_than_one_page() %}{{ reimbursed_transactions_table.pagination|safe }}{% endif %}
{% endif %} diff --git a/rs/ethereum/cketh/minter/templates/pagination.html b/rs/ethereum/cketh/minter/templates/pagination.html new file mode 100644 index 00000000000..3f9fe543fc7 --- /dev/null +++ b/rs/ethereum/cketh/minter/templates/pagination.html @@ -0,0 +1,12 @@ + + + Pages: + {% for page in pages %} + {% if page.index == current_page_index %} + {{ page.index }}  + {% else %} + {{ page.index }}  + {% endif %} + {% endfor %} + +