use alloc::collections::BTreeMap;
use alloc::string::ToString;
use alloc::vec::Vec;
use miden_protocol::Word;
use miden_protocol::account::AccountId;
use miden_protocol::asset::Asset;
use miden_protocol::block::BlockNumber;
use miden_protocol::note::{NoteHeader, NoteId, NoteInclusionProof, Nullifier};
use miden_protocol::transaction::{
InputNoteCommitment,
InputNotes,
TransactionHeader,
TransactionId,
};
use super::note::{CommittedNote, CommittedNoteMetadata};
use crate::rpc::{RpcConversionError, RpcError, generated as proto};
#[cfg(test)]
pub const ACCOUNT_ID_NATIVE_ASSET_FAUCET: u128 = 0xab00_0000_0000_cd20_0000_ac00_0000_de00_u128;
impl TryFrom<proto::primitives::Digest> for TransactionId {
type Error = RpcConversionError;
fn try_from(value: proto::primitives::Digest) -> Result<Self, Self::Error> {
let word: Word = value.try_into()?;
Ok(Self::from_raw(word))
}
}
impl TryFrom<proto::transaction::TransactionId> for TransactionId {
type Error = RpcConversionError;
fn try_from(value: proto::transaction::TransactionId) -> Result<Self, Self::Error> {
value
.id
.ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
entity: "TransactionId",
field_name: "id",
})?
.try_into()
}
}
impl From<TransactionId> for proto::transaction::TransactionId {
fn from(value: TransactionId) -> Self {
Self { id: Some(value.as_word().into()) }
}
}
#[derive(Debug, Clone)]
pub struct TransactionInclusion {
pub transaction_id: TransactionId,
pub block_num: BlockNumber,
pub account_id: AccountId,
pub initial_state_commitment: Word,
pub nullifiers: Vec<Nullifier>,
pub output_notes: Vec<CommittedNote>,
pub erased_output_note_ids: Vec<NoteId>,
}
#[derive(Debug, Clone)]
pub struct TransactionsInfo {
pub chain_tip: BlockNumber,
pub block_num: BlockNumber,
pub transaction_records: Vec<TransactionRecord>,
}
impl TryFrom<proto::rpc::SyncTransactionsResponse> for TransactionsInfo {
type Error = RpcError;
fn try_from(value: proto::rpc::SyncTransactionsResponse) -> Result<Self, Self::Error> {
let pagination_info = value.pagination_info.ok_or(
RpcConversionError::MissingFieldInProtobufRepresentation {
entity: "SyncTransactionsResponse",
field_name: "pagination_info",
},
)?;
let chain_tip = pagination_info.chain_tip.into();
let block_num = pagination_info.block_num.into();
let transaction_records = value
.transactions
.into_iter()
.map(TryInto::try_into)
.collect::<Result<Vec<TransactionRecord>, RpcError>>()?;
Ok(Self {
chain_tip,
block_num,
transaction_records,
})
}
}
#[derive(Debug, Clone)]
pub struct TransactionRecord {
pub block_num: BlockNumber,
pub transaction_header: TransactionHeader,
pub output_notes: Vec<CommittedNote>,
pub erased_output_note_ids: Vec<NoteId>,
}
impl TryFrom<proto::rpc::TransactionRecord> for TransactionRecord {
type Error = RpcError;
fn try_from(value: proto::rpc::TransactionRecord) -> Result<Self, Self::Error> {
let block_num = value.block_num.into();
let proto_header =
value.header.ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
entity: "TransactionRecord",
field_name: "transaction_header",
})?;
let (transaction_header, output_notes, erased_output_note_ids) =
convert_transaction_header(proto_header, value.output_note_proofs)?;
Ok(Self {
block_num,
transaction_header,
output_notes,
erased_output_note_ids,
})
}
}
fn convert_transaction_header(
value: proto::transaction::TransactionHeader,
output_note_proofs: Vec<proto::note::NoteInclusionInBlockProof>,
) -> Result<(TransactionHeader, Vec<CommittedNote>, Vec<NoteId>), RpcError> {
let account_id =
value
.account_id
.ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
entity: "TransactionHeader",
field_name: "account_id",
})?;
let initial_state_commitment = value.initial_state_commitment.ok_or(
RpcConversionError::MissingFieldInProtobufRepresentation {
entity: "TransactionHeader",
field_name: "initial_state_commitment",
},
)?;
let final_state_commitment = value.final_state_commitment.ok_or(
RpcConversionError::MissingFieldInProtobufRepresentation {
entity: "TransactionHeader",
field_name: "final_state_commitment",
},
)?;
let note_commitments = value
.input_notes
.into_iter()
.map(|d| {
let word: Word = d
.nullifier
.ok_or(RpcError::ExpectedDataMissing("nullifier".into()))?
.try_into()
.map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
Ok(InputNoteCommitment::from(Nullifier::from_raw(word)))
})
.collect::<Result<Vec<_>, RpcError>>()?;
let input_notes = InputNotes::new_unchecked(note_commitments);
let output_note_headers: Vec<NoteHeader> = value
.output_notes
.into_iter()
.map(|proto_header| {
proto_header
.try_into()
.map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))
})
.collect::<Result<Vec<_>, RpcError>>()?;
let mut proof_map: BTreeMap<NoteId, NoteInclusionProof> = BTreeMap::new();
for mut proto_proof in output_note_proofs {
let note_id: NoteId = proto_proof
.note_id
.take()
.ok_or(RpcError::ExpectedDataMissing("output_note_proofs.note_id".into()))?
.try_into()
.map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
let inclusion_proof: NoteInclusionProof = proto_proof
.try_into()
.map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
proof_map.insert(note_id, inclusion_proof);
}
let mut committed_output_notes = Vec::with_capacity(proof_map.len());
let mut erased_output_note_ids =
Vec::with_capacity(output_note_headers.len().saturating_sub(proof_map.len()));
for header in &output_note_headers {
let note_id = header.id();
if let Some(proof) = proof_map.remove(¬e_id) {
let metadata = CommittedNoteMetadata::Full(header.metadata().clone());
committed_output_notes.push(CommittedNote::new(note_id, metadata, proof));
} else {
erased_output_note_ids.push(note_id);
}
}
let fee_asset: Asset = value
.fee
.ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
entity: "TransactionHeader",
field_name: "fee",
})?
.try_into()?;
let fee = match fee_asset {
Asset::Fungible(fungible) => fungible,
Asset::NonFungible(_) => {
return Err(RpcError::InvalidResponse(
"expected fungible asset for transaction fee".into(),
));
},
};
let transaction_header = TransactionHeader::new(
account_id.try_into()?,
initial_state_commitment.try_into()?,
final_state_commitment.try_into()?,
input_notes,
output_note_headers,
fee,
);
Ok((transaction_header, committed_output_notes, erased_output_note_ids))
}