Skip to content

Commit

Permalink
feat: wallet grpc get fee for template registration (#6706)
Browse files Browse the repository at this point in the history
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?
---

<!-- Checklist -->
<!-- 1. Is the title of your PR in the form that would make nice release
notes? The title, excluding the conventional commit
tag, will be included exactly as is in the CHANGELOG, so please think
about it carefully. -->


Breaking Changes
---

- [x] None
- [ ] Requires data directory on base node to be deleted
- [ ] Requires hard fork
- [ ] Other - Please specify

<!-- Does this include a breaking change? If so, include this line as a
footer -->
<!-- BREAKING CHANGE: Description what the user should do, e.g. delete a
database, resync the chain -->
  • Loading branch information
ksrichard authored Nov 29, 2024
1 parent b75d533 commit 9aaa364
Show file tree
Hide file tree
Showing 6 changed files with 343 additions and 4 deletions.
7 changes: 7 additions & 0 deletions applications/minotari_app_grpc/proto/wallet.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down Expand Up @@ -290,6 +293,10 @@ message CreateTemplateRegistrationResponse {
bytes template_address = 2;
}

message GetTemplateRegistrationFeeResponse {
uint64 fee = 1;
}

message CancelTransactionRequest {
uint64 tx_id = 1;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ use minotari_app_grpc::tari_rpc::{
GetConnectivityRequest,
GetIdentityRequest,
GetIdentityResponse,
GetTemplateRegistrationFeeResponse,
GetTransactionInfoRequest,
GetTransactionInfoResponse,
GetUnspentAmountsResponse,
Expand Down Expand Up @@ -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<CreateTemplateRegistrationRequest>,
) -> Result<Response<GetTemplateRegistrationFeeResponse>, 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(
Expand Down
31 changes: 31 additions & 0 deletions base_layer/wallet/src/output_manager_service/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ pub enum OutputManagerRequest {
fee_per_gram: MicroMinotari,
lock_height: Option<u64>,
},
GetPayToSelfTransactionFee {
amount: MicroMinotari,
selection_criteria: UtxoSelectionCriteria,
output_features: Box<OutputFeatures>,
fee_per_gram: MicroMinotari,
},
CreatePayToSelfWithOutputs {
outputs: Vec<WalletOutputBuilder>,
fee_per_gram: MicroMinotari,
Expand Down Expand Up @@ -263,6 +269,7 @@ impl fmt::Display for OutputManagerRequest {
),

GetOutputInfoByTxId(t) => write!(f, "GetOutputInfoByTxId: {}", t),
GetPayToSelfTransactionFee { .. } => write!(f, "GetPayToSelfTransactionFee"),
}
}
}
Expand Down Expand Up @@ -290,6 +297,7 @@ pub enum OutputManagerResponse {
OutputConfirmed,
PendingTransactionConfirmed,
PayToSelfTransaction((MicroMinotari, Transaction)),
PayToSelfTransactionFee(MicroMinotari),
TransactionToSend(SenderTransactionProtocol),
TransactionCancelled,
SpentOutputs(Vec<DbWalletOutput>),
Expand Down Expand Up @@ -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<MicroMinotari, OutputManagerError> {
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,
Expand Down
112 changes: 108 additions & 4 deletions base_layer/wallet/src/output_manager_service/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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.
Expand All @@ -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<MicroMinotari, OutputManagerError> {
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,
Expand Down
46 changes: 46 additions & 0 deletions base_layer/wallet/src/transaction_service/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,16 @@ pub enum TransactionServiceRequest {
fee_per_gram: MicroMinotari,
sidechain_deployment_key: Option<PrivateKey>,
},
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<PrivateKey>,
},
SendOneSidedTransaction {
destination: TariAddress,
amount: MicroMinotari,
Expand Down Expand Up @@ -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)
},
}
}
}
Expand Down Expand Up @@ -431,6 +444,9 @@ pub enum TransactionServiceResponse {
tx_id: TxId,
template_address: FixedHash,
},
CodeTemplateRegistrationFeeResponse {
fee: MicroMinotari,
},
}

#[derive(Clone, Debug, Hash, PartialEq, Eq, Default)]
Expand Down Expand Up @@ -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<PrivateKey>,
) -> Result<MicroMinotari, TransactionServiceError> {
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,
Expand Down
Loading

0 comments on commit 9aaa364

Please sign in to comment.