use diesel::prelude::*;
use miden_node_db::DatabaseError;
use miden_node_proto::domain::account::NetworkAccountId;
use miden_protocol::block::BlockNumber;
use miden_protocol::note::{Note, Nullifier};
use miden_standards::note::AccountTargetNetworkNote;
use miden_tx::utils::{Deserializable, Serializable};
use crate::actor::inflight_note::InflightNetworkNote;
use crate::db::models::conv as conversions;
use crate::db::schema;
#[derive(Debug, Clone, Queryable, Selectable)]
#[diesel(table_name = schema::notes)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct NoteRow {
pub note_data: Vec<u8>,
pub attempt_count: i32,
pub last_attempt: Option<i64>,
}
#[derive(Debug, Clone, Insertable)]
#[diesel(table_name = schema::notes)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct NoteInsert {
pub nullifier: Vec<u8>,
pub account_id: Vec<u8>,
pub note_data: Vec<u8>,
pub attempt_count: i32,
pub last_attempt: Option<i64>,
pub created_by: Option<Vec<u8>>,
pub consumed_by: Option<Vec<u8>>,
}
pub fn insert_committed_notes(
conn: &mut SqliteConnection,
notes: &[AccountTargetNetworkNote],
) -> Result<(), DatabaseError> {
for note in notes {
let row = NoteInsert {
nullifier: conversions::nullifier_to_bytes(¬e.as_note().nullifier()),
account_id: conversions::network_account_id_to_bytes(
NetworkAccountId::try_from(note.target_account_id())
.expect("account ID of a network note should be a network account"),
),
note_data: note.as_note().to_bytes(),
attempt_count: 0,
last_attempt: None,
created_by: None,
consumed_by: None,
};
diesel::replace_into(schema::notes::table).values(&row).execute(conn)?;
}
Ok(())
}
#[expect(clippy::cast_possible_wrap)]
pub fn available_notes(
conn: &mut SqliteConnection,
account_id: NetworkAccountId,
block_num: BlockNumber,
max_attempts: usize,
) -> Result<Vec<InflightNetworkNote>, DatabaseError> {
let account_id_bytes = conversions::network_account_id_to_bytes(account_id);
let rows: Vec<NoteRow> = schema::notes::table
.filter(schema::notes::account_id.eq(&account_id_bytes))
.filter(schema::notes::consumed_by.is_null())
.filter(schema::notes::attempt_count.lt(max_attempts as i32))
.select(NoteRow::as_select())
.load(conn)?;
let mut result = Vec::new();
for row in rows {
#[expect(clippy::cast_sign_loss)]
let attempt_count = row.attempt_count as usize;
let note = note_row_to_inflight(
&row.note_data,
attempt_count,
row.last_attempt.map(conversions::block_num_from_i64),
)?;
if note.is_available(block_num) {
result.push(note);
}
}
Ok(result)
}
pub fn notes_failed(
conn: &mut SqliteConnection,
nullifiers: &[Nullifier],
block_num: BlockNumber,
) -> Result<(), DatabaseError> {
let block_num_val = conversions::block_num_to_i64(block_num);
for nullifier in nullifiers {
let nullifier_bytes = conversions::nullifier_to_bytes(nullifier);
diesel::update(schema::notes::table.find(&nullifier_bytes))
.set((
schema::notes::attempt_count.eq(schema::notes::attempt_count + 1),
schema::notes::last_attempt.eq(Some(block_num_val)),
))
.execute(conn)?;
}
Ok(())
}
fn note_row_to_inflight(
note_data: &[u8],
attempt_count: usize,
last_attempt: Option<BlockNumber>,
) -> Result<InflightNetworkNote, DatabaseError> {
let note = Note::read_from_bytes(note_data)
.map_err(|source| DatabaseError::deserialization("failed to parse note", source))?;
let note = AccountTargetNetworkNote::new(note).map_err(|source| {
DatabaseError::deserialization("failed to convert to network note", source)
})?;
Ok(InflightNetworkNote::from_parts(note, attempt_count, last_attempt))
}