From b3a5e2efecce99ea7f016b22e2a55440cc3915f4 Mon Sep 17 00:00:00 2001 From: Martin Stefcek <35243812+Cifko@users.noreply.github.com> Date: Wed, 24 Jan 2024 09:42:12 +0100 Subject: [PATCH] feat: claim fees ui (#895) Description --- Add claim fees button to the dan wallet ui. Motivation and Context --- How Has This Been Tested? --- I've started dan testing with 1 VN. After I had account I started a second VN with the account public key as a claim fee public key for the 2nd VN. I mined 20 blocks (to get it into the committee). I did couple of transaction and I checked that the VN has some fees to be claimed. Mined another 10 blocks to switch epoch. Pressed the button for claiming fees, filled the VN public key, selected the correct account and claimed the fees. What process can a PR reviewer use to test or verify this change? --- Breaking Changes --- - [x] None - [ ] Requires data directory to be deleted - [ ] Other - Please specify --- .../src/command/validator.rs | 4 + .../src/handlers/validator.rs | 17 ++ .../AssetVault/Components/ActionMenu.tsx | 2 + .../AssetVault/Components/ClaimFees.tsx | 252 ++++++++++++++++++ .../src/utils/json_rpc.tsx | 11 + .../tari_indexer/src/dry_run/error.rs | 4 +- .../tari_indexer/src/dry_run/processor.rs | 50 +++- clients/wallet_daemon_client/src/types.rs | 1 + integration_tests/src/wallet_daemon_cli.rs | 2 + .../tests/steps/wallet_daemon.rs | 4 +- 10 files changed, 340 insertions(+), 7 deletions(-) create mode 100644 applications/tari_dan_wallet_web_ui/src/routes/AssetVault/Components/ClaimFees.tsx diff --git a/applications/tari_dan_wallet_cli/src/command/validator.rs b/applications/tari_dan_wallet_cli/src/command/validator.rs index 9c7d8fe68..ebc0c4c7f 100644 --- a/applications/tari_dan_wallet_cli/src/command/validator.rs +++ b/applications/tari_dan_wallet_cli/src/command/validator.rs @@ -32,6 +32,8 @@ pub struct ClaimFeesArgs { pub epoch: u64, #[clap(long)] pub max_fee: Option, + #[clap(long)] + pub dry_run: bool, } #[derive(Debug, Args, Clone)] @@ -77,6 +79,7 @@ pub async fn handle_claim_validator_fees( validator_public_key, epoch, max_fee, + dry_run, } = args; println!("Submitting claim validator fees transaction..."); @@ -90,6 +93,7 @@ pub async fn handle_claim_validator_fees( validator_public_key: PublicKey::from_canonical_bytes(validator_public_key.into_inner().as_bytes()) .map_err(anyhow::Error::msg)?, epoch: Epoch(epoch), + dry_run, }) .await?; diff --git a/applications/tari_dan_wallet_daemon/src/handlers/validator.rs b/applications/tari_dan_wallet_daemon/src/handlers/validator.rs index 140a81bb8..541030d8c 100644 --- a/applications/tari_dan_wallet_daemon/src/handlers/validator.rs +++ b/applications/tari_dan_wallet_daemon/src/handlers/validator.rs @@ -80,6 +80,23 @@ pub async fn handle_claim_validator_fees( // send the transaction let required_inputs = inputs.into_iter().map(Into::into).collect(); + + if req.dry_run { + let result = sdk + .transaction_api() + .submit_dry_run_transaction(transaction, required_inputs) + .await?; + let execute_result = result.result.into_execute_result().unwrap(); + return Ok(ClaimValidatorFeesResponse { + transaction_id: result.transaction_id, + fee: execute_result + .fee_receipt + .clone() + .map(|fee_receipt| fee_receipt.total_fees_paid) + .unwrap_or_default(), + result: execute_result.finalize, + }); + } let tx_id = sdk .transaction_api() .submit_transaction(transaction, required_inputs) diff --git a/applications/tari_dan_wallet_web_ui/src/routes/AssetVault/Components/ActionMenu.tsx b/applications/tari_dan_wallet_web_ui/src/routes/AssetVault/Components/ActionMenu.tsx index e0185b663..be5d3762d 100644 --- a/applications/tari_dan_wallet_web_ui/src/routes/AssetVault/Components/ActionMenu.tsx +++ b/applications/tari_dan_wallet_web_ui/src/routes/AssetVault/Components/ActionMenu.tsx @@ -27,6 +27,7 @@ import { useAccountsCreateFreeTestCoins } from "../../../api/hooks/useAccounts"; import ClaimBurn from "./ClaimBurn"; import useAccountStore from "../../../store/accountStore"; import SendMoney from "./SendMoney"; +import ClaimFees from "./ClaimFees"; function ActionMenu() { const { mutate } = useAccountsCreateFreeTestCoins(); @@ -50,6 +51,7 @@ function ActionMenu() { }} > + diff --git a/applications/tari_dan_wallet_web_ui/src/routes/AssetVault/Components/ClaimFees.tsx b/applications/tari_dan_wallet_web_ui/src/routes/AssetVault/Components/ClaimFees.tsx new file mode 100644 index 000000000..3f930a3b8 --- /dev/null +++ b/applications/tari_dan_wallet_web_ui/src/routes/AssetVault/Components/ClaimFees.tsx @@ -0,0 +1,252 @@ +// Copyright 2022. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { useState } from "react"; +import { Form } from "react-router-dom"; +import Button from "@mui/material/Button"; +import CheckBox from "@mui/material/Checkbox"; +import TextField from "@mui/material/TextField"; +import Dialog from "@mui/material/Dialog"; +import DialogContent from "@mui/material/DialogContent"; +import DialogTitle from "@mui/material/DialogTitle"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Box from "@mui/material/Box"; +import { useAccountsList, useAccountsTransfer } from "../../../api/hooks/useAccounts"; +import { useTheme } from "@mui/material/styles"; +import useAccountStore from "../../../store/accountStore"; +import { FormControl, InputLabel, MenuItem, Select, SelectChangeEvent } from "@mui/material"; +import { claimFees } from "../../../utils/json_rpc"; +import { useKeysList } from "../../../api/hooks/useKeys"; + +export default function ClaimFees() { + const [open, setOpen] = useState(false); + const [disabled, setDisabled] = useState(false); + const [estimatedFee, setEstimatedFee] = useState(0); + const [claimFeesFormState, setClaimFeesFormState] = useState({ + account: "", + fee: "", + validatorNodePublicKey: "", + epoch: "", + key: "", + }); + + const { data: dataAccountsList } = useAccountsList(0, 10); + const { data: dataKeysList } = useKeysList(); + const { setPopup } = useAccountStore(); + + const theme = useTheme(); + + const isFormFilled = () => { + if (claimFeesFormState.validatorNodePublicKey.length !== 64) { + return false; + } + return ( + claimFeesFormState.account !== "" && + claimFeesFormState.validatorNodePublicKey !== "" && + claimFeesFormState.epoch !== "" + ); + }; + + const is_filled = isFormFilled(); + + const onClaimFeesKeyChange = (e: SelectChangeEvent) => { + let key = e.target.value; + let key_index = dataKeysList.keys.indexOf(key); + let account = claimFeesFormState.account; + let selected_account = dataAccountsList.accounts.find((account: any) => account.account.key_index === key_index); + let new_account_name; + account = selected_account.account.name; + new_account_name = false; + setClaimFeesFormState({ + ...claimFeesFormState, + key: key, + account: account, + }); + }; + + const onEpochChange = (e: React.ChangeEvent) => { + if (/^[0-9]*$/.test(e.target.value)) { + setEstimatedFee(0); + setClaimFeesFormState({ + ...claimFeesFormState, + [e.target.name]: e.target.value, + }); + } + }; + + const onClaimBurnAccountNameChange = (e: React.ChangeEvent) => { + setEstimatedFee(0); + setClaimFeesFormState({ + ...claimFeesFormState, + [e.target.name]: e.target.value, + }); + }; + + const onPublicKeyChange = (e: React.ChangeEvent) => { + if (/^[0-9a-fA-F]*$/.test(e.target.value)) { + setClaimFeesFormState({ + ...claimFeesFormState, + [e.target.name]: e.target.value, + }); + } + setEstimatedFee(0); + }; + + const onClaim = async () => { + if (claimFeesFormState.account) { + setDisabled(true); + claimFees( + claimFeesFormState.account, + 3000, + claimFeesFormState.validatorNodePublicKey, + parseInt(claimFeesFormState.epoch), + estimatedFee == 0, + ) + .then((resp) => { + if (estimatedFee == 0) { + setEstimatedFee(resp.fee); + } else { + setEstimatedFee(0); + setOpen(false); + setPopup({ title: "Claim successful", error: false }); + setClaimFeesFormState({ + account: "", + fee: "", + validatorNodePublicKey: "", + epoch: "", + key: "", + }); + } + }) + .catch((e) => { + setPopup({ title: "Claim failed", error: true, message: e.message }); + }) + .finally(() => { + setDisabled(false); + }); + } + }; + + const handleClickOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + const formattedKey = (key: any[]) => { + let account = dataAccountsList.accounts.find((account: any) => account.account.key_index === +key[0]); + if (account === undefined) { + return null; + return ( +
+ {key[0]} {key[1]} +
+ ); + } + return ( +
+ {key[0]} {key[1]} +

Account {account.account.name} +
+ ); + }; + + return ( +
+ + + Claim Fees + +
+ + Key + + + + + + + + + + + +
+
+
+ ); +} diff --git a/applications/tari_dan_wallet_web_ui/src/utils/json_rpc.tsx b/applications/tari_dan_wallet_web_ui/src/utils/json_rpc.tsx index 97becb55a..0170a938e 100644 --- a/applications/tari_dan_wallet_web_ui/src/utils/json_rpc.tsx +++ b/applications/tari_dan_wallet_web_ui/src/utils/json_rpc.tsx @@ -185,3 +185,14 @@ export const accountNFTsList = (offset: number, limit: number) => jsonRpc("nfts. // settings export const getSettings = () => jsonRpc("settings.get", []); export const setSettings = (settings: any) => jsonRpc("settings.set", settings); + +// validators +export const getFeeSummary = (validatorPublicKey: string, minEpoch: number, maxEpoch: number) => + jsonRpc("validators.get_fee_summary", [validatorPublicKey, minEpoch, maxEpoch]); +export const claimFees = ( + account: string, + maxFee: number, + validatorPublicKey: string, + epoch: number, + isDryRun: boolean, +) => jsonRpc("validators.claim_fees", [account, maxFee, validatorPublicKey, epoch, isDryRun]); diff --git a/applications/tari_indexer/src/dry_run/error.rs b/applications/tari_indexer/src/dry_run/error.rs index 30be9bd2a..0a5877b26 100644 --- a/applications/tari_indexer/src/dry_run/error.rs +++ b/applications/tari_indexer/src/dry_run/error.rs @@ -24,7 +24,7 @@ use tari_dan_app_utilities::transaction_executor::TransactionProcessorError; use tari_dan_common_types::{Epoch, SubstateAddress}; use tari_engine_types::substate::SubstateId; use tari_epoch_manager::EpochManagerError; -use tari_indexer_lib::transaction_autofiller::TransactionAutofillerError; +use tari_indexer_lib::{error::IndexerError, transaction_autofiller::TransactionAutofillerError}; use tari_rpc_framework::RpcStatus; use thiserror::Error; @@ -51,4 +51,6 @@ pub enum DryRunTransactionProcessorError { err_count: usize, committee_size: usize, }, + #[error("Indexer error : {0}")] + IndexerError(#[from] IndexerError), } diff --git a/applications/tari_indexer/src/dry_run/processor.rs b/applications/tari_indexer/src/dry_run/processor.rs index 58631771e..696ba53a6 100644 --- a/applications/tari_indexer/src/dry_run/processor.rs +++ b/applications/tari_indexer/src/dry_run/processor.rs @@ -36,6 +36,7 @@ use tari_dan_engine::{ }; use tari_engine_types::{ commit_result::ExecuteResult, + instruction::Instruction, substate::{Substate, SubstateId}, virtual_substate::{VirtualSubstate, VirtualSubstateId}, }; @@ -64,6 +65,8 @@ pub struct DryRunTransactionProcessor { transaction_autofiller: TransactionAutofiller, TariValidatorNodeRpcClientFactory, TSubstateCache>, template_manager: TemplateManager, + substate_scanner: + Arc, TariValidatorNodeRpcClientFactory, TSubstateCache>>, } impl DryRunTransactionProcessor @@ -77,13 +80,14 @@ where TSubstateCache: SubstateCache + 'static >, template_manager: TemplateManager, ) -> Self { - let transaction_autofiller = TransactionAutofiller::new(substate_scanner); + let transaction_autofiller = TransactionAutofiller::new(substate_scanner.clone()); Self { epoch_manager, client_provider, transaction_autofiller, template_manager, + substate_scanner, } } @@ -106,7 +110,7 @@ where TSubstateCache: SubstateCache + 'static let payload_processor = self.build_payload_processor(&transaction); - let virtual_substates = Self::get_virtual_substates(epoch); + let virtual_substates = self.get_virtual_substates(&transaction, epoch).await?; let mut state_store = new_state_store(); state_store.extend(found_substates); @@ -208,7 +212,11 @@ where TSubstateCache: SubstateCache + 'static }) } - fn get_virtual_substates(epoch: Epoch) -> VirtualSubstates { + async fn get_virtual_substates( + &self, + transaction: &Transaction, + epoch: Epoch, + ) -> Result { let mut virtual_substates = VirtualSubstates::new(); virtual_substates.insert( @@ -216,7 +224,41 @@ where TSubstateCache: SubstateCache + 'static VirtualSubstate::CurrentEpoch(epoch.as_u64()), ); - virtual_substates + let claim_instructions = transaction + .instructions() + .iter() + .chain(transaction.fee_instructions()) + .filter_map(|instruction| { + if let Instruction::ClaimValidatorFees { + epoch, + validator_public_key, + } = instruction + { + Some((Epoch(*epoch), validator_public_key.clone())) + } else { + None + } + }) + .collect::>(); + if !claim_instructions.is_empty() { + for (epoch, public_key) in claim_instructions { + let vn = self + .epoch_manager + .get_validator_node_by_public_key(epoch, &public_key) + .await?; + let address = VirtualSubstateId::UnclaimedValidatorFee { + epoch: epoch.as_u64(), + address: public_key, + }; + let virtual_substate = self + .substate_scanner + .get_virtual_substate_from_committee(address.clone(), vn.shard_key) + .await?; + virtual_substates.insert(address, virtual_substate); + } + } + + Ok(virtual_substates) } } diff --git a/clients/wallet_daemon_client/src/types.rs b/clients/wallet_daemon_client/src/types.rs index c8b933046..9e8c9ee4b 100644 --- a/clients/wallet_daemon_client/src/types.rs +++ b/clients/wallet_daemon_client/src/types.rs @@ -562,6 +562,7 @@ pub struct ClaimValidatorFeesRequest { pub max_fee: Option, pub validator_public_key: PublicKey, pub epoch: Epoch, + pub dry_run: bool, } #[derive(Debug, Clone, Deserialize, Serialize)] diff --git a/integration_tests/src/wallet_daemon_cli.rs b/integration_tests/src/wallet_daemon_cli.rs index 0348efde0..1266c25d3 100644 --- a/integration_tests/src/wallet_daemon_cli.rs +++ b/integration_tests/src/wallet_daemon_cli.rs @@ -105,6 +105,7 @@ pub async fn claim_fees( account_name: String, validator_name: String, epoch: u64, + dry_run: bool, ) -> Result { let mut client = get_auth_wallet_daemon_client(world, &wallet_daemon_name).await; @@ -115,6 +116,7 @@ pub async fn claim_fees( max_fee: None, validator_public_key: vn.public_key.clone(), epoch: Epoch(epoch), + dry_run, }; client.claim_validator_fees(request).await diff --git a/integration_tests/tests/steps/wallet_daemon.rs b/integration_tests/tests/steps/wallet_daemon.rs index cc546cf6c..5af4f032e 100644 --- a/integration_tests/tests/steps/wallet_daemon.rs +++ b/integration_tests/tests/steps/wallet_daemon.rs @@ -110,7 +110,7 @@ async fn when_i_claim_fees_for_validator_and_epoch( account_name: String, wallet_daemon_name: String, ) { - let resp = wallet_daemon_cli::claim_fees(world, wallet_daemon_name, account_name, validator_node, epoch) + let resp = wallet_daemon_cli::claim_fees(world, wallet_daemon_name, account_name, validator_node, epoch, false) .await .unwrap(); resp.result.result.accept().unwrap_or_else(|| { @@ -132,7 +132,7 @@ async fn when_i_claim_fees_for_validator_and_epoch_fails( account_name: String, wallet_daemon_name: String, ) { - let err = wallet_daemon_cli::claim_fees(world, wallet_daemon_name, account_name, validator_node, epoch) + let err = wallet_daemon_cli::claim_fees(world, wallet_daemon_name, account_name, validator_node, epoch, false) .await .unwrap_err();