miden-objects 0.17.0-rc.5

Canonical Protobuf representations for Miden protocol objects
Documentation
use alloc::string::ToString;
use alloc::vec;
use core::error::Error;

use assert_matches::assert_matches;
use miden_protocol::Word;
use miden_protocol::errors::TransactionHeaderError;
use miden_protocol::note::Note;
use miden_protocol::transaction::{InputNotes, TransactionHeader};

use crate::decoded::account::test_utils::private_account_id;
use crate::test_utils::error_source;
use crate::{BuildUnchecked, ConversionError, DecodeMessage, Verify, proto};

#[test]
fn transaction_id_verifies() {
    let decoded = proto::transaction::TransactionId { id: Some(Word::empty().into()) }
        .decode_fields()
        .unwrap();
    assert_eq!(
        decoded.verify().unwrap(),
        miden_protocol::transaction::TransactionId::from_raw(Word::empty())
    );
}

#[test]
fn transaction_header_unchecked_build_still_checks_id_and_duplicates() {
    use miden_protocol::account::{AccountId, AccountIdVersion, AccountType, AssetCallbackFlag};

    use crate::BuildUnchecked;
    let id = AccountId::dummy(
        [7; 15],
        AccountIdVersion::Version1,
        AccountType::Private,
        AssetCallbackFlag::Disabled,
    );
    let header = miden_protocol::transaction::TransactionHeader::new(
        id,
        Word::empty(),
        Word::empty(),
        Default::default(),
        vec![],
    )
    .unwrap();
    let wire = proto::transaction::TransactionHeader::from(&header);
    assert_eq!(wire.clone().decode_fields().unwrap().build_unchecked().unwrap(), header);
    let invalid = proto::transaction::TransactionHeader {
        transaction_id: Some(proto::transaction::TransactionId {
            id: Some(Word::from([1_u32, 0, 0, 0]).into()),
        }),
        ..wire.clone()
    }
    .decode_fields()
    .unwrap();
    assert!(matches!(
        error_source::<crate::decoded::transaction::TransactionHeaderBuildError>(
            &invalid.build_unchecked().unwrap_err()
        ),
        Some(crate::decoded::transaction::TransactionHeaderBuildError::IdMismatch { .. })
    ));
    let input = proto::transaction::InputNoteCommitment {
        nullifier: Some(Word::empty().into()),
        header: None,
    };
    let duplicate = proto::transaction::TransactionHeader {
        input_notes: vec![input.clone(), input],
        ..wire
    }
    .decode_fields()
    .unwrap();
    assert!(matches!(
        error_source::<miden_protocol::errors::TransactionInputError>(
            &duplicate.build_unchecked().unwrap_err()
        ),
        Some(miden_protocol::errors::TransactionInputError::DuplicateInputNote(_))
    ));
}

#[test]
fn transaction_header_conversion_preserves_validation_error_source() {
    let note = Note::mock_noop(Word::empty());
    let transaction = TransactionHeader::new(
        private_account_id(),
        Word::from([1_u32, 2, 3, 4]),
        Word::from([5_u32, 6, 7, 8]),
        InputNotes::default(),
        vec![*note.header()],
    )
    .unwrap();
    let mut message = proto::transaction::TransactionHeader::from(transaction);
    message.output_notes.push(message.output_notes[0].clone());

    let error = message
        .decode_fields()
        .unwrap()
        .build_unchecked()
        .map_err(ConversionError::new)
        .unwrap_err();
    let source = error
        .source()
        .and_then(Error::source)
        .unwrap()
        .downcast_ref::<TransactionHeaderError>()
        .unwrap();

    assert_matches!(
        source,
        TransactionHeaderError::DuplicateOutputNote(note_id) if *note_id == note.id()
    );
}

#[test]
fn transaction_header_unchecked_build_retains_input_note_error_context() {
    let transaction = TransactionHeader::new(
        private_account_id(),
        Word::empty(),
        Word::empty(),
        InputNotes::default(),
        vec![],
    )
    .unwrap();
    let mut message = proto::transaction::TransactionHeader::from(transaction);
    let mut header = proto::note::NoteHeader::from(*Note::mock_noop(Word::empty()).header());
    header.metadata.as_mut().unwrap().version = proto::note::NoteVersion::Unspecified as i32;
    message.input_notes = vec![
        proto::transaction::InputNoteCommitment {
            nullifier: Some(Word::empty().into()),
            header: None,
        },
        proto::transaction::InputNoteCommitment {
            nullifier: Some(Word::empty().into()),
            header: Some(header),
        },
    ];

    let error = message.decode_fields().unwrap().build_unchecked().unwrap_err();
    assert!(error.to_string().starts_with("input_notes[1]:"), "{error}");
    assert!(error.to_string().contains("header:"), "{error}");
    assert_matches!(
        error_source::<crate::decoded::note::NoteMetadataError>(&error),
        Some(crate::decoded::note::NoteMetadataError::UnspecifiedVersion)
    );
}