diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..f05172b89 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,69 @@ +name: CI +on: + push: + branches: + - main + pull_request: + types: [opened, repoened, synchronize] + +jobs: + test: + name: Test Rust ${{matrix.toolchain}} on ${{matrix.os}} + runs-on: ${{matrix.os}}-latest + strategy: + fail-fast: false + matrix: + toolchain: [stable, nightly] + os: [ubuntu] + steps: + - uses: actions/checkout@main + - name: Install rust + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{matrix.toolchain}} + override: true + - name: Test + uses: actions-rs/cargo@v1 + env: + RUSTFLAGS: -C debug-assertions + with: + command: test + args: --release + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@main + - name: Install minimal stable with clippy + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + components: clippy + override: true + + - name: Clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --all --all-targets -- -D clippy::all -D warnings + + rustfmt: + name: rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@main + - name: Install minimal stable with rustfmt + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + components: rustfmt + override: true + + - name: rustfmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..d00cf263f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,43 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-json + - id: check-toml + - id: pretty-format-json + - id: check-added-large-files + - id: check-case-conflict + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: detect-private-key +- repo: https://github.com/hackaugusto/pre-commit-cargo + rev: v1.0.0 + hooks: + # Allows cargo fmt to modify the source code prior to the commit + - id: cargo + name: Cargo fmt + args: ["+stable", "fmt", "--all"] + stages: [commit] + # Requires code to be properly formatted prior to pushing upstream + - id: cargo + name: Cargo fmt --check + args: ["+stable", "fmt", "--all", "--check"] + stages: [push, manual] + - id: cargo + name: Cargo check --all-targets + args: ["+stable", "check", "--all-targets"] + - id: cargo + name: Cargo check --all-targets --no-default-features + args: ["+stable", "check", "--all-targets", "--no-default-features"] + - id: cargo + name: Cargo check --all-targets --all-features + args: ["+stable", "check", "--all-targets", "--all-features"] + # Unlike fmt, clippy will not be automatically applied + - id: cargo + name: Cargo clippy + args: ["+nightly", "clippy", "--workspace", "--", "--deny", "clippy::all", "--deny", "warnings"] diff --git a/Cargo.toml b/Cargo.toml index e49d2997a..f60c08e34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,14 +12,15 @@ rust-version = "1.67" [features] default = ["std"] std = ["crypto/std", "objects/std"] -testing = ["objects/testing"] +testing = ["objects/testing", "mock"] [dependencies] clap = { version = "4.3" , features = ["derive"] } crypto = { package = "miden-crypto", git = "https://github.com/0xPolygonMiden/crypto", branch = "next", default-features = false } lazy_static = "1.4.0" objects = { package = "miden-objects", git = "https://github.com/0xPolygonMiden/miden-base", branch = "main", features = ["serde"] } -miden_lib = { package = "miden-lib", git = "https://github.com/0xPolygonMiden/miden-base", branch = "main", default-features = false} +miden_lib = { package = "miden-lib", git = "https://github.com/0xPolygonMiden/miden-base", branch = "main", default-features = false } +mock = { package = "miden-mock", git = "https://github.com/0xPolygonMiden/miden-base", branch = "main", default-features = false, optional = true } rusqlite = { version = "0.29.0", features = ["bundled"] } rusqlite_migration = { version = "1.0" } rand = { version="0.8.5" } @@ -28,4 +29,5 @@ serde_json = { version = "1.0", features = ["raw_value"] } cli-table = "0.4.7" [dev-dependencies] -ctor = "0.2.5" +uuid = { version = "1.6.1", features = ["serde", "v4"] } +mock = { package = "miden-mock", git = "https://github.com/0xPolygonMiden/miden-base", branch = "main", default-features = false } diff --git a/README.md b/README.md index 6976de8ac..f229c0ada 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# miden-client \ No newline at end of file +# miden-client diff --git a/src/cli/account.rs b/src/cli/account.rs index 0944f8ff9..b5e62c98a 100644 --- a/src/cli/account.rs +++ b/src/cli/account.rs @@ -1,7 +1,7 @@ use clap::Parser; use cli_table::{print_stdout, Cell, Style, Table}; use crypto::{dsa::rpo_falcon512::KeyPair, Felt}; -use miden_client::{Client, ClientConfig}; +use miden_client::Client; use miden_lib::{faucets, AuthScheme}; use objects::{accounts::AccountType, assets::TokenSymbol}; use rand::Rng; @@ -56,13 +56,13 @@ pub enum AccountTemplate { } impl AccountCmd { - pub fn execute(&self) -> Result<(), String> { + pub fn execute(&self, client: Client) -> Result<(), String> { match self { AccountCmd::List => { - list_accounts()?; + list_accounts(client)?; } AccountCmd::New { template, deploy } => { - new_account(template, *deploy)?; + new_account(client, template, *deploy)?; } AccountCmd::View { id: _ } => todo!(), } @@ -73,8 +73,7 @@ impl AccountCmd { // LIST ACCOUNTS // ================================================================================================ -fn list_accounts() -> Result<(), String> { - let client = Client::new(ClientConfig::default()).map_err(|err| err.to_string())?; +fn list_accounts(client: Client) -> Result<(), String> { let accounts = client.get_accounts().map_err(|err| err.to_string())?; let mut rows = vec![]; @@ -103,9 +102,11 @@ fn list_accounts() -> Result<(), String> { // ACCOUNT NEW // ================================================================================================ -fn new_account(template: &Option, deploy: bool) -> Result<(), String> { - let client = Client::new(ClientConfig::default()).map_err(|err| err.to_string())?; - +fn new_account( + client: Client, + template: &Option, + deploy: bool, +) -> Result<(), String> { if deploy { todo!("Recording the account on chain is not supported yet"); } @@ -156,10 +157,10 @@ fn new_account(template: &Option, deploy: bool) -> Result<(), S // TODO: Make these inserts atomic through a single transaction client .store() - .insert_account(&account) - .and_then(|_| client.store().insert_account_code(account.code())) + .insert_account_code(account.code()) .and_then(|_| client.store().insert_account_storage(account.storage())) .and_then(|_| client.store().insert_account_vault(account.vault())) + .and_then(|_| client.store().insert_account(&account)) .map(|_| { println!( "Succesfully created and stored Account ID: {}", diff --git a/src/cli/input_notes.rs b/src/cli/input_notes.rs new file mode 100644 index 000000000..2c3566c30 --- /dev/null +++ b/src/cli/input_notes.rs @@ -0,0 +1,142 @@ +use super::Client; +use super::Parser; + +use objects::notes::RecordedNote; +use objects::Digest; + +#[derive(Debug, Parser, Clone)] +#[clap(about = "View input notes")] +pub enum InputNotes { + /// List input notes + #[clap(short_flag = 'l')] + List, + + /// Show details of the input note for the specified note hash + #[clap(short_flag = 's')] + Show { + /// Hash of the input note to show + #[clap()] + hash: String, + + /// Show note script + #[clap(short, long, default_value = "false")] + script: bool, + + /// Show note vault + #[clap(short, long, default_value = "false")] + vault: bool, + + /// Show note inputs + #[clap(short, long, default_value = "false")] + inputs: bool, + }, +} + +impl InputNotes { + pub fn execute(&self, client: Client) -> Result<(), String> { + match self { + InputNotes::List => { + list_input_notes(client)?; + } + InputNotes::Show { + hash, + script, + vault, + inputs, + } => { + show_input_note(client, hash.clone(), *script, *vault, *inputs)?; + } + } + Ok(()) + } +} + +// LIST INPUT NOTES +// ================================================================================================ +fn list_input_notes(client: Client) -> Result<(), String> { + let notes = client.get_input_notes().map_err(|err| err.to_string())?; + print_notes_summary(¬es); + Ok(()) +} + +fn show_input_note( + client: Client, + hash: String, + show_script: bool, + show_vault: bool, + show_inputs: bool, +) -> Result<(), String> { + let hash = Digest::try_from(hash) + .map_err(|err| format!("Failed to parse input note hash: {}", err))?; + + let note = client + .store() + .get_input_note_by_hash(hash) + .map_err(|err| err.to_string())?; + + // print note summary + print_notes_summary(core::iter::once(¬e)); + + // print note script + if show_script { + println!("{}", "-".repeat(240)); + println!("Note script hash: {}", note.note().script().hash()); + println!("{}", "-".repeat(240)); + println!("Note Script:"); + println!("{}", "-".repeat(240)); + println!("{}", note.note().script().code()); + }; + + // print note vault + if show_vault { + println!("{}", "-".repeat(240)); + println!("Note vault hash: {}", note.note().vault().hash()); + println!("{}", "-".repeat(240)); + println!("Note Vault:"); + println!("{}", "-".repeat(240)); + for asset in note.note().vault().iter() { + // To do print this nicely + println!("{:?}", asset); + } + }; + + if show_inputs { + println!("{}", "-".repeat(240)); + println!("Note inputs hash: {}", note.note().inputs().hash()); + println!("{}", "-".repeat(240)); + println!("Note Inputs:"); + println!("{}", "-".repeat(240)); + for (idx, input) in note.note().inputs().inputs().iter().enumerate() { + // To do print this nicely + println!("{idx}: {input}"); + } + }; + + Ok(()) +} + +// HELPERS +// ================================================================================================ +fn print_notes_summary<'a, I>(notes: I) +where + I: IntoIterator, +{ + println!("{}", "-".repeat(240)); + println!( + "{0: <66} | {1: <66} | {2: <66} | {3: <66} | {4: <15}", + "hash", "script hash", "vault hash", "inputs hash", "serial num", + ); + println!("{}", "-".repeat(240)); + + for note in notes { + println!( + "{0: <66} | {1: <66} | {2: <66} | {3: <66} | {4: <15}", + note.note().hash(), + note.note().script().hash(), + note.note().vault().hash(), + note.note().inputs().hash(), + Digest::new(note.note().serial_num()), + ); + } + println!("{}", "-".repeat(240)); +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index f53ea835f..1b6c15b6e 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,6 +1,10 @@ +use crate::{Client, ClientConfig}; use clap::Parser; mod account; +mod input_notes; +#[cfg(feature = "testing")] +mod test; /// Root CLI struct #[derive(Parser, Debug)] @@ -20,13 +24,28 @@ pub struct Cli { pub enum Command { #[clap(subcommand)] Account(account::AccountCmd), + #[clap(subcommand)] + InputNotes(input_notes::InputNotes), + #[cfg(feature = "testing")] + /// Insert test data into the database + TestData, } /// CLI entry point impl Cli { pub fn execute(&self) -> Result<(), String> { + // create a client + let client = Client::new(ClientConfig::default()).map_err(|err| err.to_string())?; + + // execute cli command match &self.action { - Command::Account(account) => account.execute(), + Command::Account(account) => account.execute(client), + Command::InputNotes(notes) => notes.execute(client), + #[cfg(feature = "testing")] + Command::TestData => { + test::insert_test_data(client); + Ok(()) + } } } } diff --git a/src/cli/test.rs b/src/cli/test.rs new file mode 100644 index 000000000..57b2202bf --- /dev/null +++ b/src/cli/test.rs @@ -0,0 +1,18 @@ +use super::Client; + +pub fn insert_test_data(mut client: Client) { + use mock::mock::{ + account::MockAccountType, notes::AssetPreservationStatus, transaction::mock_inputs, + }; + + // generate test data + let (_, _, _, recorded_notes) = mock_inputs( + MockAccountType::StandardExisting, + AssetPreservationStatus::Preserved, + ); + + // insert notes into database + for note in recorded_notes.into_iter() { + client.insert_input_note(note).unwrap(); + } +} diff --git a/src/errors.rs b/src/errors.rs index b52fbf800..53fcd0c0c 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,5 +1,5 @@ use core::fmt; -use objects::AccountError; +use objects::{AccountError, Digest}; // CLIENT ERROR // ================================================================================================ @@ -35,9 +35,11 @@ impl std::error::Error for ClientError {} pub enum StoreError { ConnectionError(rusqlite::Error), MigrationError(rusqlite_migration::Error), + ColumnParsingError(rusqlite::Error), QueryError(rusqlite::Error), InputSerializationError(serde_json::Error), DataDeserializationError(serde_json::Error), + InputNoteNotFound(Digest), } impl fmt::Display for StoreError { @@ -47,12 +49,16 @@ impl fmt::Display for StoreError { ConnectionError(err) => write!(f, "failed to connect to the database: {err}"), MigrationError(err) => write!(f, "failed to update the database: {err}"), QueryError(err) => write!(f, "failed to retrieve data from the database: {err}"), + ColumnParsingError(err) => { + write!(f, "failed to parse data retrieved from the database: {err}") + } InputSerializationError(err) => { write!(f, "error trying to serialize inputs for the store: {err}") } DataDeserializationError(err) => { write!(f, "error deserializing data from the store: {err}") } + InputNoteNotFound(hash) => write!(f, "input note with hash {} not found", hash), } } } diff --git a/src/lib.rs b/src/lib.rs index a888acb35..741599586 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,8 @@ -use objects::accounts::{Account, AccountId, AccountStub}; +use objects::{ + accounts::{Account, AccountId, AccountStub}, + notes::RecordedNote, + Digest, +}; use std::path::PathBuf; mod store; @@ -47,7 +51,7 @@ impl Client { &self.store } - // DATA RETRIEVAL + // ACCOUNT DATA RETRIEVAL // -------------------------------------------------------------------------------------------- /// Returns summary info about the accounts managed by this client. @@ -73,6 +77,31 @@ impl Client { todo!() } + // INPUT NOTE DATA RETRIEVAL + // -------------------------------------------------------------------------------------------- + + /// Returns input notes managed by this client. + pub fn get_input_notes(&self) -> Result, ClientError> { + self.store.get_input_notes().map_err(|err| err.into()) + } + + /// Returns the input note with the specified hash. + pub fn get_input_note(&self, hash: Digest) -> Result { + self.store + .get_input_note_by_hash(hash) + .map_err(|err| err.into()) + } + + // INPUT NOTE CREATION + // -------------------------------------------------------------------------------------------- + + /// Inserts a new input note into the client's store. + pub fn insert_input_note(&mut self, note: RecordedNote) -> Result<(), ClientError> { + self.store + .insert_input_note(¬e) + .map_err(|err| err.into()) + } + // TODO: add methods for retrieving note and transaction info, and for creating/executing // transaction } @@ -89,6 +118,16 @@ pub struct ClientConfig { node_endpoint: Endpoint, } +impl ClientConfig { + /// Returns a new instance of [ClientConfig] with the specified store path and node endpoint. + pub fn new(store_path: String, node_endpoint: Endpoint) -> Self { + Self { + store_path, + node_endpoint, + } + } +} + impl Default for ClientConfig { fn default() -> Self { const STORE_FILENAME: &str = "store.sqlite3"; @@ -130,3 +169,74 @@ impl Default for Endpoint { } } } + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use super::store::tests::create_test_store_path; + use mock::mock::{ + account::MockAccountType, notes::AssetPreservationStatus, transaction::mock_inputs, + }; + + #[test] + fn test_input_notes_round_trip() { + // generate test store path + let store_path = create_test_store_path(); + + // generate test client + let mut client = super::Client::new(super::ClientConfig::new( + store_path.into_os_string().into_string().unwrap(), + super::Endpoint::default(), + )) + .unwrap(); + + // generate test data + let (_, _, _, recorded_notes) = mock_inputs( + MockAccountType::StandardExisting, + AssetPreservationStatus::Preserved, + ); + + // insert notes into database + for note in recorded_notes.iter().cloned() { + client.insert_input_note(note).unwrap(); + } + + // retrieve notes from database + let retrieved_notes = client.get_input_notes().unwrap(); + + // compare notes + assert_eq!(recorded_notes, retrieved_notes); + } + + #[test] + fn test_get_input_note() { + // generate test store path + let store_path = create_test_store_path(); + + // generate test client + let mut client = super::Client::new(super::ClientConfig::new( + store_path.into_os_string().into_string().unwrap(), + super::Endpoint::default(), + )) + .unwrap(); + + // generate test data + let (_, _, _, recorded_notes) = mock_inputs( + MockAccountType::StandardExisting, + AssetPreservationStatus::Preserved, + ); + + // insert note into database + client.insert_input_note(recorded_notes[0].clone()).unwrap(); + + // retrieve note from database + let retrieved_note = client + .get_input_note(recorded_notes[0].note().hash()) + .unwrap(); + + // compare notes + assert_eq!(recorded_notes[0], retrieved_note); + } +} diff --git a/src/main.rs b/src/main.rs index fc4ac83f2..d7ff9f2d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use clap::Parser; +use miden_client::{Client, ClientConfig}; mod cli; use cli::Cli; diff --git a/src/store/mod.rs b/src/store/mod.rs index 9f3fe8b06..4b6aa47d0 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -1,14 +1,37 @@ use super::{errors::StoreError, AccountStub, ClientConfig}; use crypto::{utils::collections::BTreeMap, Word}; use objects::{ - accounts::{Account, AccountCode, AccountStorage, AccountVault}, + accounts::{Account, AccountCode, AccountId, AccountStorage, AccountVault}, assembly::AstSerdeOptions, assets::Asset, + notes::{Note, NoteMetadata, RecordedNote}, + Digest, Felt, }; use rusqlite::{params, Connection}; mod migrations; +// TYPES +// ================================================================================================ + +type SerializedInputNoteData = ( + String, + String, + String, + String, + String, + String, + i64, + i64, + i64, + String, + String, + String, + i64, +); + +type SerializedInputNoteParts = (String, String, String, String, u64, u64, u64, String); + // CLIENT STORE // ================================================================================================ @@ -17,6 +40,10 @@ pub struct Store { } impl Store { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Returns a new instance of [Store] instantiated with the specified configuration options. pub fn new(config: StoreConfig) -> Result { let mut db = Connection::open(config.path).map_err(StoreError::ConnectionError)?; migrations::update_to_latest(&mut db)?; @@ -24,6 +51,9 @@ impl Store { Ok(Self { db }) } + // ACCOUNTS + // -------------------------------------------------------------------------------------------- + pub fn get_accounts(&self) -> Result, StoreError> { let mut stmt = self .db @@ -128,12 +158,98 @@ impl Store { self.db .execute( - "INSERT INTO account_vault (root, assets) VALUES (?, ?)", + "INSERT INTO account_vaults (root, assets) VALUES (?, ?)", params![vault_root, assets], ) .map(|_| ()) .map_err(StoreError::QueryError) } + + // NOTES + // -------------------------------------------------------------------------------------------- + + /// Retrieves the input notes from the database + pub fn get_input_notes(&self) -> Result, StoreError> { + const QUERY: &str = "SELECT script, inputs, vault, serial_num, sender_id, tag, num_assets, inclusion_proof FROM input_notes"; + + self.db + .prepare(QUERY) + .map_err(StoreError::QueryError)? + .query_map([], parse_input_note_columns) + .expect("no binding parameters used in query") + .map(|result| { + result + .map_err(StoreError::ColumnParsingError) + .and_then(parse_input_note) + }) + .collect::, _>>() + } + + /// Retrieves the input note with the specified hash from the database + pub fn get_input_note_by_hash(&self, hash: Digest) -> Result { + let query_hash = + serde_json::to_string(&hash).map_err(StoreError::InputSerializationError)?; + const QUERY: &str = "SELECT script, inputs, vault, serial_num, sender_id, tag, num_assets, inclusion_proof FROM input_notes WHERE hash = ?"; + + self.db + .prepare(QUERY) + .map_err(StoreError::QueryError)? + .query_map(params![query_hash.to_string()], parse_input_note_columns) + .map_err(StoreError::QueryError)? + .map(|result| { + result + .map_err(StoreError::ColumnParsingError) + .and_then(parse_input_note) + }) + .next() + .ok_or(StoreError::InputNoteNotFound(hash))? + } + + /// Inserts the provided input note into the database + pub fn insert_input_note(&self, recorded_note: &RecordedNote) -> Result<(), StoreError> { + let ( + hash, + nullifier, + script, + vault, + inputs, + serial_num, + sender_id, + tag, + num_assets, + inclusion_proof, + recipients, + status, + commit_height, + ) = serialize_input_note(recorded_note)?; + + const QUERY: &str = "\ + INSERT INTO input_notes + (hash, nullifier, script, vault, inputs, serial_num, sender_id, tag, num_assets, inclusion_proof, recipients, status, commit_height) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + self.db + .execute( + QUERY, + params![ + hash, + nullifier, + script, + vault, + inputs, + serial_num, + sender_id, + tag, + num_assets, + inclusion_proof, + recipients, + status, + commit_height + ], + ) + .map_err(StoreError::QueryError) + .map(|_| ()) + } } // STORE CONFIG @@ -151,50 +267,108 @@ impl From<&ClientConfig> for StoreConfig { } } -#[cfg(test)] -mod tests { - use std::fs; - - use ctor::dtor; - - use rusqlite::{params, Connection}; - - use super::{migrations, Store}; - - const DB_NAME: &str = "test_db.sqlite3"; - - pub fn store_for_tests() -> Store { - let mut db = Connection::open(DB_NAME).unwrap(); - migrations::update_to_latest(&mut db).unwrap(); - - Store { db } - } - - #[test] - pub fn insert_u64_max_as_id() { - let store = store_for_tests(); - let test_value: u64 = u64::MAX; +// HELPERS +// ================================================================================================ +/// Parse input note columns from the provided row into native types. +fn parse_input_note_columns( + row: &rusqlite::Row<'_>, +) -> Result { + let script: String = row.get(0)?; + let inputs: String = row.get(1)?; + let vault: String = row.get(2)?; + let serial_num: String = row.get(3)?; + let sender_id = row.get::(4)? as u64; + let tag = row.get::(5)? as u64; + let num_assets = row.get::(6)? as u64; + let inclusion_proof: String = row.get(7)?; + Ok(( + script, + inputs, + vault, + serial_num, + sender_id, + tag, + num_assets, + inclusion_proof, + )) +} - store.db.execute( - "INSERT INTO accounts (id, code_root, storage_root, vault_root, nonce, committed) VALUES (?, '1', '1', '1', '1', '1')", - params![test_value as i64], - ) - .unwrap(); +/// Parse a note from the provided parts. +fn parse_input_note( + serialized_input_note_parts: SerializedInputNoteParts, +) -> Result { + let (script, inputs, vault, serial_num, sender_id, tag, num_assets, inclusion_proof) = + serialized_input_note_parts; + let script = serde_json::from_str(&script).map_err(StoreError::DataDeserializationError)?; + let inputs = serde_json::from_str(&inputs).map_err(StoreError::DataDeserializationError)?; + let vault = serde_json::from_str(&vault).map_err(StoreError::DataDeserializationError)?; + let serial_num = + serde_json::from_str(&serial_num).map_err(StoreError::DataDeserializationError)?; + let note_metadata = NoteMetadata::new( + AccountId::new_unchecked(Felt::new(sender_id)), + Felt::new(tag), + Felt::new(num_assets), + ); + let note = Note::from_parts(script, inputs, vault, serial_num, note_metadata); + + let inclusion_proof = + serde_json::from_str(&inclusion_proof).map_err(StoreError::DataDeserializationError)?; + Ok(RecordedNote::new(note, inclusion_proof)) +} - let mut stmt = store.db.prepare("SELECT id from accounts").unwrap(); +/// Serialize the provided input note into database compatible types. +fn serialize_input_note( + recorded_note: &RecordedNote, +) -> Result { + let hash = serde_json::to_string(&recorded_note.note().hash()) + .map_err(StoreError::InputSerializationError)?; + let nullifier = serde_json::to_string(&recorded_note.note().nullifier()) + .map_err(StoreError::InputSerializationError)?; + let script = serde_json::to_string(&recorded_note.note().script()) + .map_err(StoreError::InputSerializationError)?; + let vault = serde_json::to_string(&recorded_note.note().vault()) + .map_err(StoreError::InputSerializationError)?; + let inputs = serde_json::to_string(&recorded_note.note().inputs()) + .map_err(StoreError::InputSerializationError)?; + let serial_num = serde_json::to_string(&recorded_note.note().serial_num()) + .map_err(StoreError::InputSerializationError)?; + let sender_id = u64::from(recorded_note.note().metadata().sender()) as i64; + let tag = u64::from(recorded_note.note().metadata().tag()) as i64; + let num_assets = u64::from(recorded_note.note().metadata().num_assets()) as i64; + let inclusion_proof = serde_json::to_string(&recorded_note.proof()) + .map_err(StoreError::InputSerializationError)?; + let recipients = serde_json::to_string(&recorded_note.note().metadata().tag()) + .map_err(StoreError::InputSerializationError)?; + let status = String::from("committed"); + let commit_height = recorded_note.origin().block_num.inner() as i64; + Ok(( + hash, + nullifier, + script, + vault, + inputs, + serial_num, + sender_id, + tag, + num_assets, + inclusion_proof, + recipients, + status, + commit_height, + )) +} - let mut rows = stmt.query([]).unwrap(); - while let Some(r) = rows.next().unwrap() { - let v: i64 = r.get(0).unwrap(); - if v as u64 == test_value { - return; - }; - } - panic!() - } +// TESTS +// ================================================================================================ - #[dtor] - fn cleanup() { - fs::remove_file(DB_NAME).unwrap() +#[cfg(test)] +pub mod tests { + use std::env::temp_dir; + use uuid::Uuid; + + pub fn create_test_store_path() -> std::path::PathBuf { + let mut temp_file = temp_dir(); + temp_file.push(format!("{}.sqlite3", Uuid::new_v4())); + temp_file } } diff --git a/src/store/store.sql b/src/store/store.sql index ac38d4836..8e92129e0 100644 --- a/src/store/store.sql +++ b/src/store/store.sql @@ -1,28 +1,61 @@ -- Create account_code table CREATE TABLE account_code ( - root BLOB NOT NULL, -- root of the Merkle tree for all exported procedures in account module. - procedures BLOB NOT NULL, -- serialized procedure digests for the account code. - module BLOB NOT NULL -- serialized ModuleAst for the account code. + root BLOB NOT NULL, -- root of the Merkle tree for all exported procedures in account module. + procedures BLOB NOT NULL, -- serialized procedure digests for the account code. + module BLOB NOT NULL, -- serialized ModuleAst for the account code. + PRIMARY KEY (root) ); -- Create account_storage table CREATE TABLE account_storage ( - root BLOB NOT NULL, -- root of the account storage Merkle tree. - slots BLOB NOT NULL -- serialized key-value pair of non-empty account slots. + root BLOB NOT NULL, -- root of the account storage Merkle tree. + slots BLOB NOT NULL, -- serialized key-value pair of non-empty account slots. + PRIMARY KEY (root) ); --- Create account_vault table -CREATE TABLE account_vault ( - root BLOB NOT NULL, -- root of the Merkle tree for the account vault. - assets BLOB NOT NULL -- serialized account vault assets. +-- Create account_vaults table +CREATE TABLE account_vaults ( + root BLOB NOT NULL, -- root of the Merkle tree for the account vault. + assets BLOB NOT NULL, -- serialized account vault assets. + PRIMARY KEY (root) ); --- Update accounts table +-- Create account_keys table +CREATE TABLE account_keys ( + account_id UNSIGNED BIG INT NOT NULL, -- ID of the account + key_pair BLOB NOT NULL, -- key pair + PRIMARY KEY (account_id), + FOREIGN KEY (account_id) REFERENCES accounts(id) +); + +-- Create accounts table CREATE TABLE accounts ( id UNSIGNED BIG INT NOT NULL, -- account ID. - code_root BLOB NOT NULL, -- root of the account_code Merkle tree. - storage_root BLOB NOT NULL, -- root of the account_storage Merkle tree. - vault_root BLOB NOT NULL, -- root of the account_vault Merkle tree. - nonce BIGINT NOT NULL, -- account nonce. - committed BOOLEAN NOT NULL -- true if recorded, false if not. + code_root BLOB NOT NULL, -- root of the account_code Merkle tree. + storage_root BLOB NOT NULL, -- root of the account_storage Merkle tree. + vault_root BLOB NOT NULL, -- root of the account_vault Merkle tree. + nonce BIGINT NOT NULL, -- account nonce. + committed BOOLEAN NOT NULL, -- true if recorded, false if not. + PRIMARY KEY (id), + FOREIGN KEY (code_root) REFERENCES account_code(root), + FOREIGN KEY (storage_root) REFERENCES account_storage(root), + FOREIGN KEY (vault_root) REFERENCES account_vaults(root) +); + +-- Create input notes table +CREATE TABLE input_notes ( + hash BLOB NOT NULL, -- the note hash + nullifier BLOB NOT NULL, -- the nullifier of the note + script BLOB NOT NULL, -- the serialized NoteScript, including script hash and ProgramAst + vault BLOB NOT NULL, -- the serialized NoteVault, including vault hash and list of assets + inputs BLOB NOT NULL, -- the serialized NoteInputs, including inputs hash and list of inputs + serial_num BLOB NOT NULL, -- the note serial number + sender_id UNSIGNED BIG INT NOT NULL, -- the account ID of the sender + tag UNSIGNED BIG INT NOT NULL, -- the note tag + num_assets UNSIGNED BIG INT NOT NULL, -- the number of assets in the note + inclusion_proof BLOB NOT NULL, -- the inclusion proof of the note against a block number + recipients BLOB NOT NULL, -- a list of account IDs of accounts which can consume this note + status TEXT CHECK( status IN ('pending', 'committed')), -- the status of the note - either pending or committed + commit_height UNSIGNED BIG INT NOT NULL, -- the block number at which the note was included into the chain + PRIMARY KEY (hash) );