diff --git a/Cargo.toml b/Cargo.toml index 13619ee7f..f5d8b57b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,9 @@ default = ["std"] std = ["crypto/std", "objects/std"] testing = ["objects/testing", "mock"] +[workspace] +members = [".", "entity", "migration"] + [dependencies] clap = { version = "4.3" , features = ["derive"] } crypto = { package = "miden-crypto", git = "https://github.com/0xPolygonMiden/crypto", branch = "next", default-features = false } @@ -26,6 +29,11 @@ rusqlite_migration = { version = "1.0" } rand = { version="0.8.5" } serde = {version="1.0", features = ["derive"]} serde_json = { version = "1.0", features = ["raw_value"] } +futures = "0.3.28" +sea-orm = { version = "^0.12.0", features = [ "sqlx-sqlite", "runtime-tokio-rustls", "macros"], default-features = false } +entity = { path = "entity" } +migration = { path = "migration" } +tokio = {version = "1.34.0", features = ["macros", "rt-multi-thread"] } [dev-dependencies] uuid = { version = "1.6.1", features = ["serde", "v4"] } diff --git a/entity/Cargo.toml b/entity/Cargo.toml new file mode 100644 index 000000000..6092c8d24 --- /dev/null +++ b/entity/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "entity" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +sea-orm = { version = "^0.12.0"} \ No newline at end of file diff --git a/entity/src/account_code.rs b/entity/src/account_code.rs new file mode 100644 index 000000000..24279d10e --- /dev/null +++ b/entity/src/account_code.rs @@ -0,0 +1,32 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "account_code")] +pub struct Model { + #[sea_orm( + primary_key, + auto_increment = false, + column_type = "Binary(BlobSize::Blob(None))" + )] + pub root: Vec, + #[sea_orm(column_type = "Binary(BlobSize::Blob(None))")] + pub procedures: Vec, + #[sea_orm(column_type = "Binary(BlobSize::Blob(None))")] + pub module: Vec, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::accounts::Entity")] + Accounts, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Accounts.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/entity/src/account_keys.rs b/entity/src/account_keys.rs new file mode 100644 index 000000000..a071e27f1 --- /dev/null +++ b/entity/src/account_keys.rs @@ -0,0 +1,32 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "account_keys")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub account_id: i64, + #[sea_orm(column_type = "Binary(BlobSize::Blob(None))")] + pub key_pair: Vec, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::accounts::Entity", + from = "Column::AccountId", + to = "super::accounts::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + Accounts, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Accounts.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/entity/src/account_storage.rs b/entity/src/account_storage.rs new file mode 100644 index 000000000..868be2fdd --- /dev/null +++ b/entity/src/account_storage.rs @@ -0,0 +1,30 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "account_storage")] +pub struct Model { + #[sea_orm( + primary_key, + auto_increment = false, + column_type = "Binary(BlobSize::Blob(None))" + )] + pub root: Vec, + #[sea_orm(column_type = "Binary(BlobSize::Blob(None))")] + pub slots: Vec, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::accounts::Entity")] + Accounts, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Accounts.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/entity/src/account_vaults.rs b/entity/src/account_vaults.rs new file mode 100644 index 000000000..1eb0fde16 --- /dev/null +++ b/entity/src/account_vaults.rs @@ -0,0 +1,30 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "account_vaults")] +pub struct Model { + #[sea_orm( + primary_key, + auto_increment = false, + column_type = "Binary(BlobSize::Blob(None))" + )] + pub root: Vec, + #[sea_orm(column_type = "Binary(BlobSize::Blob(None))")] + pub assets: Vec, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::accounts::Entity")] + Accounts, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Accounts.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/entity/src/accounts.rs b/entity/src/accounts.rs new file mode 100644 index 000000000..3ab772c85 --- /dev/null +++ b/entity/src/accounts.rs @@ -0,0 +1,74 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "accounts")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: i64, + #[sea_orm(column_type = "Binary(BlobSize::Blob(None))")] + pub code_root: Vec, + #[sea_orm(column_type = "Binary(BlobSize::Blob(None))")] + pub storage_root: Vec, + #[sea_orm(column_type = "Binary(BlobSize::Blob(None))")] + pub vault_root: Vec, + pub nonce: i64, + pub committed: bool, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::account_code::Entity", + from = "Column::CodeRoot", + to = "super::account_code::Column::Root", + on_update = "NoAction", + on_delete = "NoAction" + )] + AccountCode, + #[sea_orm(has_many = "super::account_keys::Entity")] + AccountKeys, + #[sea_orm( + belongs_to = "super::account_storage::Entity", + from = "Column::StorageRoot", + to = "super::account_storage::Column::Root", + on_update = "NoAction", + on_delete = "NoAction" + )] + AccountStorage, + #[sea_orm( + belongs_to = "super::account_vaults::Entity", + from = "Column::VaultRoot", + to = "super::account_vaults::Column::Root", + on_update = "NoAction", + on_delete = "NoAction" + )] + AccountVaults, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::AccountCode.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::AccountKeys.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::AccountStorage.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::AccountVaults.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/entity/src/lib.rs b/entity/src/lib.rs new file mode 100644 index 000000000..7c6a9c800 --- /dev/null +++ b/entity/src/lib.rs @@ -0,0 +1,9 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6 + +pub mod prelude; + +pub mod account_code; +pub mod account_keys; +pub mod account_storage; +pub mod account_vaults; +pub mod accounts; diff --git a/entity/src/prelude.rs b/entity/src/prelude.rs new file mode 100644 index 000000000..116e4b8ea --- /dev/null +++ b/entity/src/prelude.rs @@ -0,0 +1,7 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6 + +pub use super::account_code::Entity as AccountCode; +pub use super::account_keys::Entity as AccountKeys; +pub use super::account_storage::Entity as AccountStorage; +pub use super::account_vaults::Entity as AccountVaults; +pub use super::accounts::Entity as Accounts; diff --git a/migration/Cargo.toml b/migration/Cargo.toml new file mode 100644 index 000000000..dda708cfa --- /dev/null +++ b/migration/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "migration" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +async-std = { version = "1", features = ["attributes", "tokio1"] } + +[dependencies.sea-orm-migration] +version = "0.12.0" +features = [ + # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI. + # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime. + # e.g. + "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature + "sqlx-sqlite", # `DATABASE_DRIVER` feature +] diff --git a/migration/README.md b/migration/README.md new file mode 100644 index 000000000..3b438d89e --- /dev/null +++ b/migration/README.md @@ -0,0 +1,41 @@ +# Running Migrator CLI + +- Generate a new migration file + ```sh + cargo run -- generate MIGRATION_NAME + ``` +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/migration/src/lib.rs b/migration/src/lib.rs new file mode 100644 index 000000000..2c605afb9 --- /dev/null +++ b/migration/src/lib.rs @@ -0,0 +1,12 @@ +pub use sea_orm_migration::prelude::*; + +mod m20220101_000001_create_table; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m20220101_000001_create_table::Migration)] + } +} diff --git a/migration/src/m20220101_000001_create_table.rs b/migration/src/m20220101_000001_create_table.rs new file mode 100644 index 000000000..f5652076c --- /dev/null +++ b/migration/src/m20220101_000001_create_table.rs @@ -0,0 +1,203 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // Create account_code table + manager + .create_table( + Table::create() + .table(AccountCode::Table) + .if_not_exists() + .col( + ColumnDef::new(AccountCode::Root) + .blob(BlobSize::Long) + .not_null() + .primary_key(), + ) + .col( + ColumnDef::new(AccountCode::Procedures) + .blob(BlobSize::Long) + .not_null(), + ) + .col( + ColumnDef::new(AccountCode::Module) + .blob(BlobSize::Long) + .not_null(), + ) + .to_owned(), + ) + .await?; + + // Create account_storage table + manager + .create_table( + Table::create() + .table(AccountStorage::Table) + .if_not_exists() + .col( + ColumnDef::new(AccountStorage::Root) + .blob(BlobSize::Long) + .not_null() + .primary_key(), + ) + .col( + ColumnDef::new(AccountStorage::Slots) + .blob(BlobSize::Long) + .not_null(), + ) + .to_owned(), + ) + .await?; + + // Create account_vaults table + manager + .create_table( + Table::create() + .table(AccountVaults::Table) + .if_not_exists() + .col( + ColumnDef::new(AccountVaults::Root) + .blob(BlobSize::Long) + .not_null() + .primary_key(), + ) + .col( + ColumnDef::new(AccountVaults::Assets) + .blob(BlobSize::Long) + .not_null(), + ) + .to_owned(), + ) + .await?; + + // Create account_keys table + manager + .create_table( + Table::create() + .table(AccountKeys::Table) + .if_not_exists() + .col( + ColumnDef::new(AccountKeys::AccountId) + .big_unsigned() + .not_null() + .primary_key(), + ) + .foreign_key( + ForeignKey::create() + .name("account_keys_account_id_fk") + .from(AccountKeys::Table, AccountKeys::AccountId) + .to(Accounts::Table, Accounts::Id), + ) + .col( + ColumnDef::new(AccountKeys::KeyPair) + .blob(BlobSize::Long) + .not_null(), + ) + .to_owned(), + ) + .await?; + + // Create accounts table + manager + .create_table( + Table::create() + .table(Accounts::Table) + .if_not_exists() + .col( + ColumnDef::new(Accounts::Id) + .big_integer() + .not_null() + .primary_key(), + ) + .col( + ColumnDef::new(Accounts::CodeRoot) + .blob(BlobSize::Long) + .not_null(), + ) + .foreign_key( + ForeignKey::create() + .name("accounts_code_root_fk") + .from(Accounts::Table, Accounts::CodeRoot) + .to(AccountCode::Table, AccountCode::Root), + ) + .col( + ColumnDef::new(Accounts::StorageRoot) + .blob(BlobSize::Long) + .not_null(), + ) + .foreign_key( + ForeignKey::create() + .name("accounts_storage_root_fk") + .from(Accounts::Table, Accounts::StorageRoot) + .to(AccountStorage::Table, AccountStorage::Root), + ) + .col( + ColumnDef::new(Accounts::VaultRoot) + .blob(BlobSize::Long) + .not_null(), + ) + .foreign_key( + ForeignKey::create() + .name("accounts_vault_root_fk") + .from(Accounts::Table, Accounts::VaultRoot) + .to(AccountVaults::Table, AccountVaults::Root), + ) + .col(ColumnDef::new(Accounts::Nonce).big_integer().not_null()) + .col(ColumnDef::new(Accounts::Committed).boolean().not_null()) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(AccountCode::Table).to_owned()) + .await + } +} + +#[derive(DeriveIden)] +enum AccountCode { + Table, + Root, // blob not null primary key + Procedures, // blob not null + Module, // blob not null +} + +#[derive(DeriveIden)] +enum AccountStorage { + Table, + Root, // blob not null primary key + Slots, // blob not null +} + +#[derive(DeriveIden)] +enum AccountVaults { + Table, + Root, // blob not null primary key + Assets, // blob not null +} + +#[derive(DeriveIden)] +enum AccountKeys { + Table, + AccountId, // unsigned big int not null primary key, references accounts(id) + KeyPair, // blob not null +} + +#[derive(DeriveIden)] +enum Accounts { + Table, + Id, // unsigned big int not null primary key + CodeRoot, // blob not null, references account_code(root) + StorageRoot, // blob not null, references account_storage(root) + VaultRoot, // blob not null, references account_vaults(root) + Nonce, // big int not null + Committed, // boolean not null +} diff --git a/migration/src/main.rs b/migration/src/main.rs new file mode 100644 index 000000000..c6b6e48db --- /dev/null +++ b/migration/src/main.rs @@ -0,0 +1,6 @@ +use sea_orm_migration::prelude::*; + +#[async_std::main] +async fn main() { + cli::run_cli(migration::Migrator).await; +} diff --git a/sea-orm.md b/sea-orm.md new file mode 100644 index 000000000..ee5a5a876 --- /dev/null +++ b/sea-orm.md @@ -0,0 +1,263 @@ +# Sea orm setup + +## Install CLI +```bash +cargo install sea-orm-cli +``` + +## Initialize migration +```bash +sea-orm-cli migrate init +``` + +## Modify migration file (`x_x_create_table.rs`) +```rs +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // Create account_code table + manager + .create_table( + Table::create() + .table(AccountCode::Table) + .if_not_exists() + .col( + ColumnDef::new(AccountCode::Root) + .blob(BlobSize::Long) + .not_null() + .primary_key(), + ) + .col( + ColumnDef::new(AccountCode::Procedures) + .blob(BlobSize::Long) + .not_null(), + ) + .col( + ColumnDef::new(AccountCode::Module) + .blob(BlobSize::Long) + .not_null(), + ) + .to_owned(), + ) + .await?; + + // Create account_storage table + manager + .create_table( + Table::create() + .table(AccountStorage::Table) + .if_not_exists() + .col( + ColumnDef::new(AccountStorage::Root) + .blob(BlobSize::Long) + .not_null() + .primary_key(), + ) + .col( + ColumnDef::new(AccountStorage::Slots) + .blob(BlobSize::Long) + .not_null(), + ) + .to_owned(), + ) + .await?; + + // Create account_vaults table + manager + .create_table( + Table::create() + .table(AccountVaults::Table) + .if_not_exists() + .col( + ColumnDef::new(AccountVaults::Root) + .blob(BlobSize::Long) + .not_null() + .primary_key(), + ) + .col( + ColumnDef::new(AccountVaults::Assets) + .blob(BlobSize::Long) + .not_null(), + ) + .to_owned(), + ) + .await?; + + // Create account_keys table + manager + .create_table( + Table::create() + .table(AccountKeys::Table) + .if_not_exists() + .col( + ColumnDef::new(AccountKeys::AccountId) + .big_unsigned() + .not_null() + .primary_key(), + ) + .foreign_key( + ForeignKey::create() + .name("account_keys_account_id_fk") + .from(AccountKeys::Table, AccountKeys::AccountId) + .to(Accounts::Table, Accounts::Id), + ) + .col( + ColumnDef::new(AccountKeys::KeyPair) + .blob(BlobSize::Long) + .not_null(), + ) + .to_owned(), + ) + .await?; + + // Create accounts table + manager + .create_table( + Table::create() + .table(Accounts::Table) + .if_not_exists() + .col( + ColumnDef::new(Accounts::Id) + .big_integer() + .not_null() + .primary_key(), + ) + .col( + ColumnDef::new(Accounts::CodeRoot) + .blob(BlobSize::Long) + .not_null(), + ) + .foreign_key( + ForeignKey::create() + .name("accounts_code_root_fk") + .from(Accounts::Table, Accounts::CodeRoot) + .to(AccountCode::Table, AccountCode::Root), + ) + .col( + ColumnDef::new(Accounts::StorageRoot) + .blob(BlobSize::Long) + .not_null(), + ) + .foreign_key( + ForeignKey::create() + .name("accounts_storage_root_fk") + .from(Accounts::Table, Accounts::StorageRoot) + .to(AccountStorage::Table, AccountStorage::Root), + ) + .col( + ColumnDef::new(Accounts::VaultRoot) + .blob(BlobSize::Long) + .not_null(), + ) + .foreign_key( + ForeignKey::create() + .name("accounts_vault_root_fk") + .from(Accounts::Table, Accounts::VaultRoot) + .to(AccountVaults::Table, AccountVaults::Root), + ) + .col(ColumnDef::new(Accounts::Nonce).big_integer().not_null()) + .col(ColumnDef::new(Accounts::Committed).boolean().not_null()) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(AccountCode::Table).to_owned()) + .await + } +} + +#[derive(DeriveIden)] +enum AccountCode { + Table, + Root, // blob not null primary key + Procedures, // blob not null + Module, // blob not null +} + +#[derive(DeriveIden)] +enum AccountStorage { + Table, + Root, // blob not null primary key + Slots, // blob not null +} + +#[derive(DeriveIden)] +enum AccountVaults { + Table, + Root, // blob not null primary key + Assets, // blob not null +} + +#[derive(DeriveIden)] +enum AccountKeys { + Table, + AccountId, // unsigned big int not null primary key, references accounts(id) + KeyPair, // blob not null +} + +#[derive(DeriveIden)] +enum Accounts { + Table, + Id, // unsigned big int not null primary key + CodeRoot, // blob not null, references account_code(root) + StorageRoot, // blob not null, references account_storage(root) + VaultRoot, // blob not null, references account_vaults(root) + Nonce, // big int not null + Committed, // boolean not null +} + +``` + +## Run migration +```bash +sea-orm-cli migrate up --database-url=sqlite://store.sqlite3?mode=rwc +``` + +Verify with: +```bash +sqlite3 store.sqlite3 ".schema accounts" ".exit" +``` + +## Create entity lib +```bash +cargo new entity --lib +``` + +## Generate entity files +```bash +sea-orm-cli generate entity -o entity/src --database-url=sqlite://store.sqlite3?mode=rwc +``` + +## Modify entity cargo manifest dependencies +```toml +sea-orm = { version = "^0.12.0"} +``` + +## Set entity as library (remove mod.rs) +```bash +mv entity/src/mod.rs entity/src/lib.rs +``` + +## Modify root cargo manifest dependencies +**Set up a workspace:** +```toml +[workspace] +members = [".", "entity", "migration"] +``` + +**Set paths of workspace members** +```toml +[dependencies] +entity = { path = "entity" } +migration = { path = "migration" } +``` \ No newline at end of file diff --git a/src/cli/account.rs b/src/cli/account.rs index d1ec5bdcd..403acd6a8 100644 --- a/src/cli/account.rs +++ b/src/cli/account.rs @@ -55,13 +55,13 @@ pub enum AccountTemplate { } impl AccountCmd { - pub fn execute(&self, client: Client) -> Result<(), String> { + pub async fn execute(&self, client: Client) -> Result<(), String> { match self { AccountCmd::List => { - list_accounts(client)?; + list_accounts(client).await?; } AccountCmd::New { template, deploy } => { - new_account(client, template, *deploy)?; + new_account(client, template, *deploy).await?; } AccountCmd::View { id: _ } => todo!(), } @@ -72,7 +72,7 @@ impl AccountCmd { // LIST ACCOUNTS // ================================================================================================ -fn list_accounts(client: Client) -> Result<(), String> { +async fn list_accounts(client: Client) -> Result<(), String> { println!("{}", "-".repeat(240)); println!( "{0: <18} | {1: <66} | {2: <66} | {3: <66} | {4: <15}", @@ -80,7 +80,7 @@ fn list_accounts(client: Client) -> Result<(), String> { ); println!("{}", "-".repeat(240)); - let accounts = client.get_accounts().map_err(|err| err.to_string())?; + let accounts = client.get_accounts().await.map_err(|err| err.to_string())?; for acct in accounts { println!( @@ -99,8 +99,8 @@ fn list_accounts(client: Client) -> Result<(), String> { // ACCOUNT NEW // ================================================================================================ -fn new_account( - client: Client, +async fn new_account( + mut client: Client, template: &Option, deploy: bool, ) -> Result<(), String> { @@ -151,20 +151,9 @@ fn new_account( } .map_err(|err| err.to_string())?; - // TODO: Make these inserts atomic through a single transaction client - .store() - .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: {}", - account.id() - ) - }) + .insert_account(&account) + .await .map_err(|x| x.to_string())?; - Ok(()) } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 1b6c15b6e..14af9a59f 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -33,13 +33,15 @@ pub enum Command { /// CLI entry point impl Cli { - pub fn execute(&self) -> Result<(), String> { + pub async fn execute(&self) -> Result<(), String> { // create a client - let client = Client::new(ClientConfig::default()).map_err(|err| err.to_string())?; + let client = Client::new(ClientConfig::default()) + .await + .map_err(|err| err.to_string())?; // execute cli command match &self.action { - Command::Account(account) => account.execute(client), + Command::Account(account) => account.execute(client).await, Command::InputNotes(notes) => notes.execute(client), #[cfg(feature = "testing")] Command::TestData => { diff --git a/src/errors.rs b/src/errors.rs index 53fcd0c0c..e9baac31d 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -36,7 +36,7 @@ pub enum StoreError { ConnectionError(rusqlite::Error), MigrationError(rusqlite_migration::Error), ColumnParsingError(rusqlite::Error), - QueryError(rusqlite::Error), + QueryError(sea_orm::DbErr), InputSerializationError(serde_json::Error), DataDeserializationError(serde_json::Error), InputNoteNotFound(Digest), diff --git a/src/lib.rs b/src/lib.rs index 741599586..8a1ec43de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +use crypto::dsa::rpo_falcon512::KeyPair; use objects::{ accounts::{Account, AccountId, AccountStub}, notes::RecordedNote, @@ -37,9 +38,9 @@ impl Client { /// /// # Errors /// Returns an error if the client could not be instantiated. - pub fn new(config: ClientConfig) -> Result { + pub async fn new(config: ClientConfig) -> Result { Ok(Self { - store: Store::new((&config).into())?, + store: Store::new((&config).into()).await?, }) } @@ -51,14 +52,25 @@ impl Client { &self.store } + // ACCOUNT INSERTION + // -------------------------------------------------------------------------------------------- + + /// Inserts a new account into the client's store. + pub async fn insert_account(&mut self, account: &Account) -> Result<(), ClientError> { + self.store + .insert_account_with_metadata(account) + .await + .map_err(ClientError::StoreError) + } + // ACCOUNT DATA RETRIEVAL // -------------------------------------------------------------------------------------------- /// Returns summary info about the accounts managed by this client. /// /// TODO: replace `AccountStub` with a more relevant structure. - pub fn get_accounts(&self) -> Result, ClientError> { - self.store.get_accounts().map_err(|err| err.into()) + pub async fn get_accounts(&self) -> Result, ClientError> { + self.store.get_accounts().await.map_err(|err| err.into()) } /// Returns historical states for the account with the specified ID. @@ -180,8 +192,8 @@ mod tests { account::MockAccountType, notes::AssetPreservationStatus, transaction::mock_inputs, }; - #[test] - fn test_input_notes_round_trip() { + #[tokio::test] + async fn test_input_notes_round_trip() { // generate test store path let store_path = create_test_store_path(); @@ -190,6 +202,7 @@ mod tests { store_path.into_os_string().into_string().unwrap(), super::Endpoint::default(), )) + .await .unwrap(); // generate test data @@ -210,8 +223,8 @@ mod tests { assert_eq!(recorded_notes, retrieved_notes); } - #[test] - fn test_get_input_note() { + #[tokio::test] + async fn test_get_input_note() { // generate test store path let store_path = create_test_store_path(); @@ -220,6 +233,7 @@ mod tests { store_path.into_os_string().into_string().unwrap(), super::Endpoint::default(), )) + .await .unwrap(); // generate test data diff --git a/src/main.rs b/src/main.rs index d7ff9f2d9..2e6ff2888 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,12 +4,13 @@ use miden_client::{Client, ClientConfig}; mod cli; use cli::Cli; -fn main() { +#[tokio::main] +async fn main() { // read command-line args let cli = Cli::parse(); // execute cli action - if let Err(error) = cli.execute() { + if let Err(error) = cli.execute().await { println!("{}", error); } } diff --git a/src/store/migrations.rs b/src/store/migrations.rs deleted file mode 100644 index 52ce8da99..000000000 --- a/src/store/migrations.rs +++ /dev/null @@ -1,21 +0,0 @@ -use super::StoreError; -use lazy_static::lazy_static; -use rusqlite::Connection; -use rusqlite_migration::{Migrations, M}; - -// MIGRATIONS -// ================================================================================================ - -lazy_static! { - static ref MIGRATIONS: Migrations<'static> = - Migrations::new(vec![M::up(include_str!("store.sql")),]); -} - -// PUBLIC FUNCTIONS -// ================================================================================================ - -pub fn update_to_latest(conn: &mut Connection) -> Result<(), StoreError> { - MIGRATIONS - .to_latest(conn) - .map_err(StoreError::MigrationError) -} diff --git a/src/store/mod.rs b/src/store/mod.rs index 4b6aa47d0..b841415bc 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -9,7 +9,13 @@ use objects::{ }; use rusqlite::{params, Connection}; -mod migrations; +// mod migrations; +use entity::{account_code, account_keys, account_storage, account_vaults, accounts}; +use migration::{Migrator, MigratorTrait}; +use sea_orm::{ + ActiveModelTrait, Database, DatabaseConnection, DatabaseTransaction, EntityTrait, Set, + TransactionTrait, +}; // TYPES // ================================================================================================ @@ -36,7 +42,7 @@ type SerializedInputNoteParts = (String, String, String, String, u64, u64, u64, // ================================================================================================ pub struct Store { - db: Connection, + db: DatabaseConnection, } impl Store { @@ -44,125 +50,149 @@ impl Store { // -------------------------------------------------------------------------------------------- /// 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)?; - + pub async fn new(config: StoreConfig) -> Result { + let url = format!("sqlite://{}?mode=rwc", config.path); + let db = Database::connect(&url) + .await + .expect("Failed to setup the database"); + Migrator::up(&db, None) + .await + .expect("Failed to run migrations for tests"); Ok(Self { db }) } // ACCOUNTS // -------------------------------------------------------------------------------------------- - pub fn get_accounts(&self) -> Result, StoreError> { - let mut stmt = self - .db - .prepare("SELECT id, nonce, vault_root, storage_root, code_root FROM accounts") - .map_err(StoreError::QueryError)?; + pub async fn get_accounts(&self) -> Result, StoreError> { + Ok(accounts::Entity::find() + .all(&self.db) + .await + .unwrap() + .iter() + .map(|account| { + AccountStub::new( + (account.id as u64).try_into().unwrap(), + (account.nonce as u64).into(), + serde_json::from_str(&String::from_utf8(account.vault_root.clone()).unwrap()) + .unwrap(), + serde_json::from_str(&String::from_utf8(account.storage_root.clone()).unwrap()) + .unwrap(), + serde_json::from_str(&String::from_utf8(account.code_root.clone()).unwrap()) + .unwrap(), + ) + }) + .collect()) + } - let mut rows = stmt.query([]).map_err(StoreError::QueryError)?; - let mut result = Vec::new(); - while let Some(row) = rows.next().map_err(StoreError::QueryError)? { - // TODO: implement proper error handling and conversions - - let id: i64 = row.get(0).map_err(StoreError::QueryError)?; - let nonce: i64 = row.get(1).map_err(StoreError::QueryError)?; - - let vault_root: String = row.get(2).map_err(StoreError::QueryError)?; - let storage_root: String = row.get(3).map_err(StoreError::QueryError)?; - let code_root: String = row.get(4).map_err(StoreError::QueryError)?; - - result.push(AccountStub::new( - (id as u64) - .try_into() - .expect("Conversion from stored AccountID should not panic"), - (nonce as u64).into(), - serde_json::from_str(&vault_root).map_err(StoreError::DataDeserializationError)?, - serde_json::from_str(&storage_root) - .map_err(StoreError::DataDeserializationError)?, - serde_json::from_str(&code_root).map_err(StoreError::DataDeserializationError)?, - )); - } + pub async fn insert_account_with_metadata( + &mut self, + account: &Account, + ) -> Result<(), StoreError> { + let tx: DatabaseTransaction = self.db.begin().await.unwrap(); - Ok(result) - } + Self::insert_account_code(&tx, account.code()).await?; + Self::insert_account_storage(&tx, account.storage()).await?; + Self::insert_account_vault(&tx, account.vault()).await?; + Self::insert_account(&tx, account).await?; - pub fn insert_account(&self, account: &Account) -> Result<(), StoreError> { - let id: u64 = account.id().into(); - let code_root = serde_json::to_string(&account.code().root()) - .map_err(StoreError::InputSerializationError)?; - let storage_root = serde_json::to_string(&account.storage().root()) - .map_err(StoreError::InputSerializationError)?; - let vault_root = serde_json::to_string(&account.vault().commitment()) - .map_err(StoreError::InputSerializationError)?; - - self.db.execute( - "INSERT INTO accounts (id, code_root, storage_root, vault_root, nonce, committed) VALUES (?, ?, ?, ?, ?, ?)", - params![ - id as i64, - code_root, - storage_root, - vault_root, - account.nonce().inner() as i64, - account.is_on_chain(), - ], - ) - .map(|_| ()) - .map_err(StoreError::QueryError) + tx.commit().await.map_err(StoreError::QueryError)?; + Ok(()) } - pub fn insert_account_code(&self, account_code: &AccountCode) -> Result<(), StoreError> { - let code_root = serde_json::to_string(&account_code.root()) - .map_err(StoreError::InputSerializationError)?; - let code = serde_json::to_string(account_code.procedures()) - .map_err(StoreError::InputSerializationError)?; - let module = account_code.module().to_bytes(AstSerdeOptions { - serialize_imports: true, - }); - - self.db - .execute( - "INSERT INTO account_code (root, procedures, module) VALUES (?, ?, ?)", - params![code_root, code, module,], - ) - .map(|_| ()) - .map_err(StoreError::QueryError) + pub async fn insert_account_code( + tx: &DatabaseTransaction, + account_code: &AccountCode, + ) -> Result<(), StoreError> { + let account_code = account_code::ActiveModel { + root: Set(serde_json::to_string(&account_code.root()) + .unwrap() + .into_bytes()), + procedures: Set(serde_json::to_string(account_code.procedures()) + .unwrap() + .into_bytes()), + module: Set(account_code.module().to_bytes(AstSerdeOptions { + serialize_imports: true, + })), + }; + let _account_code = account_code + .insert(tx) + .await + .map_err(StoreError::QueryError)?; + + Ok(()) } - pub fn insert_account_storage( - &self, + pub async fn insert_account_storage( + tx: &DatabaseTransaction, account_storage: &AccountStorage, ) -> Result<(), StoreError> { - let storage_root = serde_json::to_string(&account_storage.root()) - .map_err(StoreError::InputSerializationError)?; - let storage_slots: BTreeMap = account_storage.slots().leaves().collect(); - let storage_slots = - serde_json::to_string(&storage_slots).map_err(StoreError::InputSerializationError)?; - - self.db - .execute( - "INSERT INTO account_storage (root, slots) VALUES (?, ?)", - params![storage_root, storage_slots], - ) - .map(|_| ()) - .map_err(StoreError::QueryError) - } + let storage_slots = serde_json::to_string(&storage_slots) + .map_err(StoreError::InputSerializationError) + .unwrap() + .into_bytes(); + + let account_storage = account_storage::ActiveModel { + root: Set(serde_json::to_string(&account_storage.root()) + .unwrap() + .into_bytes()), + slots: Set(storage_slots), + }; + + let _account_storage = account_storage + .insert(tx) + .await + .map_err(StoreError::QueryError)?; - pub fn insert_account_vault(&self, account_vault: &AccountVault) -> Result<(), StoreError> { - let vault_root = serde_json::to_string(&account_vault.commitment()) - .map_err(StoreError::InputSerializationError)?; + Ok(()) + } + pub async fn insert_account_vault( + tx: &DatabaseTransaction, + account_vault: &AccountVault, + ) -> Result<(), StoreError> { let assets: Vec = account_vault.assets().collect(); let assets = serde_json::to_string(&assets).map_err(StoreError::InputSerializationError)?; - self.db - .execute( - "INSERT INTO account_vaults (root, assets) VALUES (?, ?)", - params![vault_root, assets], - ) - .map(|_| ()) - .map_err(StoreError::QueryError) + let account_vault = account_vaults::ActiveModel { + root: Set(serde_json::to_string(&account_vault.commitment()) + .unwrap() + .into_bytes()), + assets: Set(serde_json::to_string(&assets).unwrap().into_bytes()), + }; + + let _account_vault = account_vault + .insert(tx) + .await + .map_err(StoreError::QueryError)?; + + Ok(()) + } + + pub async fn insert_account( + tx: &DatabaseTransaction, + account: &Account, + ) -> Result<(), StoreError> { + let id: u64 = account.id().into(); + let account = accounts::ActiveModel { + id: Set(id as i64), + code_root: Set(serde_json::to_string(&account.code().root()) + .unwrap() + .into_bytes()), + storage_root: Set(serde_json::to_string(&account.storage().root()) + .unwrap() + .into_bytes()), + vault_root: Set(serde_json::to_string(&account.vault().commitment()) + .unwrap() + .into_bytes()), + nonce: Set(account.nonce().inner() as i64), + committed: Set(account.is_on_chain()), + }; + + let _account = account.insert(tx).await.map_err(StoreError::QueryError)?; + + Ok(()) } // NOTES @@ -170,85 +200,88 @@ impl Store { /// 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::, _>>() + todo!() + // 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))? + todo!() + // 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(|_| ()) + todo!() + // 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(|_| ()) } } @@ -363,12 +396,41 @@ fn serialize_input_note( #[cfg(test)] pub mod tests { + use migration::{Migrator, MigratorTrait}; + use sea_orm::Database; use std::env::temp_dir; use uuid::Uuid; + use miden_lib::assembler::assembler; + use mock::mock::account; + + use super::Store; + 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 } + + async fn create_test_store() -> Store { + let temp_file = create_test_store_path(); + let url = format!("sqlite://{}?mode=rwc", temp_file.to_string_lossy()); + let db = Database::connect(&url) + .await + .expect("Failed to setup the database"); + Migrator::up(&db, None) + .await + .expect("Failed to run migrations for tests"); + Store { db } + } + + #[tokio::test] + async fn insert_same_account_twice_fails() { + let mut store = create_test_store().await; + let assembler = assembler(); + let account = account::mock_new_account(&assembler); + + assert!(store.insert_account_with_metadata(&account).await.is_ok()); + assert!(store.insert_account_with_metadata(&account).await.is_err()); + } } diff --git a/src/store/store.sql b/src/store/store.sql deleted file mode 100644 index 8e92129e0..000000000 --- a/src/store/store.sql +++ /dev/null @@ -1,61 +0,0 @@ --- 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. - 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. - PRIMARY KEY (root) -); - --- 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) -); - --- 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. - 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) -);