use std::sync::Arc;
use diesel::prelude::*;
use miden_protocol::Word;
use miden_protocol::block::BlockNumber;
use super::*;
use crate::NoteError;
use crate::db::models::conv as conversions;
use crate::db::{Db, schema};
use crate::test_utils::*;
fn test_note_error(msg: &str) -> NoteError {
Arc::new(std::io::Error::other(msg.to_string()))
}
fn test_conn() -> (SqliteConnection, tempfile::TempDir) {
Db::test_conn()
}
fn count_notes(conn: &mut SqliteConnection) -> i64 {
schema::notes::table.count().get_result(conn).unwrap()
}
fn count_accounts(conn: &mut SqliteConnection) -> i64 {
schema::accounts::table.count().get_result(conn).unwrap()
}
fn count_inflight_accounts(conn: &mut SqliteConnection) -> i64 {
schema::accounts::table
.filter(schema::accounts::transaction_id.is_not_null())
.count()
.get_result(conn)
.unwrap()
}
fn count_committed_accounts(conn: &mut SqliteConnection) -> i64 {
schema::accounts::table
.filter(schema::accounts::transaction_id.is_null())
.count()
.get_result(conn)
.unwrap()
}
#[test]
fn purge_inflight_clears_all_inflight_state() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let tx_id = mock_tx_id(1);
let note = mock_single_target_note(account_id, 10);
upsert_committed_account(conn, account_id, &mock_account(account_id)).unwrap();
add_transaction(conn, &tx_id, None, std::slice::from_ref(¬e), &[]).unwrap();
assert!(count_inflight_accounts(conn) == 0); assert_eq!(count_notes(conn), 1);
let tx_id2 = mock_tx_id(2);
add_transaction(conn, &tx_id2, None, &[], &[note.as_note().nullifier()]).unwrap();
let consumed_count: i64 = schema::notes::table
.filter(schema::notes::consumed_by.is_not_null())
.count()
.get_result(conn)
.unwrap();
assert_eq!(consumed_count, 1);
purge_inflight(conn).unwrap();
assert_eq!(count_inflight_accounts(conn), 0);
assert_eq!(count_committed_accounts(conn), 1);
assert_eq!(count_notes(conn), 0);
}
#[test]
fn transaction_added_inserts_notes_and_marks_consumed() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let tx_id = mock_tx_id(1);
let note1 = mock_single_target_note(account_id, 10);
let note2 = mock_single_target_note(account_id, 20);
insert_committed_notes(conn, std::slice::from_ref(¬e1)).unwrap();
assert_eq!(count_notes(conn), 1);
add_transaction(
conn,
&tx_id,
None,
std::slice::from_ref(¬e2),
&[note1.as_note().nullifier()],
)
.unwrap();
assert_eq!(count_notes(conn), 2);
let consumed: Option<Vec<u8>> = schema::notes::table
.find(conversions::nullifier_to_bytes(¬e1.as_note().nullifier()))
.select(schema::notes::consumed_by)
.first(conn)
.unwrap();
assert!(consumed.is_some());
let created: Option<Vec<u8>> = schema::notes::table
.find(conversions::nullifier_to_bytes(¬e2.as_note().nullifier()))
.select(schema::notes::created_by)
.first(conn)
.unwrap();
assert!(created.is_some());
}
#[test]
fn transaction_added_is_idempotent_for_notes() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let tx_id = mock_tx_id(1);
let note = mock_single_target_note(account_id, 10);
add_transaction(conn, &tx_id, None, std::slice::from_ref(¬e), &[]).unwrap();
add_transaction(conn, &tx_id, None, std::slice::from_ref(¬e), &[]).unwrap();
assert_eq!(count_notes(conn), 1);
}
#[test]
fn block_committed_promotes_inflight_notes_to_committed() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let tx_id = mock_tx_id(1);
let note = mock_single_target_note(account_id, 10);
let block_num = BlockNumber::from(1u32);
let header = mock_block_header(block_num);
add_transaction(conn, &tx_id, None, std::slice::from_ref(¬e), &[]).unwrap();
let created: Option<Vec<u8>> = schema::notes::table
.find(conversions::nullifier_to_bytes(¬e.as_note().nullifier()))
.select(schema::notes::created_by)
.first(conn)
.unwrap();
assert!(created.is_some());
commit_block(conn, &[tx_id], block_num, &header).unwrap();
let created: Option<Vec<u8>> = schema::notes::table
.find(conversions::nullifier_to_bytes(¬e.as_note().nullifier()))
.select(schema::notes::created_by)
.first(conn)
.unwrap();
assert!(created.is_none());
}
#[test]
fn block_committed_deletes_consumed_notes() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let note = mock_single_target_note(account_id, 10);
insert_committed_notes(conn, std::slice::from_ref(¬e)).unwrap();
assert_eq!(count_notes(conn), 1);
let tx_id = mock_tx_id(1);
add_transaction(conn, &tx_id, None, &[], &[note.as_note().nullifier()]).unwrap();
let block_num = BlockNumber::from(1u32);
let header = mock_block_header(block_num);
commit_block(conn, &[tx_id], block_num, &header).unwrap();
assert_eq!(count_notes(conn), 0);
}
#[test]
fn block_committed_promotes_inflight_account_to_committed() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let account = mock_account(account_id);
upsert_committed_account(conn, account_id, &account).unwrap();
assert_eq!(count_committed_accounts(conn), 1);
let tx_id = mock_tx_id(1);
let row = AccountInsert {
account_id: conversions::network_account_id_to_bytes(account_id),
transaction_id: Some(conversions::transaction_id_to_bytes(&tx_id)),
account_data: conversions::account_to_bytes(&account),
};
diesel::insert_into(schema::accounts::table).values(&row).execute(conn).unwrap();
assert_eq!(count_inflight_accounts(conn), 1);
assert_eq!(count_committed_accounts(conn), 1);
let block_num = BlockNumber::from(1u32);
let header = mock_block_header(block_num);
commit_block(conn, &[tx_id], block_num, &header).unwrap();
assert_eq!(count_committed_accounts(conn), 1);
assert_eq!(count_inflight_accounts(conn), 0);
}
#[test]
fn transactions_reverted_restores_consumed_notes() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let note = mock_single_target_note(account_id, 10);
insert_committed_notes(conn, std::slice::from_ref(¬e)).unwrap();
let tx_id = mock_tx_id(1);
add_transaction(conn, &tx_id, None, &[], &[note.as_note().nullifier()]).unwrap();
let consumed: Option<Vec<u8>> = schema::notes::table
.find(conversions::nullifier_to_bytes(¬e.as_note().nullifier()))
.select(schema::notes::consumed_by)
.first(conn)
.unwrap();
assert!(consumed.is_some());
revert_transaction(conn, &[tx_id]).unwrap();
let consumed: Option<Vec<u8>> = schema::notes::table
.find(conversions::nullifier_to_bytes(¬e.as_note().nullifier()))
.select(schema::notes::consumed_by)
.first(conn)
.unwrap();
assert!(consumed.is_none());
}
#[test]
fn transactions_reverted_deletes_inflight_created_notes() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let tx_id = mock_tx_id(1);
let note = mock_single_target_note(account_id, 10);
add_transaction(conn, &tx_id, None, std::slice::from_ref(¬e), &[]).unwrap();
assert_eq!(count_notes(conn), 1);
revert_transaction(conn, &[tx_id]).unwrap();
assert_eq!(count_notes(conn), 0);
}
#[test]
fn transactions_reverted_reports_reverted_account_creations() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let account = mock_account(account_id);
let tx_id = mock_tx_id(1);
let row = AccountInsert {
account_id: conversions::network_account_id_to_bytes(account_id),
transaction_id: Some(conversions::transaction_id_to_bytes(&tx_id)),
account_data: conversions::account_to_bytes(&account),
};
diesel::insert_into(schema::accounts::table).values(&row).execute(conn).unwrap();
let affected = revert_transaction(conn, &[tx_id]).unwrap();
assert!(affected.contains(&account_id));
assert_eq!(count_accounts(conn), 0);
}
#[test]
fn available_notes_filters_consumed_and_exceeded_attempts() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let note_good = mock_single_target_note(account_id, 10);
let note_consumed = mock_single_target_note(account_id, 20);
let note_failed = mock_single_target_note(account_id, 30);
insert_committed_notes(conn, &[note_good.clone(), note_consumed.clone(), note_failed.clone()])
.unwrap();
let tx_id = mock_tx_id(1);
add_transaction(conn, &tx_id, None, &[], &[note_consumed.as_note().nullifier()]).unwrap();
let block_num = BlockNumber::from(100u32);
notes_failed(
conn,
&[(note_failed.as_note().nullifier(), test_note_error("test error"))],
block_num,
)
.unwrap();
notes_failed(
conn,
&[(note_failed.as_note().nullifier(), test_note_error("test error"))],
block_num,
)
.unwrap();
notes_failed(
conn,
&[(note_failed.as_note().nullifier(), test_note_error("test error"))],
block_num,
)
.unwrap();
let result = available_notes(conn, account_id, block_num, 3).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].nullifier(), note_good.as_note().nullifier());
}
#[test]
fn available_notes_only_returns_notes_for_specified_account() {
let (conn, _dir) = &mut test_conn();
let account_id_1 = mock_network_account_id();
let account_id_2 = mock_network_account_id_seeded(42);
let note_acct1 = mock_single_target_note(account_id_1, 10);
let note_acct2 = mock_single_target_note(account_id_2, 20);
insert_committed_notes(conn, &[note_acct1.clone(), note_acct2]).unwrap();
let block_num = BlockNumber::from(100u32);
let result = available_notes(conn, account_id_1, block_num, 30).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].nullifier(), note_acct1.as_note().nullifier());
}
#[test]
fn notes_failed_increments_attempt_count() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let note = mock_single_target_note(account_id, 10);
insert_committed_notes(conn, std::slice::from_ref(¬e)).unwrap();
let block_num = BlockNumber::from(5u32);
notes_failed(
conn,
&[(note.as_note().nullifier(), test_note_error("execution failed"))],
block_num,
)
.unwrap();
notes_failed(
conn,
&[(note.as_note().nullifier(), test_note_error("execution failed 2"))],
block_num,
)
.unwrap();
let (attempt_count, last_attempt): (i32, Option<i64>) = schema::notes::table
.find(conversions::nullifier_to_bytes(¬e.as_note().nullifier()))
.select((schema::notes::attempt_count, schema::notes::last_attempt))
.first(conn)
.unwrap();
assert_eq!(attempt_count, 2);
assert_eq!(last_attempt, Some(conversions::block_num_to_i64(block_num)));
}
#[test]
fn get_note_error_returns_latest_error() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let note = mock_single_target_note(account_id, 10);
let note_id = note.as_note().id();
insert_committed_notes(conn, std::slice::from_ref(¬e)).unwrap();
let result = get_note_error(conn, &conversions::note_id_to_bytes(¬e_id)).unwrap();
assert!(result.is_some());
let row = result.unwrap();
assert!(row.last_error.is_none());
assert_eq!(row.attempt_count, 0);
let block_num = BlockNumber::from(5u32);
notes_failed(conn, &[(note.as_note().nullifier(), test_note_error("first error"))], block_num)
.unwrap();
let result = get_note_error(conn, &conversions::note_id_to_bytes(¬e_id)).unwrap();
let row = result.unwrap();
assert_eq!(row.last_error.as_deref(), Some("first error"));
assert_eq!(row.attempt_count, 1);
notes_failed(
conn,
&[(note.as_note().nullifier(), test_note_error("second error"))],
block_num,
)
.unwrap();
let result = get_note_error(conn, &conversions::note_id_to_bytes(¬e_id)).unwrap();
let row = result.unwrap();
assert_eq!(row.last_error.as_deref(), Some("second error"));
assert_eq!(row.attempt_count, 2);
}
#[test]
fn get_note_error_returns_none_for_unknown_note() {
let (conn, _dir) = &mut test_conn();
let unknown_id = vec![0u8; 32];
let result = get_note_error(conn, &unknown_id).unwrap();
assert!(result.is_none());
}
#[test]
fn upsert_chain_state_updates_singleton() {
let (conn, _dir) = &mut test_conn();
let block_num_1 = BlockNumber::from(1u32);
let header_1 = mock_block_header(block_num_1);
upsert_chain_state(conn, block_num_1, &header_1).unwrap();
let block_num_2 = BlockNumber::from(2u32);
let header_2 = mock_block_header(block_num_2);
upsert_chain_state(conn, block_num_2, &header_2).unwrap();
let row_count: i64 = schema::chain_state::table.count().get_result(conn).unwrap();
assert_eq!(row_count, 1);
let stored_block_num: i64 = schema::chain_state::table
.select(schema::chain_state::block_num)
.first(conn)
.unwrap();
assert_eq!(stored_block_num, conversions::block_num_to_i64(block_num_2));
}
#[test]
fn note_script_insert_and_lookup() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let note: miden_protocol::note::Note = mock_single_target_note(account_id, 10).into_note();
let script = note.script().clone();
let root = script.root();
insert_note_script(conn, &root, &script).unwrap();
let found = lookup_note_script(conn, &root).unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().root(), script.root());
}
#[test]
fn note_script_lookup_returns_none_for_missing() {
let (conn, _dir) = &mut test_conn();
let missing_root = Word::default();
let found = lookup_note_script(conn, &missing_root).unwrap();
assert!(found.is_none());
}
#[test]
fn note_script_insert_is_idempotent() {
let (conn, _dir) = &mut test_conn();
let account_id = mock_network_account_id();
let note: miden_protocol::note::Note = mock_single_target_note(account_id, 10).into_note();
let script = note.script().clone();
let root = script.root();
insert_note_script(conn, &root, &script).unwrap();
insert_note_script(conn, &root, &script).unwrap();
let found = lookup_note_script(conn, &root).unwrap();
assert!(found.is_some());
}