use std::num::NonZeroU64;
use async_trait::async_trait;
use ootle_byte_type::{FromByteType, ToByteType};
use tari_crypto::{
keys::PublicKey,
ristretto::{RistrettoPublicKey, RistrettoSecretKey},
};
pub use tari_ootle_common_types::engine_types::confidential::{ClaimBurnOutputData, MinotariBurnClaimProof};
use tari_ootle_common_types::engine_types::stealth::validate_transfer;
use tari_ootle_transaction::{Transaction, UnsealedTransaction, UnsignedTransaction};
use tari_ootle_wallet_crypto::{StealthCryptoApi, balance_proof::generate_stealth_balance_proof_signature, memo::Memo};
use tari_template_lib_types::{
Amount,
EncryptedData,
constants::TARI_TOKEN,
stealth::{StealthInput, StealthInputsStatement, StealthTransferStatement},
};
use crate::{
Address,
provider::{Provider, WalletProvider},
signer,
stealth::{Output, StealthProviderError},
transaction::TransactionSealSigner,
wallet::{NetworkWallet, OotleWallet, WalletResult},
};
const DEFAULT_CLAIM_MEMO: &str = "Burnt funds claimed from L1";
pub struct ClaimBurn<'a, P> {
provider: &'a P,
claim_proof: MinotariBurnClaimProof,
encrypted_data: EncryptedData,
max_fee: Amount,
recipient: Option<Address>,
memo: Option<Memo>,
}
impl<'a, P: Provider> ClaimBurn<'a, P> {
pub fn new(provider: &'a P, claim_proof: MinotariBurnClaimProof, encrypted_data: EncryptedData) -> Self {
Self {
provider,
claim_proof,
encrypted_data,
max_fee: Amount::zero(),
recipient: None,
memo: None,
}
}
pub fn with_max_fee<A: Into<Amount>>(mut self, max_fee: A) -> Self {
self.max_fee = max_fee.into();
self
}
pub fn to_recipient(mut self, recipient: Address) -> Self {
self.recipient = Some(recipient);
self
}
pub fn with_memo(mut self, memo: Memo) -> Self {
self.memo = Some(memo);
self
}
pub fn with_memo_message<T: Into<Box<str>>>(self, message: T) -> Self {
self.with_memo(Memo::new_message(message).expect("Memo message too long"))
}
}
impl<'a, P: WalletProvider<Wallet = OotleWallet>> ClaimBurn<'a, P> {
pub async fn prepare(self) -> WalletResult<(UnsignedTransaction, BurnClaimSealer)> {
let Self {
provider,
claim_proof,
encrypted_data,
max_fee,
recipient,
memo,
} = self;
let network = provider.network();
let wallet = provider.wallet();
let claimant = provider.default_signer_address().clone();
let sender_offset_public_key: RistrettoPublicKey = claim_proof
.sender_offset_public_key
.try_from_byte_type()
.map_err(|e| StealthProviderError::UnexpectedError {
details: format!("Invalid sender_offset_public_key in burn proof: {e}"),
})?;
let stealth_secret = wallet.derive_burn_claim_secret(&sender_offset_public_key).await?;
let stealth_claim_pk = RistrettoPublicKey::from_secret_key(&stealth_secret).to_byte_type();
if !StealthCryptoApi::new().validate_burn_claim_ownership_proof(
network,
&claim_proof.ownership_proof,
&claim_proof.commitment,
claim_proof.value,
&stealth_claim_pk,
) {
return Err(StealthProviderError::BurnClaimOwnershipProofInvalid.into());
}
let decrypted = wallet
.decrypt_burn_claim_output(&encrypted_data, &claim_proof.commitment, &sender_offset_public_key)
.await?;
let max_fee = u64::try_from(max_fee.to_u128()).map_err(|_| StealthProviderError::UnexpectedError {
details: "max_fee exceeds u64::MAX".to_string(),
})?;
if max_fee == 0 {
return Err(StealthProviderError::UnexpectedError {
details: "A positive max_fee is required to claim an L1 burn".to_string(),
}
.into());
}
let claimed_amount = decrypted.value();
let final_amount = claimed_amount.checked_sub(max_fee).filter(|amount| *amount > 0).ok_or(
StealthProviderError::BurnClaimFeeTooHigh {
claimed: claimed_amount,
max_fee,
},
)?;
let final_amount = NonZeroU64::new(final_amount).expect("final_amount checked to be positive above");
let recipient = recipient.unwrap_or_else(|| claimant.clone());
let memo = memo.unwrap_or_else(|| Memo::new_message(DEFAULT_CLAIM_MEMO).expect("valid memo"));
let output = Output::new(recipient, TARI_TOKEN, final_amount).with_memo(memo);
let (outputs_statement, agg_output_mask) = wallet
.generate_outputs_statement(vec![output], Amount::from(max_fee))
.await?;
let inputs_statement =
StealthInputsStatement::new(vec![StealthInput::from(claim_proof.commitment)], Amount::zero());
let agg_input_mask = decrypted.mask().clone();
let balance_proof = generate_stealth_balance_proof_signature(
&agg_input_mask,
&agg_output_mask,
&inputs_statement,
&outputs_statement,
);
let transfer = StealthTransferStatement {
inputs_statement,
outputs_statement,
balance_proof: Some(balance_proof),
};
if let Err(err) = validate_transfer(&transfer, None) {
return Err(StealthProviderError::UnexpectedError {
details: format!("Constructed burn claim transfer is invalid: {err}"),
}
.into());
}
let output_data = ClaimBurnOutputData { encrypted_data };
let unsigned_tx = Transaction::builder(network)
.with_fee_instructions_builder(|builder| {
builder
.claim_burn(claim_proof, output_data)
.stealth_transfer(TARI_TOKEN, transfer)
.put_last_instruction_output_on_workspace("fee")
.pay_fee_from_bucket("fee")
})
.build_unsigned();
Ok((unsigned_tx, BurnClaimSealer::new(stealth_secret, claimant)))
}
}
#[derive(Clone)]
pub struct BurnClaimSealer {
secret: RistrettoSecretKey,
address: Address,
}
impl BurnClaimSealer {
pub(crate) fn new(secret: RistrettoSecretKey, address: Address) -> Self {
Self { secret, address }
}
}
impl std::fmt::Debug for BurnClaimSealer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BurnClaimSealer")
.field("secret", &"<redacted>")
.field("address", &self.address)
.finish()
}
}
#[async_trait]
impl TransactionSealSigner for BurnClaimSealer {
async fn seal_transaction(&self, transaction: UnsealedTransaction) -> signer::Result<Transaction> {
Ok(transaction.seal(&self.secret))
}
}
impl NetworkWallet for BurnClaimSealer {
fn default_address(&self) -> &Address {
&self.address
}
async fn sign_transaction(&self, unsigned: UnsignedTransaction) -> WalletResult<Transaction> {
Ok(unsigned.finish().seal(&self.secret))
}
}