From c8c9ced4211041dc9f501291f84a23ce83d35eff Mon Sep 17 00:00:00 2001 From: Marvin Jones Date: Wed, 17 Jun 2026 13:53:54 -0400 Subject: [PATCH] add helper functions --- .../src/components/account_nonce_list.rs | 62 + lez/explorer_service/src/components/mod.rs | 9 + .../src/components/search_results.rs | 97 + .../src/components/transaction_details.rs | 165 ++ lez/explorer_service/src/pages/main_page.rs | 93 +- .../src/pages/transaction_page.rs | 258 +-- lez/indexer/core/src/block_store.rs | 79 +- lez/indexer/service/src/mock_service.rs | 132 +- lez/keycard_wallet/src/lib.rs | 105 +- lez/keycard_wallet/src/python_path.rs | 26 +- lez/sequencer/core/src/lib.rs | 139 +- lez/storage/src/indexer/mod.rs | 166 +- lez/storage/src/indexer/write_atomic.rs | 38 +- lez/wallet/src/account_manager.rs | 176 +- lez/wallet/src/cli/account.rs | 638 +++--- lez/wallet/src/cli/config.rs | 215 +- lez/wallet/src/cli/group.rs | 247 +-- lez/wallet/src/cli/keycard.rs | 283 +-- lez/wallet/src/cli/mod.rs | 20 + lez/wallet/src/cli/programs/amm.rs | 365 ++-- lez/wallet/src/cli/programs/ata.rs | 313 +-- .../src/cli/programs/native_token_transfer.rs | 550 ++--- lez/wallet/src/cli/programs/token.rs | 1821 +++++++++-------- lez/wallet/src/lib.rs | 26 + 24 files changed, 3271 insertions(+), 2752 deletions(-) create mode 100644 lez/explorer_service/src/components/account_nonce_list.rs create mode 100644 lez/explorer_service/src/components/search_results.rs create mode 100644 lez/explorer_service/src/components/transaction_details.rs diff --git a/lez/explorer_service/src/components/account_nonce_list.rs b/lez/explorer_service/src/components/account_nonce_list.rs new file mode 100644 index 00000000..661d8c58 --- /dev/null +++ b/lez/explorer_service/src/components/account_nonce_list.rs @@ -0,0 +1,62 @@ +use indexer_service_protocol::AccountId; +use itertools::{EitherOrBoth, Itertools as _}; +use leptos::prelude::*; +use leptos_router::components::A; + +#[component] +#[expect( + clippy::needless_pass_by_value, + reason = "Leptos component props are passed by value by framework convention" +)] +pub fn AccountNonceList(account_ids: Vec, nonces: Vec) -> impl IntoView { + view! { +
+ {account_ids + .into_iter() + .zip_longest(nonces.into_iter()) + .map(|maybe_pair| { + match maybe_pair { + EitherOrBoth::Both(account_id, nonce) => { + let account_id_str = account_id.to_string(); + view! { + + } + } + EitherOrBoth::Left(account_id) => { + let account_id_str = account_id.to_string(); + view! { + + } + } + EitherOrBoth::Right(_) => { + view! { + + } + } + } + }) + .collect::>()} +
+ } +} diff --git a/lez/explorer_service/src/components/mod.rs b/lez/explorer_service/src/components/mod.rs index 306c79a8..d31c8336 100644 --- a/lez/explorer_service/src/components/mod.rs +++ b/lez/explorer_service/src/components/mod.rs @@ -2,6 +2,15 @@ pub use account_preview::AccountPreview; pub use block_preview::BlockPreview; pub use transaction_preview::TransactionPreview; +pub mod account_nonce_list; pub mod account_preview; pub mod block_preview; +pub mod search_results; +pub mod transaction_details; pub mod transaction_preview; + +pub use account_nonce_list::AccountNonceList; +pub use search_results::SearchResultsView; +pub use transaction_details::{ + PrivacyPreservingTxDetails, ProgramDeploymentTxDetails, PublicTxDetails, +}; diff --git a/lez/explorer_service/src/components/search_results.rs b/lez/explorer_service/src/components/search_results.rs new file mode 100644 index 00000000..a01aa066 --- /dev/null +++ b/lez/explorer_service/src/components/search_results.rs @@ -0,0 +1,97 @@ +use leptos::prelude::*; + +use super::{AccountPreview, BlockPreview, TransactionPreview}; +use crate::api::SearchResults; + +/// Search results view component +#[component] +#[expect( + clippy::needless_pass_by_value, + reason = "Leptos component props are passed by value by framework convention" +)] +pub fn SearchResultsView(results: SearchResults) -> impl IntoView { + let SearchResults { + blocks, + transactions, + accounts, + } = results; + let has_results = !blocks.is_empty() || !transactions.is_empty() || !accounts.is_empty(); + + view! { +
+

"Search Results"

+ {if has_results { + view! { +
+ {if blocks.is_empty() { + ().into_any() + } else { + view! { +
+

"Blocks"

+
+ {blocks + .into_iter() + .map(|block| { + view! { } + }) + .collect::>()} +
+
+ } + .into_any() + }} + + {if transactions.is_empty() { + ().into_any() + } else { + view! { +
+

"Transactions"

+
+ {transactions + .into_iter() + .map(|tx| { + view! { } + }) + .collect::>()} +
+
+ } + .into_any() + }} + + {if accounts.is_empty() { + ().into_any() + } else { + view! { +
+

"Accounts"

+
+ {accounts + .into_iter() + .map(|(id, account)| { + view! { + + } + }) + .collect::>()} +
+
+ } + .into_any() + }} + +
+ } + .into_any() + } else { + view! {
"No results found"
} + .into_any() + }} +
+ } +} diff --git a/lez/explorer_service/src/components/transaction_details.rs b/lez/explorer_service/src/components/transaction_details.rs new file mode 100644 index 00000000..a79ab275 --- /dev/null +++ b/lez/explorer_service/src/components/transaction_details.rs @@ -0,0 +1,165 @@ +use indexer_service_protocol::{ + PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage, + ProgramDeploymentTransaction, PublicMessage, PublicTransaction, WitnessSet, +}; +use leptos::prelude::*; + +use super::AccountNonceList; + +/// Public transaction details component +#[component] +#[expect( + clippy::needless_pass_by_value, + reason = "Leptos component props are passed by value by framework convention" +)] +pub fn PublicTxDetails(tx: PublicTransaction) -> impl IntoView { + let PublicTransaction { + hash: _, + message, + witness_set, + } = tx; + let PublicMessage { + program_id, + account_ids, + nonces, + instruction_data, + } = message; + let WitnessSet { + signatures_and_public_keys, + proof, + } = witness_set; + + let program_id_str = program_id.to_string(); + let proof_len = proof.map_or(0, |p| p.0.len()); + let signatures_count = signatures_and_public_keys.len(); + + view! { +
+

"Public Transaction Details"

+
+
+ "Program ID:" + {program_id_str} +
+
+ "Instruction Data:" + + {format!("{} u32 values", instruction_data.len())} + +
+
+ "Proof Size:" + {format!("{proof_len} bytes")} +
+
+ "Signatures:" + {signatures_count.to_string()} +
+
+ +

"Accounts"

+ +
+ } +} + +/// Privacy-preserving transaction details component +#[component] +#[expect( + clippy::needless_pass_by_value, + reason = "Leptos component props are passed by value by framework convention" +)] +pub fn PrivacyPreservingTxDetails(tx: PrivacyPreservingTransaction) -> impl IntoView { + let PrivacyPreservingTransaction { + hash: _, + message, + witness_set, + } = tx; + let PrivacyPreservingMessage { + public_account_ids, + nonces, + public_post_states: _, + encrypted_private_post_states, + new_commitments, + new_nullifiers, + block_validity_window, + timestamp_validity_window, + } = message; + let WitnessSet { + signatures_and_public_keys: _, + proof, + } = witness_set; + let proof_len = proof.map_or(0, |p| p.0.len()); + + view! { +
+

"Privacy-Preserving Transaction Details"

+
+
+ "Public Accounts:" + + {public_account_ids.len().to_string()} + +
+
+ "New Commitments:" + {new_commitments.len().to_string()} +
+
+ "Nullifiers:" + {new_nullifiers.len().to_string()} +
+
+ "Encrypted States:" + + {encrypted_private_post_states.len().to_string()} + +
+
+ "Proof Size:" + {format!("{proof_len} bytes")} +
+
+ "Block Validity Window:" + {block_validity_window.to_string()} +
+
+ "Timestamp Validity Window:" + {timestamp_validity_window.to_string()} +
+
+ +

"Public Accounts"

+ +
+ } +} + +/// Program deployment transaction details component +#[component] +#[expect( + clippy::needless_pass_by_value, + reason = "Leptos component props are passed by value by framework convention" +)] +pub fn ProgramDeploymentTxDetails(tx: ProgramDeploymentTransaction) -> impl IntoView { + let ProgramDeploymentTransaction { + hash: _, + message, + } = tx; + let ProgramDeploymentMessage { bytecode } = message; + + let bytecode_len = bytecode.len(); + view! { +
+

"Program Deployment Transaction Details"

+
+
+ "Bytecode Size:" + + {format!("{bytecode_len} bytes")} + +
+
+
+ } +} diff --git a/lez/explorer_service/src/pages/main_page.rs b/lez/explorer_service/src/pages/main_page.rs index 7e26e794..831182ce 100644 --- a/lez/explorer_service/src/pages/main_page.rs +++ b/lez/explorer_service/src/pages/main_page.rs @@ -6,8 +6,8 @@ use leptos_router::{ use web_sys::SubmitEvent; use crate::{ - api::{self, SearchResults}, - components::{AccountPreview, BlockPreview, TransactionPreview}, + api, + components::{BlockPreview, SearchResultsView}, }; const RECENT_BLOCKS_LIMIT: u64 = 10; @@ -138,93 +138,8 @@ pub fn MainPage() -> impl IntoView { .get() .and_then(|opt_results| opt_results) .map(|results| { - let SearchResults { - blocks, - transactions, - accounts, - } = results; - let has_results = !blocks.is_empty() - || !transactions.is_empty() - || !accounts.is_empty(); - view! { -
-

"Search Results"

- {if has_results { - view! { -
- {if blocks.is_empty() { - ().into_any() - } else { - view! { -
-

"Blocks"

-
- {blocks - .into_iter() - .map(|block| { - view! { } - }) - .collect::>()} -
-
- } - .into_any() - }} - - {if transactions.is_empty() { - ().into_any() - } else { - view! { -
-

"Transactions"

-
- {transactions - .into_iter() - .map(|tx| { - view! { } - }) - .collect::>()} -
-
- } - .into_any() - }} - - {if accounts.is_empty() { - ().into_any() - } else { - view! { -
-

"Accounts"

-
- {accounts - .into_iter() - .map(|(id, account)| { - view! { - - } - }) - .collect::>()} -
-
- } - .into_any() - }} - -
- } - .into_any() - } else { - view! {
"No results found"
} - .into_any() - }} -
- } - .into_any() - }) + view! { }.into_any() + }) }} diff --git a/lez/explorer_service/src/pages/transaction_page.rs b/lez/explorer_service/src/pages/transaction_page.rs index 0a3fc8e2..04ad1399 100644 --- a/lez/explorer_service/src/pages/transaction_page.rs +++ b/lez/explorer_service/src/pages/transaction_page.rs @@ -1,14 +1,11 @@ use std::str::FromStr as _; -use indexer_service_protocol::{ - HashType, PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage, - ProgramDeploymentTransaction, PublicMessage, PublicTransaction, Transaction, WitnessSet, -}; -use itertools::{EitherOrBoth, Itertools as _}; +use indexer_service_protocol::{HashType, Transaction}; use leptos::prelude::*; -use leptos_router::{components::A, hooks::use_params_map}; +use leptos_router::hooks::use_params_map; use crate::api; +use crate::components::{PrivacyPreservingTxDetails, ProgramDeploymentTxDetails, PublicTxDetails}; /// Transaction page component #[component] @@ -66,244 +63,21 @@ pub fn TransactionPage() -> impl IntoView { { match tx { - Transaction::Public(ptx) => { - let PublicTransaction { - hash: _, - message, - witness_set, - } = ptx; - let PublicMessage { - program_id, - account_ids, - nonces, - instruction_data, - } = message; - let WitnessSet { - signatures_and_public_keys, - proof, - } = witness_set; + Transaction::Public(ptx) => { + view! { }.into_any() + } + Transaction::PrivacyPreserving(pptx) => { + view! { }.into_any() + } + Transaction::ProgramDeployment(pdtx) => { + view! { }.into_any() + } + } + } - let program_id_str = program_id.to_string(); - let proof_len = proof.map_or(0, |p| p.0.len()); - let signatures_count = signatures_and_public_keys.len(); - - view! { -
-

"Public Transaction Details"

-
-
- "Program ID:" - {program_id_str} -
-
- "Instruction Data:" - - {format!("{} u32 values", instruction_data.len())} - -
-
- "Proof Size:" - {format!("{proof_len} bytes")} -
-
- "Signatures:" - {signatures_count.to_string()} -
-
- -

"Accounts"

-
- {account_ids - .into_iter() - .zip_longest(nonces.into_iter()) - .map(|maybe_pair| { - match maybe_pair { - EitherOrBoth::Both(account_id, nonce) => { - let account_id_str = account_id.to_string(); - view! { - - } - } - EitherOrBoth::Left(account_id) => { - let account_id_str = account_id.to_string(); - view! { - - } - } - EitherOrBoth::Right(_) => { - view! { - - } - } - } - }) - .collect::>()} -
-
- } - .into_any() + } - Transaction::PrivacyPreserving(pptx) => { - let PrivacyPreservingTransaction { - hash: _, - message, - witness_set, - } = pptx; - let PrivacyPreservingMessage { - public_account_ids, - nonces, - public_post_states: _, - encrypted_private_post_states, - new_commitments, - new_nullifiers, - block_validity_window, - timestamp_validity_window, - } = message; - let WitnessSet { - signatures_and_public_keys: _, - proof, - } = witness_set; - let proof_len = proof.map_or(0, |p| p.0.len()); - view! { -
-

"Privacy-Preserving Transaction Details"

-
-
- "Public Accounts:" - - {public_account_ids.len().to_string()} - -
-
- "New Commitments:" - {new_commitments.len().to_string()} -
-
- "Nullifiers:" - {new_nullifiers.len().to_string()} -
-
- "Encrypted States:" - - {encrypted_private_post_states.len().to_string()} - -
-
- "Proof Size:" - {format!("{proof_len} bytes")} -
-
- "Block Validity Window:" - {block_validity_window.to_string()} -
-
- "Timestamp Validity Window:" - {timestamp_validity_window.to_string()} -
-
- -

"Public Accounts"

-
- {public_account_ids - .into_iter() - .zip_longest(nonces.into_iter()) - .map(|maybe_pair| { - match maybe_pair { - EitherOrBoth::Both(account_id, nonce) => { - let account_id_str = account_id.to_string(); - view! { - - } - } - EitherOrBoth::Left(account_id) => { - let account_id_str = account_id.to_string(); - view! { - - } - } - EitherOrBoth::Right(_) => { - view! { - - } - } - } - }) - .collect::>()} -
-
- } - .into_any() - } - Transaction::ProgramDeployment(pdtx) => { - let ProgramDeploymentTransaction { - hash: _, - message, - } = pdtx; - let ProgramDeploymentMessage { bytecode } = message; - - let bytecode_len = bytecode.len(); - view! { -
-

"Program Deployment Transaction Details"

-
-
- "Bytecode Size:" - - {format!("{bytecode_len} bytes")} - -
-
-
- } - .into_any() - } - }} - - - } - .into_any() + .into_any() } Err(e) => { view! { diff --git a/lez/indexer/core/src/block_store.rs b/lez/indexer/core/src/block_store.rs index f00c94c5..0f624221 100644 --- a/lez/indexer/core/src/block_store.rs +++ b/lez/indexer/core/src/block_store.rs @@ -222,10 +222,50 @@ mod tests { assert_eq!(final_id, None); } + struct TestFixture { + storage: IndexerStore, + from: AccountId, + to: AccountId, + _home: tempfile::TempDir, + } + + async fn store_with_transfer_blocks( + block_count: u64, + prev_hash: Option, + ) -> TestFixture { + let home = tempdir().unwrap(); + let storage = IndexerStore::open_db(home.path()).unwrap(); + + let initial_accounts = initial_pub_accounts_private_keys(); + let from = initial_accounts[0].account_id; + let to = initial_accounts[1].account_id; + let sign_key = initial_accounts[0].pub_sign_key.clone(); + + let mut prev_hash = prev_hash; + for i in 0..block_count { + let tx = common::test_utils::create_transaction_native_token_transfer( + from, u128::from(i), to, 10, &sign_key, + ); + let block_id = i + 1; + + let next_block = common::test_utils::produce_dummy_block(block_id, prev_hash, vec![tx]); + prev_hash = Some(next_block.header.hash); + + storage + .put_block( + next_block, + HeaderId::from([u8::try_from(i + 1).unwrap(); 32]), + ) + .await + .unwrap(); + } + + TestFixture { storage, from, to, _home: home } + } + #[tokio::test] async fn state_transition() { let home = tempdir().unwrap(); - let storage = IndexerStore::open_db(home.as_ref()).unwrap(); let initial_accounts = initial_pub_accounts_private_keys(); @@ -233,7 +273,6 @@ mod tests { let to = initial_accounts[1].account_id; let sign_key = initial_accounts[0].pub_sign_key.clone(); - // Submit genesis block let clock_tx = LeeTransaction::Public(clock_invocation(0)); let genesis_block_data = HashableBlockData { block_id: 1, @@ -249,15 +288,13 @@ mod tests { .await .unwrap(); - for i in 0..10 { + for i in 0..10_u128 { let tx = common::test_utils::create_transaction_native_token_transfer( from, i, to, 10, &sign_key, ); let block_id = u64::try_from(i + 1).unwrap(); - let next_block = common::test_utils::produce_dummy_block(block_id, prev_hash, vec![tx]); prev_hash = Some(next_block.header.hash); - storage .put_block( next_block, @@ -276,48 +313,18 @@ mod tests { #[tokio::test] async fn account_state_at_block() { - let home = tempdir().unwrap(); + let TestFixture { storage, from, to, _home } = store_with_transfer_blocks(10, None).await; - let storage = IndexerStore::open_db(home.as_ref()).unwrap(); - - let mut prev_hash = None; - - let initial_accounts = initial_pub_accounts_private_keys(); - let from = initial_accounts[0].account_id; - let to = initial_accounts[1].account_id; - let sign_key = initial_accounts[0].pub_sign_key.clone(); - - for i in 0..10 { - let tx = common::test_utils::create_transaction_native_token_transfer( - from, i, to, 10, &sign_key, - ); - let block_id = u64::try_from(i + 1).unwrap(); - - let next_block = common::test_utils::produce_dummy_block(block_id, prev_hash, vec![tx]); - prev_hash = Some(next_block.header.hash); - - storage - .put_block( - next_block, - HeaderId::from([u8::try_from(i + 1).unwrap(); 32]), - ) - .await - .unwrap(); - } - - // Genesis block: no transfers applied yet. let acc1_at_1 = storage.account_state_at_block(&from, 1).unwrap(); let acc2_at_1 = storage.account_state_at_block(&to, 1).unwrap(); assert_eq!(acc1_at_1.balance, 9990); assert_eq!(acc2_at_1.balance, 20010); - // After block 5: 4 transfers of 10 applied (one each in blocks 2..=5). let acc1_at_5 = storage.account_state_at_block(&from, 5).unwrap(); let acc2_at_5 = storage.account_state_at_block(&to, 5).unwrap(); assert_eq!(acc1_at_5.balance, 9950); assert_eq!(acc2_at_5.balance, 20050); - // After final block 9: 8 transfers applied; should match current state. let acc1_at_9 = storage.account_state_at_block(&from, 9).unwrap(); let acc2_at_9 = storage.account_state_at_block(&to, 9).unwrap(); assert_eq!(acc1_at_9.balance, 9910); diff --git a/lez/indexer/service/src/mock_service.rs b/lez/indexer/service/src/mock_service.rs index d9ab9484..7bf6c528 100644 --- a/lez/indexer/service/src/mock_service.rs +++ b/lez/indexer/service/src/mock_service.rs @@ -330,6 +330,76 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { } } +fn mock_public_tx( + tx_hash: HashType, + block_id: BlockId, + tx_idx: u64, + account_ids: &[AccountId], +) -> Transaction { + Transaction::Public(PublicTransaction { + hash: tx_hash, + message: PublicMessage { + program_id: ProgramId([1_u32; 8]), + account_ids: vec![ + account_ids[tx_idx as usize % account_ids.len()], + account_ids[(tx_idx as usize + 1) % account_ids.len()], + ], + nonces: vec![block_id as u128, (block_id + 1) as u128], + instruction_data: vec![1, 2, 3, 4], + }, + witness_set: WitnessSet { + signatures_and_public_keys: vec![], + proof: None, + }, + }) +} + +fn mock_privacy_preserving_tx( + tx_hash: HashType, + block_id: BlockId, + tx_idx: u64, + account_ids: &[AccountId], +) -> Transaction { + Transaction::PrivacyPreserving(PrivacyPreservingTransaction { + hash: tx_hash, + message: PrivacyPreservingMessage { + public_account_ids: vec![account_ids[tx_idx as usize % account_ids.len()]], + nonces: vec![block_id as u128], + public_post_states: vec![Account { + program_owner: ProgramId([1_u32; 8]), + balance: 500, + data: Data(vec![0xdd, 0xee]), + nonce: block_id as u128, + }], + encrypted_private_post_states: vec![EncryptedAccountData { + ciphertext: indexer_service_protocol::Ciphertext(vec![0x01, 0x02, 0x03, 0x04]), + epk: indexer_service_protocol::EphemeralPublicKey(vec![0xaa; 32]), + view_tag: 42, + }], + new_commitments: vec![Commitment([block_id as u8; 32])], + new_nullifiers: vec![( + indexer_service_protocol::Nullifier([tx_idx as u8; 32]), + CommitmentSetDigest([0xff; 32]), + )], + block_validity_window: ValidityWindow((None, None)), + timestamp_validity_window: ValidityWindow((None, None)), + }, + witness_set: WitnessSet { + signatures_and_public_keys: vec![], + proof: Some(indexer_service_protocol::Proof(vec![0; 32])), + }, + }) +} + +fn mock_program_deployment_tx(tx_hash: HashType) -> Transaction { + Transaction::ProgramDeployment(ProgramDeploymentTransaction { + hash: tx_hash, + message: ProgramDeploymentMessage { + bytecode: vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00], + }, + }) +} + fn build_mock_block( block_id: BlockId, prev_hash: HashType, @@ -344,7 +414,6 @@ fn build_mock_block( HashType(hash) }; - // Create 2-4 transactions per block (mix of Public, PrivacyPreserving, and ProgramDeployment) let num_txs = 2 + (block_id % 3); let mut block_transactions = Vec::new(); @@ -356,65 +425,10 @@ fn build_mock_block( HashType(hash) }; - // Vary transaction types: Public, PrivacyPreserving, or ProgramDeployment let tx = match (block_id + tx_idx) % 5 { - // Public transactions (most common) - 0 | 1 => Transaction::Public(PublicTransaction { - hash: tx_hash, - message: PublicMessage { - program_id: ProgramId([1_u32; 8]), - account_ids: vec![ - account_ids[tx_idx as usize % account_ids.len()], - account_ids[(tx_idx as usize + 1) % account_ids.len()], - ], - nonces: vec![block_id as u128, (block_id + 1) as u128], - instruction_data: vec![1, 2, 3, 4], - }, - witness_set: WitnessSet { - signatures_and_public_keys: vec![], - proof: None, - }, - }), - // PrivacyPreserving transactions - 2 | 3 => Transaction::PrivacyPreserving(PrivacyPreservingTransaction { - hash: tx_hash, - message: PrivacyPreservingMessage { - public_account_ids: vec![account_ids[tx_idx as usize % account_ids.len()]], - nonces: vec![block_id as u128], - public_post_states: vec![Account { - program_owner: ProgramId([1_u32; 8]), - balance: 500, - data: Data(vec![0xdd, 0xee]), - nonce: block_id as u128, - }], - encrypted_private_post_states: vec![EncryptedAccountData { - ciphertext: indexer_service_protocol::Ciphertext(vec![ - 0x01, 0x02, 0x03, 0x04, - ]), - epk: indexer_service_protocol::EphemeralPublicKey(vec![0xaa; 32]), - view_tag: 42, - }], - new_commitments: vec![Commitment([block_id as u8; 32])], - new_nullifiers: vec![( - indexer_service_protocol::Nullifier([tx_idx as u8; 32]), - CommitmentSetDigest([0xff; 32]), - )], - block_validity_window: ValidityWindow((None, None)), - timestamp_validity_window: ValidityWindow((None, None)), - }, - witness_set: WitnessSet { - signatures_and_public_keys: vec![], - proof: Some(indexer_service_protocol::Proof(vec![0; 32])), - }, - }), - // ProgramDeployment transactions (rare) - _ => Transaction::ProgramDeployment(ProgramDeploymentTransaction { - hash: tx_hash, - message: ProgramDeploymentMessage { - bytecode: vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00], /* WASM magic - * number */ - }, - }), + 0 | 1 => mock_public_tx(tx_hash, block_id, tx_idx, account_ids), + 2 | 3 => mock_privacy_preserving_tx(tx_hash, block_id, tx_idx, account_ids), + _ => mock_program_deployment_tx(tx_hash), }; block_transactions.push(tx); diff --git a/lez/keycard_wallet/src/lib.rs b/lez/keycard_wallet/src/lib.rs index 73486392..41541ec0 100644 --- a/lez/keycard_wallet/src/lib.rs +++ b/lez/keycard_wallet/src/lib.rs @@ -134,10 +134,6 @@ impl KeycardWallet { }) } - #[expect( - clippy::arithmetic_side_effects, - reason = "64 - s_stripped.len() is safe: s_stripped.len() ≤ 31 because py_signature.len() is in [32, 63]" - )] pub fn sign_message_for_path( &self, py: Python, @@ -150,33 +146,9 @@ impl KeycardWallet { .call_method1("sign_message_for_path", (message, path))? .extract()?; - // The keycard Python library strips leading zeros from S when S < 2^(8k) for some k. - // Left-pad S back to 32 bytes so the full signature is always 64 bytes (R || S). - let py_signature = if py_signature.len() < 64 { - if py_signature.len() < 32 { - return Err(PyErr::new::(format!( - "signature from keycard too short: {} bytes", - py_signature.len() - ))); - } - let s_stripped = &py_signature[32..]; - let mut padded = [0_u8; 64]; - padded[..32].copy_from_slice(&py_signature[..32]); - padded[(64 - s_stripped.len())..].copy_from_slice(s_stripped); - padded.to_vec() - } else { - py_signature + let sig = Signature { + value: normalize_keycard_signature(py_signature)?, }; - - let signature: [u8; 64] = py_signature.try_into().map_err(|vec: Vec| { - PyErr::new::(format!( - "Invalid signature length: expected 64 bytes, got {} (bytes: {:02x?})", - vec.len(), - vec - )) - })?; - - let sig = Signature { value: signature }; let pub_key = self.get_public_key_for_path(py, path)?; if !sig.is_valid_for(message, &pub_key) { return Err(PyErr::new::( @@ -224,32 +196,8 @@ impl KeycardWallet { .call_method1("get_private_keys_for_path", (path,))? .extract()?; - let raw_nsk = Zeroizing::new(raw_nsk); - let raw_vsk = Zeroizing::new(raw_vsk); - - let nsk = { - if raw_nsk.len() != 32 { - return Err(PyErr::new::(format!( - "expected 32-byte NSK from keycard, got {} bytes", - raw_nsk.len() - ))); - } - let mut arr = Zeroizing::new([0_u8; 32]); - arr.copy_from_slice(&raw_nsk); - arr - }; - - let vsk = { - if raw_vsk.len() != 64 { - return Err(PyErr::new::(format!( - "expected 64-byte VSK from keycard, got {} bytes", - raw_vsk.len() - ))); - } - let mut arr = Zeroizing::new([0_u8; 64]); - arr.copy_from_slice(&raw_vsk); - arr - }; + let nsk = zeroizing_fixed_bytes::<32>("nullifier secret key", Zeroizing::new(raw_nsk))?; + let vsk = zeroizing_fixed_bytes::<64>("view secret key", Zeroizing::new(raw_vsk))?; Ok((nsk, vsk)) } @@ -269,6 +217,51 @@ impl KeycardWallet { } } +/// The keycard Python library strips leading zeros from S when S < 2^(8k) for some k. +/// Left-pad S back to 32 bytes so the full signature is always 64 bytes (R || S). +#[expect( + clippy::arithmetic_side_effects, + reason = "64 - s_stripped.len() is safe: s_stripped.len() ≤ 31 because py_signature.len() is in [32, 63]" +)] +fn normalize_keycard_signature(py_signature: Vec) -> PyResult<[u8; 64]> { + if py_signature.len() < 64 { + if py_signature.len() < 32 { + return Err(PyErr::new::(format!( + "signature from keycard too short: {} bytes", + py_signature.len() + ))); + } + let s_stripped = &py_signature[32..]; + let mut padded = [0_u8; 64]; + padded[..32].copy_from_slice(&py_signature[..32]); + padded[(64 - s_stripped.len())..].copy_from_slice(s_stripped); + Ok(padded) + } else { + py_signature.try_into().map_err(|vec: Vec| { + PyErr::new::(format!( + "Invalid signature length: expected 64 bytes, got {} (bytes: {:02x?})", + vec.len(), + vec + )) + }) + } +} + +fn zeroizing_fixed_bytes( + label: &str, + raw: Zeroizing>, +) -> PyResult> { + if raw.len() != N { + return Err(PyErr::new::(format!( + "expected {N}-byte {label} from keycard, got {} bytes", + raw.len() + ))); + } + let mut arr = Zeroizing::new([0_u8; N]); + arr.copy_from_slice(&raw); + Ok(arr) +} + fn pairing_file_path() -> Option { let home = std::env::var("LEE_WALLET_HOME_DIR") .map(PathBuf::from) diff --git a/lez/keycard_wallet/src/python_path.rs b/lez/keycard_wallet/src/python_path.rs index 99ed936e..61196ad5 100644 --- a/lez/keycard_wallet/src/python_path.rs +++ b/lez/keycard_wallet/src/python_path.rs @@ -2,8 +2,7 @@ use std::{env, path::PathBuf}; use pyo3::{prelude::*, types::PyList}; -/// Adds the project's `python/` directory and venv site-packages to Python's sys.path. -pub fn add_python_path(py: Python<'_>) -> PyResult<()> { +fn collect_python_paths() -> Vec { let current_dir = env::current_dir().expect("Failed to get current working directory"); let python_base = env::var("VIRTUAL_ENV") @@ -11,7 +10,7 @@ pub fn add_python_path(py: Python<'_>) -> PyResult<()> { .and_then(|v| PathBuf::from(v).parent().map(PathBuf::from)) .unwrap_or_else(|| current_dir.clone()); - let mut paths_to_add: Vec = vec![ + let mut paths = vec![ python_base .join("lez") .join("keycard_wallet") @@ -23,24 +22,28 @@ pub fn add_python_path(py: Python<'_>) -> PyResult<()> { .join("keycard-py"), ]; - // If a virtualenv is active, add its site-packages so that dependencies - // installed in the venv (e.g. smartcard, ecdsa) are importable by the - // pyo3 embedded interpreter, which does not inherit sys.path from the - // shell's `python3` executable. + // pyo3's embedded interpreter does not inherit sys.path from the shell, + // so venv site-packages must be added explicitly. if let Ok(venv) = env::var("VIRTUAL_ENV") { let lib = PathBuf::from(&venv).join("lib"); if let Ok(entries) = std::fs::read_dir(&lib) { for entry in entries.flatten() { let site_packages = entry.path().join("site-packages"); if site_packages.exists() { - paths_to_add.push(site_packages); + paths.push(site_packages); } } } } - // Sanity check — warns early if a path doesn't exist - for path in &paths_to_add { + paths +} + +/// Adds the project's `python/` directory and venv site-packages to Python's sys.path. +pub fn add_python_path(py: Python<'_>) -> PyResult<()> { + let paths = collect_python_paths(); + + for path in &paths { if !path.exists() { log::info!("Warning: Python path does not exist: {}", path.display()); } @@ -50,10 +53,9 @@ pub fn add_python_path(py: Python<'_>) -> PyResult<()> { let binding = sys.getattr("path")?; let sys_path = binding.cast::()?; - for path in &paths_to_add { + for path in &paths { let path_str = path.to_str().expect("Invalid path"); - // Avoid duplicating the path let already_present = sys_path .iter() .any(|p| p.extract::<&str>().is_ok_and(|s| s == path_str)); diff --git a/lez/sequencer/core/src/lib.rs b/lez/sequencer/core/src/lib.rs index b140fda8..004424d2 100644 --- a/lez/sequencer/core/src/lib.rs +++ b/lez/sequencer/core/src/lib.rs @@ -69,17 +69,13 @@ impl SequencerCore { /// assumed to represent the correct latest state consistent with Bedrock-finalized data. /// If no database is found, the sequencer performs a fresh start from genesis, /// initializing its state with the accounts defined in the configuration file. - pub async fn start_from_config( - config: SequencerConfig, - ) -> (Self, MemPoolHandle<(TransactionOrigin, LeeTransaction)>) { + fn open_or_create_store( + config: &SequencerConfig, + ) -> (SequencerStore, lee::V03State, Block) { let signing_key = lee::PrivateKey::try_new(config.signing_key).unwrap(); - - let bedrock_signing_key = - load_or_create_signing_key(&config.home.join("bedrock_signing_key")) - .expect("Failed to load or create bedrock signing key"); - let db_path = config.home.join("rocksdb"); - let (store, state, genesis_block) = if db_path.exists() { + + if db_path.exists() { let store = SequencerStore::open_db(&db_path, signing_key.clone()).unwrap_or_else(|err| { panic!( @@ -101,7 +97,7 @@ impl SequencerCore { db_path.display() ); - let (genesis_state, genesis_txs) = build_genesis_state(&config); + let (genesis_state, genesis_txs) = build_genesis_state(config); let hashable_data = HashableBlockData { block_id: GENESIS_BLOCK_ID, @@ -120,7 +116,17 @@ impl SequencerCore { .expect("Failed to create database with genesis block"); (store, genesis_state, genesis_block) - }; + } + } + + pub async fn start_from_config( + config: SequencerConfig, + ) -> (Self, MemPoolHandle<(TransactionOrigin, LeeTransaction)>) { + let bedrock_signing_key = + load_or_create_signing_key(&config.home.join("bedrock_signing_key")) + .expect("Failed to load or create bedrock signing key"); + + let (store, state, genesis_block) = Self::open_or_create_store(&config); let latest_block_meta = store .latest_block_meta() @@ -333,6 +339,61 @@ impl SequencerCore { /// Builds a new block from transactions in the mempool. /// Does NOT publish or store the block — the caller is responsible for that. + fn apply_mempool_transaction( + &mut self, + origin: TransactionOrigin, + tx: &LeeTransaction, + block_height: u64, + timestamp: u64, + deposit_event_ids: &mut Vec, + withdrawals: &mut Vec, + ) -> Result { + let tx_hash = tx.hash(); + match origin { + TransactionOrigin::User => { + let validated_diff = match tx.validate_on_state( + &self.state, + block_height, + timestamp, + ) { + Ok(diff) => diff, + Err(err) => { + error!( + "Transaction with hash {tx_hash} failed execution check with error: {err:#?}, skipping it", + ); + return Ok(false); + } + }; + + if let Some(withdraw_data) = extract_bridge_withdraw_data(tx) { + withdrawals.push(withdraw_data); + } + + self.state.apply_state_diff(validated_diff); + } + TransactionOrigin::Sequencer => { + let LeeTransaction::Public(public_tx) = tx else { + panic!("Sequencer may only generate Public transactions, found {tx:#?}"); + }; + + if let Some(deposit_op_id) = extract_bridge_deposit_id(tx) { + deposit_event_ids.push(deposit_op_id); + } + + self.state + .transition_from_public_transaction( + public_tx, + block_height, + timestamp, + ) + .context("Failed to execute sequencer-generated transaction")?; + } + } + + info!("Validated transaction with hash {tx_hash}, including it in block"); + Ok(true) + } + fn build_block_from_mempool(&mut self) -> Result { let now = Instant::now(); @@ -353,14 +414,12 @@ impl SequencerCore { let new_block_timestamp = u64::try_from(chrono::Utc::now().timestamp_millis()) .expect("Timestamp must be positive"); - // Pre-create the mandatory clock tx so its size is included in the block size check. let clock_tx = clock_invocation(new_block_timestamp); let clock_lee_tx = LeeTransaction::Public(clock_tx.clone()); while let Some((origin, tx)) = self.mempool.pop() { let tx_hash = tx.hash(); - // Check if block size exceeds limit (including the mandatory clock tx). let temp_valid_transactions = [ valid_transactions.as_slice(), std::slice::from_ref(&tx), @@ -379,66 +438,30 @@ impl SequencerCore { .len(); if block_size > max_block_size { - // Block would exceed size limit, remove last transaction and push back warn!( "Transaction with hash {tx_hash} deferred to next block: \ block size {block_size} bytes would exceed limit of {max_block_size} bytes", ); - self.mempool.push_front((origin, tx)); break; } - match origin { - TransactionOrigin::User => { - let validated_diff = match tx.validate_on_state( - &self.state, - new_block_height, - new_block_timestamp, - ) { - Ok(diff) => diff, - Err(err) => { - error!( - "Transaction with hash {tx_hash} failed execution check with error: {err:#?}, skipping it", - ); - continue; - } - }; - - if let Some(withdraw_data) = extract_bridge_withdraw_data(&tx) { - withdrawals.push(withdraw_data); - } - - self.state.apply_state_diff(validated_diff); - } - TransactionOrigin::Sequencer => { - let LeeTransaction::Public(public_tx) = &tx else { - panic!("Sequencer may only generate Public transactions, found {tx:#?}"); - }; - - if let Some(deposit_op_id) = extract_bridge_deposit_id(&tx) { - deposit_event_ids.push(deposit_op_id); - } - - self.state - .transition_from_public_transaction( - public_tx, - new_block_height, - new_block_timestamp, - ) - .context("Failed to execute sequencer-generated transaction")?; - } + if self.apply_mempool_transaction( + origin, + &tx, + new_block_height, + new_block_timestamp, + &mut deposit_event_ids, + &mut withdrawals, + )? { + valid_transactions.push(tx); } - valid_transactions.push(tx); - - info!("Validated transaction with hash {tx_hash}, including it in block"); if valid_transactions.len() >= self.sequencer_config.max_num_tx_in_block { break; } } - // Append the Clock Program invocation as the mandatory last transaction. self.state .transition_from_public_transaction(&clock_tx, new_block_height, new_block_timestamp) .context("Clock transaction failed. Aborting block production.")?; diff --git a/lez/storage/src/indexer/mod.rs b/lez/storage/src/indexer/mod.rs index a753a71a..6b772adf 100644 --- a/lez/storage/src/indexer/mod.rs +++ b/lez/storage/src/indexer/mod.rs @@ -153,97 +153,19 @@ impl RocksDBIO { } let br_id = closest_breakpoint_id(block_id); - let mut breakpoint = self.get_breakpoint(br_id)?; + let mut state = self.get_breakpoint(br_id)?; let start = u64::from(BREAKPOINT_INTERVAL) .checked_mul(br_id) .expect("Reached maximum breakpoint id"); - for mut block in self.get_block_batch_seq( + for block in self.get_block_batch_seq( start.checked_add(1).expect("Will be lesser that u64::MAX")..=block_id, )? { - let expected_clock = LeeTransaction::Public(clock_invocation(block.header.timestamp)); - - let clock_tx = block.body.transactions.pop().ok_or_else(|| { - DbError::db_interaction_error( - "Block must contain clock transaction at the end".to_owned(), - ) - })?; - let user_txs = block.body.transactions; - - if clock_tx != expected_clock { - return Err(DbError::db_interaction_error( - "Last transaction in block must be the clock invocation for the block timestamp" - .to_owned(), - )); - } - for transaction in user_txs { - let is_genesis = block.header.block_id == GENESIS_BLOCK_ID; - if is_genesis { - let genesis_tx = match transaction { - LeeTransaction::Public(public_tx) => public_tx, - LeeTransaction::PrivacyPreserving(_) - | LeeTransaction::ProgramDeployment(_) => { - return Err(DbError::db_interaction_error( - "Genesis block should contain only public transactions".to_owned(), - )); - } - }; - breakpoint - .transition_from_public_transaction( - &genesis_tx, - block.header.block_id, - block.header.timestamp, - ) - .map_err(|err| { - DbError::db_interaction_error(format!( - "genesis transaction execution failed with err {err:?}" - )) - })?; - } else { - transaction - .transaction_stateless_check() - .map_err(|err| { - DbError::db_interaction_error(format!( - "transaction pre check failed with err {err:?}" - )) - })? - // FIXME: HOT FIX (testnet v0.2): does not check for system account updates due to - // sequencer-generated deposit tx'es; - // CHANGE ME back to `execute_check_on_state` when the indexer can authenticate deposit transactions - .execute_without_system_accounts_check_on_state( - &mut breakpoint, - block.header.block_id, - block.header.timestamp, - ) - .map_err(|err| { - DbError::db_interaction_error(format!( - "transaction execution failed with err {err:?}" - )) - })?; - } - } - - let LeeTransaction::Public(clock_public_tx) = clock_tx else { - return Err(DbError::db_interaction_error( - "Clock invocation must be a public transaction".to_owned(), - )); - }; - - breakpoint - .transition_from_public_transaction( - &clock_public_tx, - block.header.block_id, - block.header.timestamp, - ) - .map_err(|err| { - DbError::db_interaction_error(format!( - "clock transaction execution failed with err {err:?}" - )) - })?; + apply_block_transactions(block, &mut state)?; } - Ok(breakpoint) + Ok(state) } pub fn final_state(&self) -> DbResult { @@ -252,6 +174,86 @@ impl RocksDBIO { } } +fn apply_block_transactions(mut block: Block, state: &mut V03State) -> DbResult<()> { + let expected_clock = LeeTransaction::Public(clock_invocation(block.header.timestamp)); + + let clock_tx = block.body.transactions.pop().ok_or_else(|| { + DbError::db_interaction_error("Block must contain clock transaction at the end".to_owned()) + })?; + + if clock_tx != expected_clock { + return Err(DbError::db_interaction_error( + "Last transaction in block must be the clock invocation for the block timestamp" + .to_owned(), + )); + } + + for transaction in block.body.transactions { + if block.header.block_id == GENESIS_BLOCK_ID { + let genesis_tx = match transaction { + LeeTransaction::Public(public_tx) => public_tx, + LeeTransaction::PrivacyPreserving(_) | LeeTransaction::ProgramDeployment(_) => { + return Err(DbError::db_interaction_error( + "Genesis block should contain only public transactions".to_owned(), + )); + } + }; + state + .transition_from_public_transaction( + &genesis_tx, + block.header.block_id, + block.header.timestamp, + ) + .map_err(|err| { + DbError::db_interaction_error(format!( + "genesis transaction execution failed with err {err:?}" + )) + })?; + } else { + transaction + .transaction_stateless_check() + .map_err(|err| { + DbError::db_interaction_error(format!( + "transaction pre check failed with err {err:?}" + )) + })? + // FIXME: HOT FIX (testnet v0.2): does not check for system account updates due to + // sequencer-generated deposit tx'es; + // CHANGE ME back to `execute_check_on_state` when the indexer can authenticate deposit transactions + .execute_without_system_accounts_check_on_state( + state, + block.header.block_id, + block.header.timestamp, + ) + .map_err(|err| { + DbError::db_interaction_error(format!( + "transaction execution failed with err {err:?}" + )) + })?; + } + } + + let LeeTransaction::Public(clock_public_tx) = clock_tx else { + return Err(DbError::db_interaction_error( + "Clock invocation must be a public transaction".to_owned(), + )); + }; + + state + .transition_from_public_transaction( + &clock_public_tx, + block.header.block_id, + block.header.timestamp, + ) + .map_err(|err| { + DbError::db_interaction_error(format!( + "clock transaction execution failed with err {err:?}" + )) + })?; + + Ok(()) +} + fn closest_breakpoint_id(block_id: u64) -> u64 { block_id .saturating_sub(1) diff --git a/lez/storage/src/indexer/write_atomic.rs b/lez/storage/src/indexer/write_atomic.rs index a88e46ef..ec28f8ec 100644 --- a/lez/storage/src/indexer/write_atomic.rs +++ b/lez/storage/src/indexer/write_atomic.rs @@ -52,44 +52,8 @@ impl RocksDBIO { acc_id: [u8; 32], tx_hashes: &[[u8; 32]], ) -> DbResult<()> { - let acc_num_tx = self.get_acc_meta_num_tx(acc_id)?.unwrap_or(0); - let cf_att = self.account_id_to_tx_hash_column(); let mut write_batch = WriteBatch::new(); - - for (tx_id, tx_hash) in tx_hashes.iter().enumerate() { - let put_id = acc_num_tx - .checked_add(tx_id.try_into().expect("Must fit into u64")) - .expect("Tx count should be lesser that u64::MAX"); - - let mut prefix = borsh::to_vec(&acc_id).map_err(|berr| { - DbError::borsh_cast_message(berr, Some("Failed to serialize account id".to_owned())) - })?; - let suffix = borsh::to_vec(&put_id).map_err(|berr| { - DbError::borsh_cast_message(berr, Some("Failed to serialize tx id".to_owned())) - })?; - - prefix.extend_from_slice(&suffix); - - write_batch.put_cf( - &cf_att, - prefix, - borsh::to_vec(tx_hash).map_err(|berr| { - DbError::borsh_cast_message( - berr, - Some("Failed to serialize tx hash".to_owned()), - ) - })?, - ); - } - - self.update_acc_meta_batch( - acc_id, - acc_num_tx - .checked_add(tx_hashes.len().try_into().expect("Must fit into u64")) - .expect("Tx count should be lesser that u64::MAX"), - &mut write_batch, - )?; - + self.put_account_transactions_dependant(acc_id, tx_hashes, &mut write_batch)?; self.db.write(write_batch).map_err(|rerr| { DbError::rocksdb_cast_message(rerr, Some("Failed to write batch".to_owned())) }) diff --git a/lez/wallet/src/account_manager.rs b/lez/wallet/src/account_manager.rs index ce9d1833..87c0b829 100644 --- a/lez/wallet/src/account_manager.rs +++ b/lez/wallet/src/account_manager.rs @@ -200,38 +200,15 @@ impl AccountManager { for account in accounts { let state = match account { AccountIdentity::Public(account_id) => { - let acc = wallet - .get_account_public(account_id) - .await - .map_err(ExecutionFailureKind::SequencerError)?; - - let sk = wallet.get_account_public_signing_key(account_id).cloned(); - let account = AccountWithMetadata::new(acc.clone(), sk.is_some(), account_id); - - State::Public { account, sk } + prepare_public_state(wallet, account_id, true).await? } AccountIdentity::PublicNoSign(account_id) => { - let acc = wallet - .get_account_public(account_id) - .await - .map_err(ExecutionFailureKind::SequencerError)?; - - let sk = None; - let account = AccountWithMetadata::new(acc.clone(), sk.is_some(), account_id); - - State::Public { account, sk } + prepare_public_state(wallet, account_id, false).await? } AccountIdentity::PublicKeycard { account_id, key_path, } => { - let acc = wallet - .get_account_public(account_id) - .await - .map_err(ExecutionFailureKind::SequencerError)?; - - let account = AccountWithMetadata::new(acc.clone(), true, account_id); - if pin.is_none() { pin = Some( crate::helperfunctions::read_pin() @@ -247,66 +224,25 @@ impl AccountManager { .to_owned(), ); } - - State::PublicKeycard { account, key_path } + prepare_public_keycard_state(wallet, account_id, key_path).await? } AccountIdentity::PrivateOwned(account_id) => { - let pre = private_key_tree_acc_preparation(wallet, account_id, false).await?; - - State::Private(pre) + State::Private(private_key_tree_acc_preparation(wallet, account_id, false).await?) } AccountIdentity::PrivateForeign { npk, vpk, identifier, - } => { - let acc = lee_core::account::Account::default(); - let auth_acc = AccountWithMetadata::new(acc, false, (&npk, identifier)); - let eph_holder = EphemeralKeyHolder::new(&vpk); - let ssk = eph_holder.calculate_shared_secret_sender(); - let epk = eph_holder.ephemeral_public_key().clone(); - let pre = AccountPreparedData { - nsk: None, - npk, - identifier, - vpk, - pre_state: auth_acc, - proof: None, - ssk, - epk, - is_pda: false, - }; - - State::Private(pre) - } + } => State::Private(private_foreign_acc_preparation(npk, vpk, identifier)), AccountIdentity::PrivatePdaOwned(account_id) => { - let pre = private_key_tree_acc_preparation(wallet, account_id, true).await?; - State::Private(pre) + State::Private(private_key_tree_acc_preparation(wallet, account_id, true).await?) } AccountIdentity::PrivatePdaForeign { account_id, npk, vpk, identifier, - } => { - let acc = lee_core::account::Account::default(); - let auth_acc = AccountWithMetadata::new(acc, false, account_id); - let eph_holder = EphemeralKeyHolder::new(&vpk); - let ssk = eph_holder.calculate_shared_secret_sender(); - let epk = eph_holder.ephemeral_public_key().clone(); - let pre = AccountPreparedData { - nsk: None, - npk, - identifier, - vpk, - pre_state: auth_acc, - proof: None, - ssk, - epk, - is_pda: true, - }; - State::Private(pre) - } + } => State::Private(private_pda_foreign_acc_preparation(account_id, npk, vpk, identifier)), AccountIdentity::PrivateShared { nsk, npk, @@ -314,12 +250,9 @@ impl AccountManager { identifier, } => { let account_id = lee::AccountId::from((&npk, identifier)); - let pre = private_shared_acc_preparation( - wallet, account_id, nsk, npk, vpk, identifier, false, + State::Private( + private_shared_acc_preparation(wallet, account_id, nsk, npk, vpk, identifier, false).await?, ) - .await?; - - State::Private(pre) } AccountIdentity::PrivatePdaShared { account_id, @@ -327,14 +260,9 @@ impl AccountManager { npk, vpk, identifier, - } => { - let pre = private_shared_acc_preparation( - wallet, account_id, nsk, npk, vpk, identifier, true, - ) - .await?; - - State::Private(pre) - } + } => State::Private( + private_shared_acc_preparation(wallet, account_id, nsk, npk, vpk, identifier, true).await?, + ), }; states.push(state); @@ -517,6 +445,37 @@ struct AccountPreparedData { is_pda: bool, } +async fn prepare_public_state( + wallet: &WalletCore, + account_id: AccountId, + lookup_signing_key: bool, +) -> Result { + let acc = wallet + .get_account_public(account_id) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + let sk = if lookup_signing_key { + wallet.get_account_public_signing_key(account_id).cloned() + } else { + None + }; + let account = AccountWithMetadata::new(acc.clone(), sk.is_some(), account_id); + Ok(State::Public { account, sk }) +} + +async fn prepare_public_keycard_state( + wallet: &WalletCore, + account_id: AccountId, + key_path: String, +) -> Result { + let acc = wallet + .get_account_public(account_id) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + let account = AccountWithMetadata::new(acc.clone(), true, account_id); + Ok(State::PublicKeycard { account, key_path }) +} + async fn private_key_tree_acc_preparation( wallet: &WalletCore, account_id: AccountId, @@ -599,6 +558,55 @@ async fn private_shared_acc_preparation( }) } +fn private_foreign_acc_preparation( + npk: NullifierPublicKey, + vpk: ViewingPublicKey, + identifier: Identifier, +) -> AccountPreparedData { + let acc = lee_core::account::Account::default(); + let pre_state = AccountWithMetadata::new(acc, false, (&npk, identifier)); + let eph_holder = EphemeralKeyHolder::new(&vpk); + let ssk = eph_holder.calculate_shared_secret_sender(); + let epk = eph_holder.ephemeral_public_key().clone(); + + AccountPreparedData { + nsk: None, + npk, + identifier, + vpk, + pre_state, + proof: None, + ssk, + epk, + is_pda: false, + } +} + +fn private_pda_foreign_acc_preparation( + account_id: AccountId, + npk: NullifierPublicKey, + vpk: ViewingPublicKey, + identifier: Identifier, +) -> AccountPreparedData { + let acc = lee_core::account::Account::default(); + let pre_state = AccountWithMetadata::new(acc, false, account_id); + let eph_holder = EphemeralKeyHolder::new(&vpk); + let ssk = eph_holder.calculate_shared_secret_sender(); + let epk = eph_holder.ephemeral_public_key().clone(); + + AccountPreparedData { + nsk: None, + npk, + identifier, + vpk, + pre_state, + proof: None, + ssk, + epk, + is_pda: true, + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/lez/wallet/src/cli/account.rs b/lez/wallet/src/cli/account.rs index fb0db2cb..efa8dbf5 100644 --- a/lez/wallet/src/cli/account.rs +++ b/lez/wallet/src/cli/account.rs @@ -125,6 +125,164 @@ pub enum NewSubcommand { }, } +impl NewSubcommand { + async fn handle_public( + cci: Option, + label: Option