use anyhow::{Context, Result, anyhow, bail};
use pallas_codec::minicbor;
use pallas_codec::utils::{Bytes, MaybeIndefArray};
use pallas_crypto::hash::{Hash, Hasher};
use pallas_primitives::conway::{self, ExUnits, PlutusData, Redeemer, RedeemerTag, Redeemers};
use pallas_primitives::{Fragment, NonEmptyKeyValuePairs};
use pallas_txbuilder::BuiltTransaction;
use crate::build::tx_id;
pub fn reward_account(script_hash: &[u8; 28], network_flag: bool) -> Vec<u8> {
let mut account = Vec::with_capacity(29);
account.push(if network_flag { 0xf0 } else { 0xf1 });
account.extend_from_slice(script_hash);
account
}
pub fn script_data_hash(
redeemers_cbor: &[u8],
datums_cbor: Option<&[u8]>,
cost_model_v3: &[i64],
) -> Result<Hash<32>> {
let mut preimage = redeemers_cbor.to_vec();
if let Some(datums) = datums_cbor {
preimage.extend_from_slice(datums);
}
let mut view = minicbor::Encoder::new(Vec::new());
view.map(1)
.and_then(|e| e.u8(2))
.and_then(|e| e.encode(cost_model_v3))
.map_err(|e| anyhow!("The language view can't be encoded: {e}"))?;
preimage.extend(view.into_writer());
Ok(Hasher::<256>::hash(&preimage))
}
#[derive(Debug, Clone)]
pub struct WithdrawZero {
pub account: Vec<u8>,
pub redeemer: PlutusData,
}
impl WithdrawZero {
pub fn new(script_hash: &[u8; 28], network_flag: bool, redeemer_cbor: &[u8]) -> Result<Self> {
let redeemer = PlutusData::decode_fragment(redeemer_cbor)
.map_err(|e| anyhow!("The withdrawal's redeemer isn't Plutus data: {e}"))?;
Ok(WithdrawZero {
account: reward_account(script_hash, network_flag),
redeemer,
})
}
pub fn patch(
&self,
mut built: BuiltTransaction,
budget: ExUnits,
cost_model_v3: &[i64],
) -> Result<BuiltTransaction> {
if built.signatures.is_some() {
bail!("A transaction is patched before it's signed, not after");
}
let mut tx = conway::Tx::decode_fragment(&built.tx_bytes.0)
.map_err(|e| anyhow!("The built transaction isn't a Conway transaction: {e}"))?;
let body = &mut tx.transaction_body;
let account = Bytes::from(self.account.clone());
let others = body
.withdrawals
.as_ref()
.map(|w| w.iter().any(|(a, _)| *a != account))
.unwrap_or(false);
if others {
bail!("The transaction already withdraws from another account");
}
body.withdrawals = Some(NonEmptyKeyValuePairs::Def(vec![(account, 0)]));
let witness = &mut tx.transaction_witness_set;
if witness.plutus_data.is_some() {
bail!("A transaction with witness datums can't be given a withdraw-zero yet");
}
let mut redeemers = match witness.redeemer.take() {
None => Vec::new(),
Some(Redeemers::List(list)) => list.to_vec(),
Some(Redeemers::Map(_)) => {
bail!("The transaction's redeemers are a map, which this patch doesn't write")
}
};
redeemers.retain(|r| r.tag != RedeemerTag::Reward);
redeemers.push(Redeemer {
tag: RedeemerTag::Reward,
index: 0,
data: self.redeemer.clone(),
ex_units: budget,
});
let redeemers = Redeemers::List(MaybeIndefArray::Def(redeemers));
let redeemers_cbor = minicbor::to_vec(&redeemers)
.map_err(|e| anyhow!("The redeemers can't be encoded: {e}"))?;
tx.transaction_body.script_data_hash =
Some(script_data_hash(&redeemers_cbor, None, cost_model_v3)?);
tx.transaction_witness_set.redeemer = Some(redeemers);
let bytes = tx
.encode_fragment()
.map_err(|e| anyhow!("Failed to encode the transaction: {e}"))?;
built.tx_hash.0 = *tx_id(&bytes).context("The patched transaction can't be hashed")?;
built.tx_bytes.0 = bytes;
Ok(built)
}
}