From 9aaa3642ac9949c212d9ebf708413851b09794f7 Mon Sep 17 00:00:00 2001 From: Richard Bertok Date: Fri, 29 Nov 2024 16:41:19 +0100 Subject: [PATCH] feat: wallet grpc get fee for template registration (#6706) Description --- For Tari CLI I needed a new grpc method on wallet to get the calculated fee for a new template registration. Motivation and Context --- How Has This Been Tested? --- What process can a PR reviewer use to test or verify this change? --- Breaking Changes --- - [x] None - [ ] Requires data directory on base node to be deleted - [ ] Requires hard fork - [ ] Other - Please specify --- .../minotari_app_grpc/proto/wallet.proto | 7 ++ .../src/grpc/wallet_grpc_server.rs | 57 +++++++++ .../src/output_manager_service/handle.rs | 31 +++++ .../src/output_manager_service/service.rs | 112 +++++++++++++++++- .../wallet/src/transaction_service/handle.rs | 46 +++++++ .../wallet/src/transaction_service/service.rs | 94 +++++++++++++++ 6 files changed, 343 insertions(+), 4 deletions(-) diff --git a/applications/minotari_app_grpc/proto/wallet.proto b/applications/minotari_app_grpc/proto/wallet.proto index 7ade0301e6..ce4fb17360 100644 --- a/applications/minotari_app_grpc/proto/wallet.proto +++ b/applications/minotari_app_grpc/proto/wallet.proto @@ -80,6 +80,9 @@ service Wallet { rpc StreamTransactionEvents(TransactionEventRequest) returns (stream TransactionEventResponse); rpc RegisterValidatorNode(RegisterValidatorNodeRequest) returns (RegisterValidatorNodeResponse); + + // Get calculated fees for a `CreateTemplateRegistrationRequest` + rpc GetTemplateRegistrationFee(CreateTemplateRegistrationRequest) returns (GetTemplateRegistrationFeeResponse); } message GetVersionRequest {} @@ -290,6 +293,10 @@ message CreateTemplateRegistrationResponse { bytes template_address = 2; } +message GetTemplateRegistrationFeeResponse { + uint64 fee = 1; +} + message CancelTransactionRequest { uint64 tx_id = 1; } diff --git a/applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs b/applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs index 197c6aff98..fcec4fca9d 100644 --- a/applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs +++ b/applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs @@ -55,6 +55,7 @@ use minotari_app_grpc::tari_rpc::{ GetConnectivityRequest, GetIdentityRequest, GetIdentityResponse, + GetTemplateRegistrationFeeResponse, GetTransactionInfoRequest, GetTransactionInfoResponse, GetUnspentAmountsResponse, @@ -1075,6 +1076,62 @@ impl wallet_server::Wallet for WalletGrpcServer { }; Ok(Response::new(response)) } + + /// Returns the fee to register a template. + /// This method is needed by Tari CLI now, so it provides a better UX and tells the user instantly + /// how much a new template registration will cost. + async fn get_template_registration_fee( + &self, + request: Request, + ) -> Result, Status> { + let message = request.into_inner(); + let mut transaction_service = self.wallet.transaction_service.clone(); + let fee_per_gram = message.fee_per_gram.into(); + let fee = transaction_service + .code_template_fee( + message + .template_name + .try_into() + .map_err(|_| Status::invalid_argument("template name is too long"))?, + message + .template_version + .try_into() + .map_err(|_| Status::invalid_argument("template version is too large for a u16"))?, + if let Some(tt) = message.template_type { + tt.try_into() + .map_err(|_| Status::invalid_argument("template type is invalid"))? + } else { + return Err(Status::invalid_argument("template type is missing")); + }, + if let Some(bi) = message.build_info { + bi.try_into() + .map_err(|_| Status::invalid_argument("build info is invalid"))? + } else { + return Err(Status::invalid_argument("build info is missing")); + }, + message + .binary_sha + .try_into() + .map_err(|_| Status::invalid_argument("binary sha is malformed"))?, + message + .binary_url + .try_into() + .map_err(|_| Status::invalid_argument("binary URL is too long"))?, + fee_per_gram, + if message.sidechain_deployment_key.is_empty() { + None + } else { + Some( + RistrettoSecretKey::from_canonical_bytes(&message.sidechain_deployment_key) + .map_err(|_| Status::invalid_argument("sidechain_deployment_key is malformed"))?, + ) + }, + ) + .await + .map_err(|e| Status::internal(e.to_string()))?; + + Ok(Response::new(GetTemplateRegistrationFeeResponse { fee: fee.as_u64() })) + } } async fn handle_completed_tx( diff --git a/base_layer/wallet/src/output_manager_service/handle.rs b/base_layer/wallet/src/output_manager_service/handle.rs index 7f32a2f147..d9521e6546 100644 --- a/base_layer/wallet/src/output_manager_service/handle.rs +++ b/base_layer/wallet/src/output_manager_service/handle.rs @@ -101,6 +101,12 @@ pub enum OutputManagerRequest { fee_per_gram: MicroMinotari, lock_height: Option, }, + GetPayToSelfTransactionFee { + amount: MicroMinotari, + selection_criteria: UtxoSelectionCriteria, + output_features: Box, + fee_per_gram: MicroMinotari, + }, CreatePayToSelfWithOutputs { outputs: Vec, fee_per_gram: MicroMinotari, @@ -263,6 +269,7 @@ impl fmt::Display for OutputManagerRequest { ), GetOutputInfoByTxId(t) => write!(f, "GetOutputInfoByTxId: {}", t), + GetPayToSelfTransactionFee { .. } => write!(f, "GetPayToSelfTransactionFee"), } } } @@ -290,6 +297,7 @@ pub enum OutputManagerResponse { OutputConfirmed, PendingTransactionConfirmed, PayToSelfTransaction((MicroMinotari, Transaction)), + PayToSelfTransactionFee(MicroMinotari), TransactionToSend(SenderTransactionProtocol), TransactionCancelled, SpentOutputs(Vec), @@ -924,6 +932,29 @@ impl OutputManagerHandle { } } + /// Get pay to self transaction fee without locking any UTXOs. + pub async fn pay_to_self_transaction_fee( + &mut self, + amount: MicroMinotari, + utxo_selection: UtxoSelectionCriteria, + output_features: OutputFeatures, + fee_per_gram: MicroMinotari, + ) -> Result { + match self + .handle + .call(OutputManagerRequest::GetPayToSelfTransactionFee { + amount, + selection_criteria: utxo_selection, + output_features: Box::new(output_features), + fee_per_gram, + }) + .await?? + { + OutputManagerResponse::PayToSelfTransactionFee(fee) => Ok(fee), + _ => Err(OutputManagerError::UnexpectedApiResponse), + } + } + pub async fn reinstate_cancelled_inbound_transaction_outputs( &mut self, tx_id: TxId, diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index 3b14cb59a1..b243f5ecf4 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -360,6 +360,15 @@ where ) .await .map(OutputManagerResponse::PayToSelfTransaction), + OutputManagerRequest::GetPayToSelfTransactionFee { + amount, + selection_criteria, + output_features, + fee_per_gram, + } => self + .pay_to_self_transaction_fee(amount, selection_criteria, *output_features, fee_per_gram) + .await + .map(OutputManagerResponse::PayToSelfTransactionFee), OutputManagerRequest::FeeEstimate { amount, selection_criteria, @@ -1533,10 +1542,10 @@ where &aggregated_metadata_ephemeral_public_key_shares, ) .await - .map_err(|e|service_error_with_id(tx_id, e.to_string(), true))? + .map_err(|e| service_error_with_id(tx_id, e.to_string(), true))? .try_build(&self.resources.key_manager) .await - .map_err(|e|service_error_with_id(tx_id, e.to_string(), true))?; + .map_err(|e| service_error_with_id(tx_id, e.to_string(), true))?; let total_metadata_ephemeral_public_key = aggregated_metadata_ephemeral_public_key_shares + output.metadata_signature.ephemeral_pubkey(); trace!(target: LOG_TARGET, "encumber_aggregate_utxo: created output with partial metadata signature"); @@ -1849,10 +1858,10 @@ where &recipient_address, ) .await - .map_err(|e|service_error_with_id(tx_id, e.to_string(), true))? + .map_err(|e| service_error_with_id(tx_id, e.to_string(), true))? .try_build(&self.resources.key_manager) .await - .map_err(|e|service_error_with_id(tx_id, e.to_string(), true))?; + .map_err(|e| service_error_with_id(tx_id, e.to_string(), true))?; // Finalize the partial transaction - it will not be valid at this stage as the metadata and script // signatures are not yet complete. @@ -1877,6 +1886,101 @@ where Ok((tx, amount, fee)) } + /// Returns the transaction fee for a pay to self transaction. + /// If there are not enough funds, we do an estimation with minimal input/output count. + /// This is needed to NOT lock up any UTXOs, just calculate fees without any data modification. + async fn pay_to_self_transaction_fee( + &mut self, + amount: MicroMinotari, + selection_criteria: UtxoSelectionCriteria, + output_features: OutputFeatures, + fee_per_gram: MicroMinotari, + ) -> Result { + let covenant = Covenant::default(); + + let features_and_scripts_byte_size = self + .resources + .consensus_constants + .transaction_weight_params() + .round_up_features_and_scripts_size( + output_features + .get_serialized_size() + .map_err(|e| OutputManagerError::ConversionError(e.to_string()))? + + TariScript::default() + .get_serialized_size() + .map_err(|e| OutputManagerError::ConversionError(e.to_string()))? + + covenant + .get_serialized_size() + .map_err(|e| OutputManagerError::ConversionError(e.to_string()))?, + ); + + let input_selection = match self + .select_utxos( + amount, + selection_criteria, + fee_per_gram, + 1, + features_and_scripts_byte_size, + ) + .await + { + Ok(v) => Ok(v), + Err(OutputManagerError::FundsPending | OutputManagerError::NotEnoughFunds) => { + debug!( + target: LOG_TARGET, + "We dont have enough funds available to make a fee estimate, so we estimate 1 input and 1 change" + ); + let fee_calc = self.get_fee_calc(); + // note that this is the minimal use case for estimation, so at least 1 input and 2 outputs + return Ok(fee_calc.calculate(fee_per_gram, 1, 1, 2, features_and_scripts_byte_size)); + }, + Err(e) => Err(e), + }?; + + // Create builder with no recipients (other than ourselves) + let mut builder = SenderTransactionProtocol::builder( + self.resources.consensus_constants.clone(), + self.resources.key_manager.clone(), + ); + builder + .with_lock_height(0) + .with_fee_per_gram(fee_per_gram) + .with_prevent_fee_gt_amount(self.resources.config.prevent_fee_gt_amount) + .with_kernel_features(KernelFeatures::empty()); + + for kmo in input_selection.iter() { + builder.with_input(kmo.wallet_output.clone()).await?; + } + + let (output, sender_offset_key_id) = self.output_to_self(output_features, amount, covenant).await?; + + builder + .with_output(output.wallet_output.clone(), sender_offset_key_id.clone()) + .await + .map_err(|e| OutputManagerError::BuildError(e.to_string()))?; + + let (change_commitment_mask_key_id, change_script_public_key) = self + .resources + .key_manager + .get_next_commitment_mask_and_script_key() + .await?; + builder.with_change_data( + script!(PushPubKey(Box::new(change_script_public_key.pub_key.clone())))?, + ExecutionStack::default(), + change_script_public_key.key_id.clone(), + change_commitment_mask_key_id.key_id, + Covenant::default(), + self.resources.interactive_tari_address.clone(), + ); + + let stp = builder + .build() + .await + .map_err(|e| OutputManagerError::BuildError(e.message))?; + + Ok(stp.get_fee_amount()?) + } + async fn create_pay_to_self_transaction( &mut self, tx_id: TxId, diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index 3de77deb1d..0044c85d38 100644 --- a/base_layer/wallet/src/transaction_service/handle.rs +++ b/base_layer/wallet/src/transaction_service/handle.rs @@ -150,6 +150,16 @@ pub enum TransactionServiceRequest { fee_per_gram: MicroMinotari, sidechain_deployment_key: Option, }, + GetCodeTemplateFee { + template_name: MaxSizeString<32>, + template_version: u16, + template_type: TemplateType, + build_info: BuildInfo, + binary_sha: FixedHash, + binary_url: MaxSizeString<255>, + fee_per_gram: MicroMinotari, + sidechain_deployment_key: Option, + }, SendOneSidedTransaction { destination: TariAddress, amount: MicroMinotari, @@ -382,6 +392,9 @@ impl fmt::Display for TransactionServiceRequest { TransactionServiceRequest::RegisterCodeTemplate { template_name, .. } => { write!(f, "RegisterCodeTemplate: {}", template_name) }, + TransactionServiceRequest::GetCodeTemplateFee { template_name, .. } => { + write!(f, "GetCodeTemplateFee: {}", template_name) + }, } } } @@ -431,6 +444,9 @@ pub enum TransactionServiceResponse { tx_id: TxId, template_address: FixedHash, }, + CodeTemplateRegistrationFeeResponse { + fee: MicroMinotari, + }, } #[derive(Clone, Debug, Hash, PartialEq, Eq, Default)] @@ -714,6 +730,36 @@ impl TransactionServiceHandle { } } + pub async fn code_template_fee( + &mut self, + template_name: MaxSizeString<32>, + template_version: u16, + template_type: TemplateType, + build_info: BuildInfo, + binary_sha: FixedHash, + binary_url: MaxSizeString<255>, + fee_per_gram: MicroMinotari, + sidechain_deployment_key: Option, + ) -> Result { + match self + .handle + .call(TransactionServiceRequest::GetCodeTemplateFee { + template_name, + template_version, + template_type, + build_info, + binary_sha, + binary_url, + fee_per_gram, + sidechain_deployment_key, + }) + .await?? + { + TransactionServiceResponse::CodeTemplateRegistrationFeeResponse { fee } => Ok(fee), + _ => Err(TransactionServiceError::UnexpectedApiResponse), + } + } + pub async fn send_one_sided_transaction( &mut self, destination: TariAddress, diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index 260cbb75be..0b2d5c8996 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -974,6 +974,31 @@ where self.handle_get_fee_per_gram_stats_per_block_request(count, reply_channel); return Ok(()); }, + TransactionServiceRequest::GetCodeTemplateFee { + template_name, + template_version, + template_type, + build_info, + binary_sha, + binary_url, + fee_per_gram, + sidechain_deployment_key, + } => { + let fee = self + .code_template_fee( + fee_per_gram, + template_name.to_string(), + template_version, + template_type, + build_info, + binary_sha, + binary_url, + sidechain_deployment_key, + UtxoSelectionCriteria::default(), + ) + .await?; + Ok(TransactionServiceResponse::CodeTemplateRegistrationFeeResponse { fee }) + }, }; // If the individual handlers did not already send the API response then do it here. @@ -2442,6 +2467,75 @@ where Ok(()) } + pub async fn code_template_fee( + &mut self, + fee_per_gram: MicroMinotari, + template_name: String, + template_version: u16, + template_type: TemplateType, + build_info: BuildInfo, + binary_sha: FixedHash, + binary_url: MaxSizeString<255>, + sidechain_deployment_key: Option, + selection_criteria: UtxoSelectionCriteria, + ) -> Result { + let author_key = self + .resources + .transaction_key_manager_service + .get_next_key(TransactionKeyManagerBranch::CodeTemplateAuthor.get_branch_key()) + .await?; + let (nonce_secret, nonce_pub) = RistrettoPublicKey::random_keypair(&mut OsRng); + let nonce_id = self + .resources + .transaction_key_manager_service + .import_key(nonce_secret) + .await?; + let (sidechain_id, sidechain_id_knowledge_proof) = match sidechain_deployment_key { + Some(k) => ( + Some(PublicKey::from_secret_key(&k)), + Some( + SchnorrSignature::sign(&k, author_key.pub_key.as_bytes(), &mut OsRng) + .map_err(|e| TransactionServiceError::SidechainSigningError(e.to_string()))?, + ), + ), + None => (None, None), + }; + let mut template_registration = CodeTemplateRegistration { + author_public_key: author_key.pub_key.clone(), + author_signature: Signature::default(), + template_name: template_name + .try_into() + .map_err(|_| TransactionServiceError::InvalidDataError { + field: "template_name".to_string(), + })?, + template_version, + template_type, + build_info, + binary_sha, + binary_url, + sidechain_id, + sidechain_id_knowledge_proof, + }; + let challenge = template_registration.create_challenge(&nonce_pub); + let author_sig = self + .resources + .transaction_key_manager_service + .sign_with_nonce_and_challenge(&author_key.key_id, &nonce_id, &challenge) + .await + .map_err(|e| TransactionServiceError::SidechainSigningError(e.to_string()))?; + + template_registration.author_signature = author_sig; + let output_features = OutputFeatures::for_template_registration(template_registration); + + let fee = self + .resources + .output_manager_service + .pay_to_self_transaction_fee(0.into(), selection_criteria, output_features, fee_per_gram) + .await?; + + Ok(fee) + } + pub async fn register_code_template( &mut self, fee_per_gram: MicroMinotari,