use packable::PackableExt;
use crate::{
client::{
api::{types::PreparedTransactionData, ClientBlockBuilder},
secret::{types::InputSigningData, SecretManageExt},
Error, Result,
},
types::block::{
input::{Input, UtxoInput},
output::{InputsCommitment, Output, OutputId},
payload::{
transaction::{RegularTransactionEssence, TransactionEssence, TransactionPayload},
Payload, TaggedDataPayload,
},
semantic::{semantic_validation, ConflictReason, ValidationContext},
signature::Ed25519Signature,
Block, BlockId,
},
};
const MAX_TX_LENGTH_FOR_BLOCK_WITH_8_PARENTS: usize = Block::LENGTH_MAX - Block::LENGTH_MIN - (7 * BlockId::LENGTH);
const SINGLE_UNLOCK_LENGTH: usize = 1 + 1 + Ed25519Signature::PUBLIC_KEY_LENGTH + Ed25519Signature::SIGNATURE_LENGTH;
const REFERENCE_ALIAS_NFT_UNLOCK_LENGTH: usize = 1 + 2;
impl<'a> ClientBlockBuilder<'a> {
pub async fn prepare_transaction(&self) -> Result<PreparedTransactionData> {
log::debug!("[prepare_transaction]");
let protocol_parameters = self.client.get_protocol_parameters().await?;
let token_supply = self.client.get_token_supply().await?;
for output in &self.outputs {
output.verify_storage_deposit(protocol_parameters.rent_structure().clone(), token_supply)?;
}
let selected_transaction_data = if self.inputs.is_some() {
self.get_custom_inputs(&protocol_parameters, self.burn.clone()).await?
} else {
self.get_inputs(&protocol_parameters).await?
};
let inputs_commitment = InputsCommitment::new(selected_transaction_data.inputs.iter().map(|i| &i.output));
let mut essence = RegularTransactionEssence::builder(self.client.get_network_id().await?, inputs_commitment);
let inputs = selected_transaction_data
.inputs
.iter()
.map(|i| {
Ok(Input::Utxo(UtxoInput::new(
*i.output_metadata.transaction_id(),
i.output_metadata.output_index(),
)?))
})
.collect::<Result<Vec<Input>>>()?;
essence = essence.with_inputs(inputs);
essence = essence.with_outputs(selected_transaction_data.outputs);
if let Some(index) = self.tag.clone() {
let tagged_data_payload = TaggedDataPayload::new(index.to_vec(), self.data.clone().unwrap_or_default())?;
essence = essence.with_payload(Payload::from(tagged_data_payload));
}
let regular_essence = essence.finish(&self.client.get_protocol_parameters().await?)?;
validate_regular_transaction_essence_length(®ular_essence)?;
let essence = TransactionEssence::Regular(regular_essence);
Ok(PreparedTransactionData {
essence,
inputs_data: selected_transaction_data.inputs,
remainder: selected_transaction_data.remainder,
})
}
pub async fn sign_transaction(&self, prepared_transaction_data: PreparedTransactionData) -> Result<Payload> {
log::debug!("[sign_transaction] {:?}", prepared_transaction_data);
let secret_manager = self.secret_manager.ok_or(Error::MissingParameter("secret manager"))?;
let current_time = self.client.get_time_checked().await?;
let unlocks = secret_manager
.sign_transaction_essence(&prepared_transaction_data, Some(current_time))
.await?;
let tx_payload = TransactionPayload::new(prepared_transaction_data.essence.clone(), unlocks)?;
validate_transaction_payload_length(&tx_payload)?;
let conflict = verify_semantic(&prepared_transaction_data.inputs_data, &tx_payload, current_time)?;
if conflict != ConflictReason::None {
log::debug!("[sign_transaction] conflict: {conflict:?} for {:#?}", tx_payload);
return Err(Error::TransactionSemantic(conflict));
}
Ok(Payload::from(tx_payload))
}
}
pub fn verify_semantic(
input_signing_data: &[InputSigningData],
transaction: &TransactionPayload,
current_time: u32,
) -> crate::client::Result<ConflictReason> {
let transaction_id = transaction.id();
let TransactionEssence::Regular(essence) = transaction.essence();
let output_ids = input_signing_data.iter().map(|input| *input.output_id());
let outputs = input_signing_data
.iter()
.map(|i| i.output.clone())
.collect::<Vec<Output>>();
let inputs = output_ids
.into_iter()
.zip(outputs.iter())
.collect::<Vec<(OutputId, &Output)>>();
let context = ValidationContext::new(
&transaction_id,
essence,
inputs.iter().map(|(id, input)| (id, *input)),
transaction.unlocks(),
current_time,
);
Ok(semantic_validation(context, inputs.as_slice(), transaction.unlocks())?)
}
pub fn validate_transaction_payload_length(transaction_payload: &TransactionPayload) -> Result<()> {
let transaction_payload_bytes = transaction_payload.pack_to_vec();
if transaction_payload_bytes.len() > MAX_TX_LENGTH_FOR_BLOCK_WITH_8_PARENTS {
return Err(Error::InvalidTransactionPayloadLength {
length: transaction_payload_bytes.len(),
max_length: MAX_TX_LENGTH_FOR_BLOCK_WITH_8_PARENTS,
});
}
Ok(())
}
pub fn validate_regular_transaction_essence_length(
regular_transaction_essence: &RegularTransactionEssence,
) -> Result<()> {
let regular_transaction_essence_bytes = regular_transaction_essence.pack_to_vec();
let reference_alias_nft_unlocks_amount = regular_transaction_essence.inputs().len() - 1;
let max_length = MAX_TX_LENGTH_FOR_BLOCK_WITH_8_PARENTS
- SINGLE_UNLOCK_LENGTH
- (reference_alias_nft_unlocks_amount * REFERENCE_ALIAS_NFT_UNLOCK_LENGTH);
if regular_transaction_essence_bytes.len() > max_length {
return Err(Error::InvalidRegularTransactionEssenceLength {
length: regular_transaction_essence_bytes.len(),
max_length,
});
}
Ok(())
}