diff --git a/Makefile.toml b/Makefile.toml index aae4cea83..b40286e99 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -49,7 +49,7 @@ args = ["-rf", "miden-node"] description = "Clone or update miden-node repository and clean up files" script_runner = "bash" script = [ - 'if [ -d miden-node ]; then cd miden-node && git pull ; else git clone --branch next https://github.com/0xPolygonMiden/miden-node.git && cd miden-node; fi', + 'if [ -d miden-node ]; then cd miden-node && git checkout next && git pull origin next && cargo update; else git clone --branch next https://github.com/0xPolygonMiden/miden-node.git && cd miden-node && cargo update; fi', 'rm -rf miden-store.sqlite3 miden-store.sqlite3-wal miden-store.sqlite3-shm', 'cargo run --bin $NODE_BINARY --features $NODE_FEATURES_TESTING -- make-genesis --inputs-path node/genesis.toml --force' ] diff --git a/clippy.toml b/clippy.toml index d2fb6fd2f..49f302572 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1,2 @@ large-error-threshold = 256 +too-many-arguments-threshold = 8 diff --git a/docs/library.md b/docs/library.md index 342d4db17..e7b5d8d91 100644 --- a/docs/library.md +++ b/docs/library.md @@ -33,7 +33,18 @@ With the Miden client, you can create and track local (not on-chain) accounts. W let (new_account, account_seed) = client.new_account(client_template)?; ``` -The `AccountTemplate` enum defines which type of account will be created. Note that once an account is created, it will be kept locally and its state will automatically be tracked by the client. Any number of accounts can be created and stored by the client. +The `AccountTemplate` enum defines which type of account will be created. Note that once an account is created, it will be kept locally and its state will automatically be tracked by the client. Any number of accounts can be created and stored by the client. You can also create onchain accounts with: `accounts::AccountStorageMode::OnChain` + +```Rust + let account_template = AccountTemplate::BasicWallet { + mutable_code: false, + storage_mode: accounts::AccountStorageMode::OnChain, + }; + + let (new_account, account_seed) = client.new_account(client_template)?; +``` + +The account's state is also tracked locally, but during sync the client updates the account state by querying the node for outdated on-chain accounts. ## Execute a transaction diff --git a/src/cli/account.rs b/src/cli/account.rs index fe268db5b..c625ca357 100644 --- a/src/cli/account.rs +++ b/src/cli/account.rs @@ -1,6 +1,6 @@ use std::{fs, path::PathBuf}; -use clap::Parser; +use clap::{Parser, ValueEnum}; use comfy_table::{presets, Attribute, Cell, ContentArrangement, Table}; use miden_client::{ client::{accounts, rpc::NodeRpcClient, Client}, @@ -61,9 +61,15 @@ pub enum AccountCmd { #[clap()] pub enum AccountTemplate { /// Creates a basic account (Regular account with immutable code) - BasicImmutable, + BasicImmutable { + #[clap(short, long, value_enum, default_value_t = AccountStorageMode::OffChain)] + storage_mode: AccountStorageMode, + }, /// Creates a basic account (Regular account with mutable code) - BasicMutable, + BasicMutable { + #[clap(short, long, value_enum, default_value_t = AccountStorageMode::OffChain)] + storage_mode: AccountStorageMode, + }, /// Creates a faucet for fungible tokens FungibleFaucet { #[clap(short, long)] @@ -72,9 +78,36 @@ pub enum AccountTemplate { decimals: u8, #[clap(short, long)] max_supply: u64, + #[clap(short, long, value_enum, default_value_t = AccountStorageMode::OffChain)] + storage_mode: AccountStorageMode, }, /// Creates a faucet for non-fungible tokens - NonFungibleFaucet, + NonFungibleFaucet { + #[clap(short, long, value_enum, default_value_t = AccountStorageMode::OffChain)] + storage_mode: AccountStorageMode, + }, +} + +// TODO: Review this enum and variant names to have a consistent naming across all crates +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum AccountStorageMode { + OffChain, + OnChain, +} + +impl From for accounts::AccountStorageMode { + fn from(value: AccountStorageMode) -> Self { + match value { + AccountStorageMode::OffChain => accounts::AccountStorageMode::Local, + AccountStorageMode::OnChain => accounts::AccountStorageMode::OnChain, + } + } +} + +impl From<&AccountStorageMode> for accounts::AccountStorageMode { + fn from(value: &AccountStorageMode) -> Self { + accounts::AccountStorageMode::from(*value) + } } impl AccountCmd { @@ -88,26 +121,31 @@ impl AccountCmd { }, AccountCmd::New { template } => { let client_template = match template { - AccountTemplate::BasicImmutable => accounts::AccountTemplate::BasicWallet { - mutable_code: false, - storage_mode: accounts::AccountStorageMode::Local, + AccountTemplate::BasicImmutable { storage_mode } => { + accounts::AccountTemplate::BasicWallet { + mutable_code: false, + storage_mode: storage_mode.into(), + } }, - AccountTemplate::BasicMutable => accounts::AccountTemplate::BasicWallet { - mutable_code: true, - storage_mode: accounts::AccountStorageMode::Local, + AccountTemplate::BasicMutable { storage_mode } => { + accounts::AccountTemplate::BasicWallet { + mutable_code: true, + storage_mode: storage_mode.into(), + } }, AccountTemplate::FungibleFaucet { token_symbol, decimals, max_supply, + storage_mode, } => accounts::AccountTemplate::FungibleFaucet { token_symbol: TokenSymbol::new(token_symbol) .map_err(|err| format!("error: token symbol is invalid: {}", err))?, decimals: *decimals, max_supply: *max_supply, - storage_mode: accounts::AccountStorageMode::Local, + storage_mode: storage_mode.into(), }, - AccountTemplate::NonFungibleFaucet => todo!(), + AccountTemplate::NonFungibleFaucet { storage_mode: _ } => todo!(), }; let (_new_account, _account_seed) = client.new_account(client_template)?; }, diff --git a/src/client/accounts.rs b/src/client/accounts.rs index 966df2656..c130b9b65 100644 --- a/src/client/accounts.rs +++ b/src/client/accounts.rs @@ -30,11 +30,22 @@ pub enum AccountTemplate { }, } +// TODO: Review this enum and variant names to have a consistent naming across all crates +#[derive(Debug, Clone, Copy)] pub enum AccountStorageMode { Local, OnChain, } +impl From for AccountStorageType { + fn from(mode: AccountStorageMode) -> Self { + match mode { + AccountStorageMode::Local => AccountStorageType::OffChain, + AccountStorageMode::OnChain => AccountStorageType::OnChain, + } + } +} + impl Client { // ACCOUNT CREATION // -------------------------------------------------------------------------------------------- @@ -108,20 +119,11 @@ impl Client { } /// Creates a new regular account and saves it in the store along with its seed and auth data - /// - /// # Panics - /// - /// If the passed [AccountStorageMode] is [AccountStorageMode::OnChain], this function panics - /// since this feature is not currently supported on Miden fn new_basic_wallet( &mut self, mutable_code: bool, account_storage_mode: AccountStorageMode, ) -> Result<(Account, Word), ClientError> { - if let AccountStorageMode::OnChain = account_storage_mode { - todo!("Recording the account on chain is not supported yet"); - } - // TODO: This should be initialized with_rng let key_pair = SecretKey::new(); @@ -138,14 +140,14 @@ impl Client { init_seed, auth_scheme, AccountType::RegularAccountImmutableCode, - AccountStorageType::OffChain, + account_storage_mode.into(), ) } else { miden_lib::accounts::wallets::create_basic_wallet( init_seed, auth_scheme, AccountType::RegularAccountUpdatableCode, - AccountStorageType::OffChain, + account_storage_mode.into(), ) }?; @@ -160,10 +162,6 @@ impl Client { max_supply: u64, account_storage_mode: AccountStorageMode, ) -> Result<(Account, Word), ClientError> { - if let AccountStorageMode::OnChain = account_storage_mode { - todo!("On-chain accounts are not supported yet"); - } - // TODO: This should be initialized with_rng let key_pair = SecretKey::new(); @@ -181,7 +179,7 @@ impl Client { decimals, Felt::try_from(max_supply.to_le_bytes().as_slice()) .expect("u64 can be safely converted to a field element"), - AccountStorageType::OffChain, + account_storage_mode.into(), auth_scheme, )?; diff --git a/src/client/rpc/mod.rs b/src/client/rpc/mod.rs index b1840e9ec..b5ef844dc 100644 --- a/src/client/rpc/mod.rs +++ b/src/client/rpc/mod.rs @@ -2,7 +2,7 @@ use core::fmt; use async_trait::async_trait; use miden_objects::{ - accounts::AccountId, + accounts::{Account, AccountId}, crypto::merkle::{MerklePath, MmrDelta}, notes::{NoteId, NoteMetadata}, transaction::ProvenTransaction, @@ -56,6 +56,11 @@ pub trait NodeRpcClient { note_tags: &[u16], nullifiers_tags: &[u16], ) -> Result; + + async fn get_account_update( + &mut self, + account_id: AccountId, + ) -> Result; } // STATE SYNC INFO @@ -130,6 +135,7 @@ impl CommittedNote { // #[derive(Debug)] pub enum NodeRpcClientEndpoint { + GetAccountDetails, GetBlockHeaderByNumber, SyncState, SubmitProvenTx, @@ -141,6 +147,7 @@ impl fmt::Display for NodeRpcClientEndpoint { f: &mut fmt::Formatter<'_>, ) -> fmt::Result { match self { + NodeRpcClientEndpoint::GetAccountDetails => write!(f, "get_account_details"), NodeRpcClientEndpoint::GetBlockHeaderByNumber => { write!(f, "get_block_header_by_number") }, diff --git a/src/client/rpc/tonic_client.rs b/src/client/rpc/tonic_client.rs index b0e228e5f..06ce2f6a0 100644 --- a/src/client/rpc/tonic_client.rs +++ b/src/client/rpc/tonic_client.rs @@ -3,16 +3,18 @@ use miden_node_proto::{ errors::ConversionError, generated::{ requests::{ - GetBlockHeaderByNumberRequest, SubmitProvenTransactionRequest, SyncStateRequest, + GetAccountDetailsRequest, GetBlockHeaderByNumberRequest, + SubmitProvenTransactionRequest, SyncStateRequest, }, responses::SyncStateResponse, rpc::api_client::ApiClient, }, }; use miden_objects::{ - accounts::AccountId, + accounts::{Account, AccountId}, notes::{NoteId, NoteMetadata, NoteType}, transaction::ProvenTransaction, + utils::Deserializable, BlockHeader, Digest, }; use miden_tx::utils::Serializable; @@ -127,6 +129,45 @@ impl NodeRpcClient for TonicRpcClient { })?; response.into_inner().try_into() } + + /// TODO: fill description + async fn get_account_update( + &mut self, + account_id: AccountId, + ) -> Result { + if !account_id.is_on_chain() { + return Err(NodeRpcClientError::InvalidAccountReceived( + "should only get updates for offchain accounts".to_string(), + )); + } + + let account_id = account_id.into(); + let request = GetAccountDetailsRequest { + account_id: Some(account_id), + }; + + let rpc_api = self.rpc_api().await?; + + let response = rpc_api.get_account_details(request).await.map_err(|err| { + NodeRpcClientError::RequestError( + NodeRpcClientEndpoint::GetAccountDetails.to_string(), + err.to_string(), + ) + })?; + let response = response.into_inner(); + let account_info = response.account.ok_or(NodeRpcClientError::ExpectedFieldMissing( + "GetAccountDetails response should have an `account`".to_string(), + ))?; + + let details_bytes = + account_info.details.ok_or(NodeRpcClientError::ExpectedFieldMissing( + "GetAccountDetails response's account should have `details`".to_string(), + ))?; + + let details = Account::read_from_bytes(&details_bytes)?; + + Ok(details) + } } // STATE SYNC INFO CONVERSION diff --git a/src/client/sync.rs b/src/client/sync.rs index 730ae6d23..34170750b 100644 --- a/src/client/sync.rs +++ b/src/client/sync.rs @@ -1,12 +1,12 @@ use crypto::merkle::{InOrderIndex, MmrDelta, MmrPeaks, PartialMmr}; use miden_objects::{ - accounts::{AccountId, AccountStub}, + accounts::{Account, AccountId, AccountStub}, crypto::{self, rand::FeltRng}, notes::{NoteId, NoteInclusionProof}, transaction::TransactionId, BlockHeader, Digest, }; -use tracing::warn; +use tracing::{info, warn}; use super::{ rpc::{CommittedNote, NodeRpcClient}, @@ -137,8 +137,13 @@ impl Client { let committed_notes = self.build_inclusion_proofs(response.note_inclusions, &response.block_header)?; - // Check if the returned account hashes match latest account hashes in the database - check_account_hashes(&response.account_hash_updates, &accounts)?; + let (onchain_accounts, offchain_accounts): (Vec<_>, Vec<_>) = + accounts.into_iter().partition(|account_stub| account_stub.id().is_on_chain()); + + let updated_onchain_accounts = self + .get_updated_onchain_accounts(&response.account_hash_updates, &onchain_accounts) + .await?; + self.validate_local_account_hashes(&response.account_hash_updates, &offchain_accounts)?; // Derive new nullifiers data let new_nullifiers = self.get_new_nullifiers(response.nullifiers)?; @@ -179,6 +184,7 @@ impl Client { &transactions_to_commit, new_peaks, &new_authentication_nodes, + &updated_onchain_accounts, ) .map_err(ClientError::StoreError)?; @@ -287,6 +293,47 @@ impl Client { Ok(new_nullifiers) } + + async fn get_updated_onchain_accounts( + &mut self, + account_updates: &[(AccountId, Digest)], + current_onchain_accounts: &[AccountStub], + ) -> Result, ClientError> { + let mut accounts_to_update: Vec = Vec::new(); + for (remote_account_id, remote_account_hash) in account_updates { + // check if this updated account is tracked by the client + let current_account = current_onchain_accounts + .iter() + .find(|acc| *remote_account_id == acc.id() && *remote_account_hash != acc.hash()); + + if let Some(tracked_account) = current_account { + info!("On-chain account hash difference detected for account with ID: {}. Fetching node for updates...", tracked_account.id()); + let account = self.rpc_api.get_account_update(tracked_account.id()).await?; + accounts_to_update.push(account); + } + } + Ok(accounts_to_update) + } + + /// Validates account hash updates and returns an error if there is a mismatch. + fn validate_local_account_hashes( + &mut self, + account_updates: &[(AccountId, Digest)], + current_offchain_accounts: &[AccountStub], + ) -> Result<(), ClientError> { + for (remote_account_id, remote_account_hash) in account_updates { + // ensure that if we track that account, it has the same hash + let mismatched_accounts = current_offchain_accounts + .iter() + .find(|acc| *remote_account_id == acc.id() && *remote_account_hash != acc.hash()); + + // OffChain accounts should always have the latest known state + if mismatched_accounts.is_some() { + return Err(StoreError::AccountHashMismatch(*remote_account_id).into()); + } + } + Ok(()) + } } // UTILS @@ -318,25 +365,6 @@ fn apply_mmr_changes( Ok((partial_mmr.peaks(), new_authentication_nodes)) } -/// Validates account hash updates and returns an error if there is a mismatch. -fn check_account_hashes( - account_updates: &[(AccountId, Digest)], - current_accounts: &[AccountStub], -) -> Result<(), StoreError> { - for (remote_account_id, remote_account_hash) in account_updates { - { - if let Some(local_account) = - current_accounts.iter().find(|acc| *remote_account_id == acc.id()) - { - if *remote_account_hash != local_account.hash() { - return Err(StoreError::AccountHashMismatch(*remote_account_id)); - } - } - } - } - Ok(()) -} - /// Returns the list of transactions that should be marked as committed based on the state update info /// /// To set an uncommitted transaction as committed three things must hold: diff --git a/src/client/transactions/mod.rs b/src/client/transactions/mod.rs index e6eaa15f9..7300dcca9 100644 --- a/src/client/transactions/mod.rs +++ b/src/client/transactions/mod.rs @@ -236,7 +236,7 @@ impl Client { account_id, block_num, ¬e_ids, - Some(transaction_request.into()), + transaction_request.into(), )?; // Check that the expected output notes is a subset of the transaction's output notes diff --git a/src/client/transactions/transaction_request.rs b/src/client/transactions/transaction_request.rs index 0963084fc..4b6f8ab1a 100644 --- a/src/client/transactions/transaction_request.rs +++ b/src/client/transactions/transaction_request.rs @@ -5,6 +5,7 @@ use miden_objects::{ assets::{Asset, FungibleAsset}, notes::{Note, NoteId}, transaction::{TransactionArgs, TransactionScript}, + vm::AdviceMap, Word, }; @@ -88,7 +89,12 @@ impl TransactionRequest { impl From for TransactionArgs { fn from(val: TransactionRequest) -> Self { let note_args = val.get_note_args(); - TransactionArgs::new(val.tx_script, Some(note_args)) + let mut tx_args = TransactionArgs::new(val.tx_script, Some(note_args), AdviceMap::new()); + + let output_notes = val.expected_output_notes.into_iter(); + tx_args.extend_expected_output_notes(output_notes); + + tx_args } } diff --git a/src/mock.rs b/src/mock.rs index babf4e138..9fc6013d1 100644 --- a/src/mock.rs +++ b/src/mock.rs @@ -134,6 +134,13 @@ impl NodeRpcClient for MockRpcApi { // TODO: add some basic validations to test error cases Ok(()) } + + async fn get_account_update( + &mut self, + _account_id: AccountId, + ) -> Result { + panic!("shouldn't be used for now") + } } // HELPERS diff --git a/src/store/mod.rs b/src/store/mod.rs index d9d35ccdb..c9e8593af 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -250,6 +250,7 @@ pub trait Store { committed_transactions: &[TransactionId], new_mmr_peaks: MmrPeaks, new_authentication_nodes: &[(InOrderIndex, Digest)], + updated_onchain_accounts: &[Account], ) -> Result<(), StoreError>; } diff --git a/src/store/sqlite_store/accounts.rs b/src/store/sqlite_store/accounts.rs index 58fc54e50..ac6895bc1 100644 --- a/src/store/sqlite_store/accounts.rs +++ b/src/store/sqlite_store/accounts.rs @@ -189,6 +189,20 @@ impl SqliteStore { // HELPERS // ================================================================================================ +/// Update previously-existing account after a transaction execution +/// +/// Because the Client retrieves the account by account ID before applying the delta, we don't +/// need to check that it exists here. This inserts a new row into the accounts table. +/// We can later identify the proper account state by looking at the nonce. +pub(crate) fn update_account( + tx: &Transaction<'_>, + new_account_state: &Account, +) -> Result<(), StoreError> { + insert_account_storage(tx, new_account_state.storage())?; + insert_account_asset_vault(tx, new_account_state.vault())?; + insert_account_record(tx, new_account_state, None) +} + pub(super) fn insert_account_record( tx: &Transaction<'_>, account: &Account, diff --git a/src/store/sqlite_store/mod.rs b/src/store/sqlite_store/mod.rs index 3b0388d0e..8e54ceb8f 100644 --- a/src/store/sqlite_store/mod.rs +++ b/src/store/sqlite_store/mod.rs @@ -132,6 +132,7 @@ impl Store for SqliteStore { committed_transactions: &[TransactionId], new_mmr_peaks: MmrPeaks, new_authentication_nodes: &[(InOrderIndex, Digest)], + updated_onchain_accounts: &[Account], ) -> Result<(), StoreError> { self.apply_state_sync( block_header, @@ -140,6 +141,7 @@ impl Store for SqliteStore { committed_transactions, new_mmr_peaks, new_authentication_nodes, + updated_onchain_accounts, ) } diff --git a/src/store/sqlite_store/sync.rs b/src/store/sqlite_store/sync.rs index 766b6b762..9d30f1e2e 100644 --- a/src/store/sqlite_store/sync.rs +++ b/src/store/sqlite_store/sync.rs @@ -1,4 +1,5 @@ use miden_objects::{ + accounts::Account, crypto::merkle::{InOrderIndex, MmrPeaks}, notes::{NoteId, NoteInclusionProof}, transaction::TransactionId, @@ -7,7 +8,7 @@ use miden_objects::{ use rusqlite::{named_params, params}; use super::SqliteStore; -use crate::errors::StoreError; +use crate::{errors::StoreError, store::sqlite_store::accounts::update_account}; impl SqliteStore { pub(crate) fn get_note_tags(&self) -> Result, StoreError> { @@ -65,6 +66,7 @@ impl SqliteStore { committed_transactions: &[TransactionId], new_mmr_peaks: MmrPeaks, new_authentication_nodes: &[(InOrderIndex, Digest)], + updated_onchain_accounts: &[Account], ) -> Result<(), StoreError> { let tx = self.db.transaction()?; @@ -139,6 +141,11 @@ impl SqliteStore { committed_transactions, )?; + // Update onchain accounts on the db that have been updated onchain + for account in updated_onchain_accounts { + update_account(&tx, account)?; + } + // Commit the updates tx.commit()?; diff --git a/src/store/sqlite_store/transactions.rs b/src/store/sqlite_store/transactions.rs index 43bdefee3..8b227b1fb 100644 --- a/src/store/sqlite_store/transactions.rs +++ b/src/store/sqlite_store/transactions.rs @@ -1,7 +1,7 @@ use alloc::collections::BTreeMap; use miden_objects::{ - accounts::{Account, AccountId}, + accounts::AccountId, assembly::{AstSerdeOptions, ProgramAst}, crypto::utils::{Deserializable, Serializable}, transaction::{OutputNotes, ToNullifier, TransactionId, TransactionScript}, @@ -11,7 +11,7 @@ use rusqlite::{params, Transaction}; use tracing::info; use super::{ - accounts::{insert_account_asset_vault, insert_account_record, insert_account_storage}, + accounts::update_account, notes::{insert_input_note_tx, insert_output_note_tx}, SqliteStore, }; @@ -107,7 +107,7 @@ impl SqliteStore { insert_proven_transaction_data(&tx, tx_result)?; // Account Data - update_account(&tx, account)?; + update_account(&tx, &account)?; // Updates for notes @@ -148,20 +148,6 @@ impl SqliteStore { } } -/// Update previously-existing account after a transaction execution -/// -/// Because the Client retrieves the account by account ID before applying the delta, we don't -/// need to check that it exists here. This inserts a new row into the accounts table. -/// We can later identify the proper account state by looking at the nonce. -fn update_account( - tx: &Transaction<'_>, - new_account_state: Account, -) -> Result<(), StoreError> { - insert_account_storage(tx, new_account_state.storage())?; - insert_account_asset_vault(tx, new_account_state.vault())?; - insert_account_record(tx, &new_account_state, None) -} - pub(super) fn insert_proven_transaction_data( tx: &Transaction<'_>, transaction_result: TransactionResult, diff --git a/tests/integration/main.rs b/tests/integration/main.rs index 376dccb9e..4f5b9f1fe 100644 --- a/tests/integration/main.rs +++ b/tests/integration/main.rs @@ -120,7 +120,10 @@ const MINT_AMOUNT: u64 = 1000; const TRANSFER_AMOUNT: u64 = 50; /// Sets up a basic client and returns (basic_account, basic_account, faucet_account) -async fn setup(client: &mut TestClient) -> (Account, Account, Account) { +async fn setup( + client: &mut TestClient, + accounts_storage_mode: AccountStorageMode, +) -> (Account, Account, Account) { // Enusre clean state assert!(client.get_accounts().unwrap().is_empty()); assert!(client.get_transactions(TransactionFilter::All).unwrap().is_empty()); @@ -132,7 +135,7 @@ async fn setup(client: &mut TestClient) -> (Account, Account, Account) { token_symbol: TokenSymbol::new("MATIC").unwrap(), decimals: 8, max_supply: 1_000_000_000, - storage_mode: AccountStorageMode::Local, + storage_mode: accounts_storage_mode, }) .unwrap(); @@ -212,7 +215,7 @@ async fn consume_notes( async fn test_added_notes() { let mut client = create_test_client(); - let (_, _, faucet_account_stub) = setup(&mut client).await; + let (_, _, faucet_account_stub) = setup(&mut client, AccountStorageMode::Local).await; // Mint some asset for an account not tracked by the client. It should not be stored as an // input note afterwards since it is not being tracked by the client let fungible_asset = FungibleAsset::new(faucet_account_stub.id(), MINT_AMOUNT).unwrap(); @@ -235,7 +238,7 @@ async fn test_p2id_transfer() { let mut client = create_test_client(); let (first_regular_account, second_regular_account, faucet_account_stub) = - setup(&mut client).await; + setup(&mut client, AccountStorageMode::Local).await; let from_account_id = first_regular_account.id(); let to_account_id = second_regular_account.id(); @@ -303,7 +306,7 @@ async fn test_p2idr_transfer() { let mut client = create_test_client(); let (first_regular_account, second_regular_account, faucet_account_stub) = - setup(&mut client).await; + setup(&mut client, AccountStorageMode::Local).await; let from_account_id = first_regular_account.id(); let to_account_id = second_regular_account.id(); @@ -620,3 +623,57 @@ fn create_custom_note( let note_recipient = NoteRecipient::new(serial_num, note_script, inputs); Note::new(note_assets, note_metadata, note_recipient) } + +#[tokio::test] +async fn test_onchain_mint() { + let mut client_1 = create_test_client(); + let mut client_2 = create_test_client(); + + let (first_regular_account, _second_regular_account, faucet_account_stub) = + setup(&mut client_1, AccountStorageMode::OnChain).await; + + let ( + second_client_first_regular_account, + _other_second_regular_account, + _other_faucet_account_stub, + ) = setup(&mut client_2, AccountStorageMode::Local).await; + + let target_account_id = first_regular_account.id(); + let second_client_target_account_id = second_client_first_regular_account.id(); + let faucet_account_id = faucet_account_stub.id(); + + let (_, faucet_seed) = client_1.get_account_stub_by_id(faucet_account_id).unwrap(); + let auth_info = client_1.get_account_auth(faucet_account_id).unwrap(); + client_2.insert_account(&faucet_account_stub, faucet_seed, &auth_info).unwrap(); + + // First Mint necesary token + println!("First client consuming note"); + let note = mint_note(&mut client_1, target_account_id, faucet_account_id).await; + + // Update the state in the other client and ensure the onchain faucet hash is consistent + // between clients + client_2.sync_state().await.unwrap(); + + let (client_1_faucet, _) = client_1.get_account_stub_by_id(faucet_account_stub.id()).unwrap(); + let (client_2_faucet, _) = client_2.get_account_stub_by_id(faucet_account_stub.id()).unwrap(); + + assert_eq!(client_1_faucet.hash(), client_2_faucet.hash()); + + // Now use the faucet in the second client to mint to its own account + println!("Second client consuming note"); + let second_client_note = + mint_note(&mut client_2, second_client_target_account_id, faucet_account_id).await; + + // Update the state in the other client and ensure the onchain faucet hash is consistent + // between clients + client_1.sync_state().await.unwrap(); + + println!("about to consume"); + consume_notes(&mut client_1, target_account_id, &[note]).await; + consume_notes(&mut client_2, second_client_target_account_id, &[second_client_note]).await; + + let (client_1_faucet, _) = client_1.get_account_stub_by_id(faucet_account_stub.id()).unwrap(); + let (client_2_faucet, _) = client_2.get_account_stub_by_id(faucet_account_stub.id()).unwrap(); + + assert_eq!(client_1_faucet.hash(), client_2_faucet.hash()); +}