add helper functions

This commit is contained in:
Marvin Jones 2026-06-17 13:53:54 -04:00
parent e37876a640
commit c8c9ced421
24 changed files with 3271 additions and 2752 deletions

View File

@ -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<AccountId>, nonces: Vec<u128>) -> impl IntoView {
view! {
<div class="accounts-list">
{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! {
<div class="account-item">
<A href=format!("/account/{}", account_id_str)>
<span class="hash">{account_id_str}</span>
</A>
<span class="nonce">
" (nonce: " {nonce.to_string()} ")"
</span>
</div>
}
}
EitherOrBoth::Left(account_id) => {
let account_id_str = account_id.to_string();
view! {
<div class="account-item">
<A href=format!("/account/{}", account_id_str)>
<span class="hash">{account_id_str}</span>
</A>
<span class="nonce">
" (nonce: "{"Not affected by this transaction".to_owned()}" )"
</span>
</div>
}
}
EitherOrBoth::Right(_) => {
view! {
<div class="account-item">
<A href=format!("/account/{}", "Account not found")>
<span class="hash">{"Account not found"}</span>
</A>
<span class="nonce">
" (nonce: "{"Account not found".to_owned()}" )"
</span>
</div>
}
}
}
})
.collect::<Vec<_>>()}
</div>
}
}

View File

@ -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,
};

View File

@ -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! {
<div class="search-results">
<h2>"Search Results"</h2>
{if has_results {
view! {
<div class="results-container">
{if blocks.is_empty() {
().into_any()
} else {
view! {
<div class="results-section">
<h3>"Blocks"</h3>
<div class="results-list">
{blocks
.into_iter()
.map(|block| {
view! { <BlockPreview block=block /> }
})
.collect::<Vec<_>>()}
</div>
</div>
}
.into_any()
}}
{if transactions.is_empty() {
().into_any()
} else {
view! {
<div class="results-section">
<h3>"Transactions"</h3>
<div class="results-list">
{transactions
.into_iter()
.map(|tx| {
view! { <TransactionPreview transaction=tx /> }
})
.collect::<Vec<_>>()}
</div>
</div>
}
.into_any()
}}
{if accounts.is_empty() {
().into_any()
} else {
view! {
<div class="results-section">
<h3>"Accounts"</h3>
<div class="results-list">
{accounts
.into_iter()
.map(|(id, account)| {
view! {
<AccountPreview
account_id=id
account=account
/>
}
})
.collect::<Vec<_>>()}
</div>
</div>
}
.into_any()
}}
</div>
}
.into_any()
} else {
view! { <div class="not-found">"No results found"</div> }
.into_any()
}}
</div>
}
}

View File

@ -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! {
<div class="transaction-details">
<h2>"Public Transaction Details"</h2>
<div class="info-grid">
<div class="info-row">
<span class="info-label">"Program ID:"</span>
<span class="info-value hash">{program_id_str}</span>
</div>
<div class="info-row">
<span class="info-label">"Instruction Data:"</span>
<span class="info-value">
{format!("{} u32 values", instruction_data.len())}
</span>
</div>
<div class="info-row">
<span class="info-label">"Proof Size:"</span>
<span class="info-value">{format!("{proof_len} bytes")}</span>
</div>
<div class="info-row">
<span class="info-label">"Signatures:"</span>
<span class="info-value">{signatures_count.to_string()}</span>
</div>
</div>
<h3>"Accounts"</h3>
<AccountNonceList account_ids=account_ids nonces=nonces />
</div>
}
}
/// 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! {
<div class="transaction-details">
<h2>"Privacy-Preserving Transaction Details"</h2>
<div class="info-grid">
<div class="info-row">
<span class="info-label">"Public Accounts:"</span>
<span class="info-value">
{public_account_ids.len().to_string()}
</span>
</div>
<div class="info-row">
<span class="info-label">"New Commitments:"</span>
<span class="info-value">{new_commitments.len().to_string()}</span>
</div>
<div class="info-row">
<span class="info-label">"Nullifiers:"</span>
<span class="info-value">{new_nullifiers.len().to_string()}</span>
</div>
<div class="info-row">
<span class="info-label">"Encrypted States:"</span>
<span class="info-value">
{encrypted_private_post_states.len().to_string()}
</span>
</div>
<div class="info-row">
<span class="info-label">"Proof Size:"</span>
<span class="info-value">{format!("{proof_len} bytes")}</span>
</div>
<div class="info-row">
<span class="info-label">"Block Validity Window:"</span>
<span class="info-value">{block_validity_window.to_string()}</span>
</div>
<div class="info-row">
<span class="info-label">"Timestamp Validity Window:"</span>
<span class="info-value">{timestamp_validity_window.to_string()}</span>
</div>
</div>
<h3>"Public Accounts"</h3>
<AccountNonceList account_ids=public_account_ids nonces=nonces />
</div>
}
}
/// 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! {
<div class="transaction-details">
<h2>"Program Deployment Transaction Details"</h2>
<div class="info-grid">
<div class="info-row">
<span class="info-label">"Bytecode Size:"</span>
<span class="info-value">
{format!("{bytecode_len} bytes")}
</span>
</div>
</div>
</div>
}
}

View File

@ -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! {
<div class="search-results">
<h2>"Search Results"</h2>
{if has_results {
view! {
<div class="results-container">
{if blocks.is_empty() {
().into_any()
} else {
view! {
<div class="results-section">
<h3>"Blocks"</h3>
<div class="results-list">
{blocks
.into_iter()
.map(|block| {
view! { <BlockPreview block=block /> }
})
.collect::<Vec<_>>()}
</div>
</div>
}
.into_any()
}}
{if transactions.is_empty() {
().into_any()
} else {
view! {
<div class="results-section">
<h3>"Transactions"</h3>
<div class="results-list">
{transactions
.into_iter()
.map(|tx| {
view! { <TransactionPreview transaction=tx /> }
})
.collect::<Vec<_>>()}
</div>
</div>
}
.into_any()
}}
{if accounts.is_empty() {
().into_any()
} else {
view! {
<div class="results-section">
<h3>"Accounts"</h3>
<div class="results-list">
{accounts
.into_iter()
.map(|(id, account)| {
view! {
<AccountPreview
account_id=id
account=account
/>
}
})
.collect::<Vec<_>>()}
</div>
</div>
}
.into_any()
}}
</div>
}
.into_any()
} else {
view! { <div class="not-found">"No results found"</div> }
.into_any()
}}
</div>
}
.into_any()
})
view! { <SearchResultsView results=results /> }.into_any()
})
}}
</Suspense>

View File

@ -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! { <PublicTxDetails tx=ptx /> }.into_any()
}
Transaction::PrivacyPreserving(pptx) => {
view! { <PrivacyPreservingTxDetails tx=pptx /> }.into_any()
}
Transaction::ProgramDeployment(pdtx) => {
view! { <ProgramDeploymentTxDetails tx=pdtx /> }.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! {
<div class="transaction-details">
<h2>"Public Transaction Details"</h2>
<div class="info-grid">
<div class="info-row">
<span class="info-label">"Program ID:"</span>
<span class="info-value hash">{program_id_str}</span>
</div>
<div class="info-row">
<span class="info-label">"Instruction Data:"</span>
<span class="info-value">
{format!("{} u32 values", instruction_data.len())}
</span>
</div>
<div class="info-row">
<span class="info-label">"Proof Size:"</span>
<span class="info-value">{format!("{proof_len} bytes")}</span>
</div>
<div class="info-row">
<span class="info-label">"Signatures:"</span>
<span class="info-value">{signatures_count.to_string()}</span>
</div>
</div>
<h3>"Accounts"</h3>
<div class="accounts-list">
{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! {
<div class="account-item">
<A href=format!("/account/{}", account_id_str)>
<span class="hash">{account_id_str}</span>
</A>
<span class="nonce">
" (nonce: " {nonce.to_string()} ")"
</span>
</div>
}
}
EitherOrBoth::Left(account_id) => {
let account_id_str = account_id.to_string();
view! {
<div class="account-item">
<A href=format!("/account/{}", account_id_str)>
<span class="hash">{account_id_str}</span>
</A>
<span class="nonce">
" (nonce: "{"Not affected by this transaction".to_owned()}" )"
</span>
</div>
}
}
EitherOrBoth::Right(_) => {
view! {
<div class="account-item">
<A href=format!("/account/{}", "Account not found")>
<span class="hash">{"Account not found"}</span>
</A>
<span class="nonce">
" (nonce: "{"Account not found".to_owned()}" )"
</span>
</div>
}
}
}
})
.collect::<Vec<_>>()}
</div>
</div>
}
.into_any()
</div>
}
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! {
<div class="transaction-details">
<h2>"Privacy-Preserving Transaction Details"</h2>
<div class="info-grid">
<div class="info-row">
<span class="info-label">"Public Accounts:"</span>
<span class="info-value">
{public_account_ids.len().to_string()}
</span>
</div>
<div class="info-row">
<span class="info-label">"New Commitments:"</span>
<span class="info-value">{new_commitments.len().to_string()}</span>
</div>
<div class="info-row">
<span class="info-label">"Nullifiers:"</span>
<span class="info-value">{new_nullifiers.len().to_string()}</span>
</div>
<div class="info-row">
<span class="info-label">"Encrypted States:"</span>
<span class="info-value">
{encrypted_private_post_states.len().to_string()}
</span>
</div>
<div class="info-row">
<span class="info-label">"Proof Size:"</span>
<span class="info-value">{format!("{proof_len} bytes")}</span>
</div>
<div class="info-row">
<span class="info-label">"Block Validity Window:"</span>
<span class="info-value">{block_validity_window.to_string()}</span>
</div>
<div class="info-row">
<span class="info-label">"Timestamp Validity Window:"</span>
<span class="info-value">{timestamp_validity_window.to_string()}</span>
</div>
</div>
<h3>"Public Accounts"</h3>
<div class="accounts-list">
{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! {
<div class="account-item">
<A href=format!("/account/{}", account_id_str)>
<span class="hash">{account_id_str}</span>
</A>
<span class="nonce">
" (nonce: " {nonce.to_string()} ")"
</span>
</div>
}
}
EitherOrBoth::Left(account_id) => {
let account_id_str = account_id.to_string();
view! {
<div class="account-item">
<A href=format!("/account/{}", account_id_str)>
<span class="hash">{account_id_str}</span>
</A>
<span class="nonce">
" (nonce: "{"Not affected by this transaction".to_owned()}" )"
</span>
</div>
}
}
EitherOrBoth::Right(_) => {
view! {
<div class="account-item">
<A href=format!("/account/{}", "Account not found")>
<span class="hash">{"Account not found"}</span>
</A>
<span class="nonce">
" (nonce: "{"Account not found".to_owned()}" )"
</span>
</div>
}
}
}
})
.collect::<Vec<_>>()}
</div>
</div>
}
.into_any()
}
Transaction::ProgramDeployment(pdtx) => {
let ProgramDeploymentTransaction {
hash: _,
message,
} = pdtx;
let ProgramDeploymentMessage { bytecode } = message;
let bytecode_len = bytecode.len();
view! {
<div class="transaction-details">
<h2>"Program Deployment Transaction Details"</h2>
<div class="info-grid">
<div class="info-row">
<span class="info-label">"Bytecode Size:"</span>
<span class="info-value">
{format!("{bytecode_len} bytes")}
</span>
</div>
</div>
</div>
}
.into_any()
}
}}
</div>
}
.into_any()
.into_any()
}
Err(e) => {
view! {

View File

@ -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<common::HashType>,
) -> 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);

View File

@ -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);

View File

@ -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::<pyo3::exceptions::PyValueError, _>(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<u8>| {
PyErr::new::<pyo3::exceptions::PyValueError, _>(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::<pyo3::exceptions::PyValueError, _>(
@ -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::<pyo3::exceptions::PyValueError, _>(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::<pyo3::exceptions::PyValueError, _>(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<u8>) -> PyResult<[u8; 64]> {
if py_signature.len() < 64 {
if py_signature.len() < 32 {
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(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<u8>| {
PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
"Invalid signature length: expected 64 bytes, got {} (bytes: {:02x?})",
vec.len(),
vec
))
})
}
}
fn zeroizing_fixed_bytes<const N: usize>(
label: &str,
raw: Zeroizing<Vec<u8>>,
) -> PyResult<Zeroizing<[u8; N]>> {
if raw.len() != N {
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(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<PathBuf> {
let home = std::env::var("LEE_WALLET_HOME_DIR")
.map(PathBuf::from)

View File

@ -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<PathBuf> {
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<PathBuf> = 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::<PyList>()?;
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));

View File

@ -69,17 +69,13 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
/// 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<BP: BlockPublisherTrait> SequencerCore<BP> {
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<BP: BlockPublisherTrait> SequencerCore<BP> {
.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<BP: BlockPublisherTrait> SequencerCore<BP> {
/// 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<HashType>,
withdrawals: &mut Vec<WithdrawArg>,
) -> Result<bool> {
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<BlockWithMeta> {
let now = Instant::now();
@ -353,14 +414,12 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
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<BP: BlockPublisherTrait> SequencerCore<BP> {
.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.")?;

View File

@ -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<V03State> {
@ -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)

View File

@ -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()))
})

View File

@ -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<State, ExecutionFailureKind> {
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<State, ExecutionFailureKind> {
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::*;

View File

@ -125,6 +125,164 @@ pub enum NewSubcommand {
},
}
impl NewSubcommand {
async fn handle_public(
cci: Option<ChainIndex>,
label: Option<Label>,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
if let Some(label) = &label {
wallet_core.storage().check_label_availability(label)?;
}
let (account_id, chain_index) = wallet_core.create_new_account_public(cci);
let private_key = wallet_core
.storage
.key_chain()
.pub_account_signing_key(account_id)
.unwrap();
let public_key = PublicKey::new_from_private_key(private_key);
if let Some(label) = label {
wallet_core
.storage_mut()
.add_label(label, AccountIdWithPrivacy::Public(account_id))?;
}
println!(
"Generated new account with account_id Public/{account_id} at path {chain_index}"
);
println!("With pk {}", hex::encode(public_key.value()));
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::RegisterAccount { account_id })
}
async fn handle_private(
cci: Option<ChainIndex>,
label: Option<Label>,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
if let Some(label) = &label {
wallet_core.storage().check_label_availability(label)?;
}
let (account_id, chain_index) = wallet_core.create_new_account_private(cci);
if let Some(label) = label {
wallet_core
.storage_mut()
.add_label(label, AccountIdWithPrivacy::Private(account_id))?;
}
let found_acc = wallet_core
.storage()
.key_chain()
.private_account(account_id)
.expect("Account should exist after creation");
let key_chain = found_acc.key_chain;
println!(
"Generated new account with account_id Private/{account_id} at path {chain_index}"
);
println!("With npk {}", hex::encode(key_chain.nullifier_public_key.0));
println!(
"With vpk {}",
hex::encode(key_chain.viewing_public_key.to_bytes())
);
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::RegisterAccount { account_id })
}
async fn handle_private_gms(
group: Label,
label: Option<Label>,
pda: bool,
seed: Option<String>,
program_id: Option<String>,
identifier: Option<u128>,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
if let Some(label) = &label {
wallet_core.storage().check_label_availability(label)?;
}
let info = if pda {
let seed_hex = seed.context("--seed is required for PDA accounts")?;
let pid_hex =
program_id.context("--program-id is required for PDA accounts")?;
let seed_bytes: [u8; 32] = hex::decode(&seed_hex)
.context("Invalid seed hex")?
.try_into()
.map_err(|_err| anyhow::anyhow!("Seed must be exactly 32 bytes"))?;
let pda_seed = lee_core::program::PdaSeed::new(seed_bytes);
let pid_bytes = hex::decode(&pid_hex).context("Invalid program ID hex")?;
if pid_bytes.len() != 32 {
anyhow::bail!("Program ID must be exactly 32 bytes");
}
let mut pid: lee_core::program::ProgramId = [0; 8];
for (i, chunk) in pid_bytes.chunks_exact(4).enumerate() {
pid[i] = u32::from_le_bytes(chunk.try_into().unwrap());
}
wallet_core.create_shared_pda_account(
group.clone(),
pda_seed,
pid,
identifier.unwrap_or_else(rand::random),
)?
} else {
wallet_core.create_shared_regular_account(group.clone())?
};
if let Some(label) = label {
wallet_core
.storage_mut()
.add_label(label, AccountIdWithPrivacy::Private(info.account_id))?;
}
println!("Shared account from group '{group}'");
println!("AccountId: Private/{}", info.account_id);
println!("NPK: {}", hex::encode(info.npk.0));
println!("VPK: {}", hex::encode(info.vpk.to_bytes()));
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::RegisterAccount {
account_id: info.account_id,
})
}
async fn handle_private_accounts_key(
cci: Option<ChainIndex>,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let chain_index = wallet_core.create_private_accounts_key(cci);
let key_chain = wallet_core
.storage()
.key_chain()
.private_account_key_chain_by_index(&chain_index)
.expect("Key chain should exist after creation");
println!("Generated new private key node at path {chain_index}");
println!("With npk {}", hex::encode(key_chain.nullifier_public_key.0));
println!(
"With vpk {}",
hex::encode(key_chain.viewing_public_key.to_bytes())
);
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
}
impl WalletSubcommand for NewSubcommand {
async fn handle_subcommand(
self,
@ -132,67 +290,10 @@ impl WalletSubcommand for NewSubcommand {
) -> Result<SubcommandReturnValue> {
match self {
Self::Public { cci, label } => {
if let Some(label) = &label {
wallet_core.storage().check_label_availability(label)?;
}
let (account_id, chain_index) = wallet_core.create_new_account_public(cci);
let private_key = wallet_core
.storage
.key_chain()
.pub_account_signing_key(account_id)
.unwrap();
let public_key = PublicKey::new_from_private_key(private_key);
if let Some(label) = label {
wallet_core
.storage_mut()
.add_label(label, AccountIdWithPrivacy::Public(account_id))?;
}
println!(
"Generated new account with account_id Public/{account_id} at path {chain_index}"
);
println!("With pk {}", hex::encode(public_key.value()));
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::RegisterAccount { account_id })
Self::handle_public(cci, label, wallet_core).await
}
Self::Private { cci, label } => {
if let Some(label) = &label {
wallet_core.storage().check_label_availability(label)?;
}
let (account_id, chain_index) = wallet_core.create_new_account_private(cci);
if let Some(label) = label {
wallet_core
.storage_mut()
.add_label(label, AccountIdWithPrivacy::Private(account_id))?;
}
let found_acc = wallet_core
.storage()
.key_chain()
.private_account(account_id)
.expect("Account should exist after creation");
let key_chain = found_acc.key_chain;
println!(
"Generated new account with account_id Private/{account_id} at path {chain_index}"
);
println!("With npk {}", hex::encode(key_chain.nullifier_public_key.0));
println!(
"With vpk {}",
hex::encode(key_chain.viewing_public_key.to_bytes())
);
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::RegisterAccount { account_id })
Self::handle_private(cci, label, wallet_core).await
}
Self::PrivateGms {
group,
@ -202,79 +303,201 @@ impl WalletSubcommand for NewSubcommand {
program_id,
identifier,
} => {
if let Some(label) = &label {
wallet_core.storage().check_label_availability(label)?;
}
let info = if pda {
let seed_hex = seed.context("--seed is required for PDA accounts")?;
let pid_hex =
program_id.context("--program-id is required for PDA accounts")?;
let seed_bytes: [u8; 32] = hex::decode(&seed_hex)
.context("Invalid seed hex")?
.try_into()
.map_err(|_err| anyhow::anyhow!("Seed must be exactly 32 bytes"))?;
let pda_seed = lee_core::program::PdaSeed::new(seed_bytes);
let pid_bytes = hex::decode(&pid_hex).context("Invalid program ID hex")?;
if pid_bytes.len() != 32 {
anyhow::bail!("Program ID must be exactly 32 bytes");
}
let mut pid: lee_core::program::ProgramId = [0; 8];
for (i, chunk) in pid_bytes.chunks_exact(4).enumerate() {
pid[i] = u32::from_le_bytes(chunk.try_into().unwrap());
}
wallet_core.create_shared_pda_account(
group.clone(),
pda_seed,
pid,
identifier.unwrap_or_else(rand::random),
)?
} else {
wallet_core.create_shared_regular_account(group.clone())?
};
if let Some(label) = label {
wallet_core
.storage_mut()
.add_label(label, AccountIdWithPrivacy::Private(info.account_id))?;
}
println!("Shared account from group '{group}'");
println!("AccountId: Private/{}", info.account_id);
println!("NPK: {}", hex::encode(info.npk.0));
println!("VPK: {}", hex::encode(info.vpk.to_bytes()));
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::RegisterAccount {
account_id: info.account_id,
})
Self::handle_private_gms(group, label, pda, seed, program_id, identifier, wallet_core).await
}
Self::PrivateAccountsKey { cci } => {
let chain_index = wallet_core.create_private_accounts_key(cci);
let key_chain = wallet_core
.storage()
.key_chain()
.private_account_key_chain_by_index(&chain_index)
.expect("Key chain should exist after creation");
println!("Generated new private key node at path {chain_index}");
println!("With npk {}", hex::encode(key_chain.nullifier_public_key.0));
println!(
"With vpk {}",
hex::encode(key_chain.viewing_public_key.to_bytes())
);
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
Self::handle_private_accounts_key(cci, wallet_core).await
}
}
}
}
impl AccountSubcommand {
async fn handle_get(
raw: bool,
keys: bool,
account_id: CliAccountMention,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let resolved = account_id.resolve(wallet_core.storage())?;
wallet_core
.storage()
.labels_for_account(resolved)
.for_each(|label| {
println!("Label: {label}");
});
let account = wallet_core.get_account(resolved).await?;
// Helper closure to display keys for the account
let display_keys = |wallet_core: &WalletCore| -> Result<()> {
match resolved {
AccountIdWithPrivacy::Public(account_id) => {
let private_key = wallet_core
.storage
.key_chain()
.pub_account_signing_key(account_id)
.context("Public account not found in storage")?;
let public_key = PublicKey::new_from_private_key(private_key);
println!("pk {}", hex::encode(public_key.value()));
}
AccountIdWithPrivacy::Private(account_id) => {
let acc = wallet_core
.storage
.key_chain()
.private_account(account_id)
.context("Private account not found in storage")?;
println!("npk {}", hex::encode(acc.key_chain.nullifier_public_key.0));
println!(
"vpk {}",
hex::encode(acc.key_chain.viewing_public_key.to_bytes())
);
}
}
Ok(())
};
if account == Account::default() {
println!("Account is Uninitialized");
if keys {
display_keys(wallet_core)?;
}
return Ok(SubcommandReturnValue::Empty);
}
if raw {
let account_hr: HumanReadableAccount = account.into();
println!("{account_hr}");
return Ok(SubcommandReturnValue::Empty);
}
let (description, json_view) = format_account_details(&account);
println!("{description}");
println!("{json_view}");
if keys {
display_keys(wallet_core)?;
}
Ok(SubcommandReturnValue::Empty)
}
async fn handle_list(
long: bool,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let key_chain = &wallet_core.storage.key_chain();
let storage = wallet_core.storage();
let format_with_label =
|id: AccountIdWithPrivacy, chain_index: Option<&ChainIndex>| {
let id_str =
chain_index.map_or_else(|| id.to_string(), |cci| format!("{cci} {id}"));
let labels = storage.labels_for_account(id).format(", ").to_string();
if labels.is_empty() {
id_str
} else {
format!("{id_str} [{labels}]")
}
};
if !long {
let accounts = key_chain
.account_ids()
.map(|(id, idx)| format_with_label(id, idx))
.format("\n");
println!("{accounts}");
return Ok(SubcommandReturnValue::Empty);
}
// Detailed listing with --long flag
// Public key tree accounts
for (id, chain_index) in key_chain.public_account_ids() {
println!(
"{}",
format_with_label(AccountIdWithPrivacy::Public(id), chain_index)
);
match wallet_core.get_account_public(id).await {
Ok(account) if account != Account::default() => {
let (description, json_view) = format_account_details(&account);
println!(" {description}");
println!(" {json_view}");
}
Ok(_) => println!(" Uninitialized"),
Err(e) => println!(" Error fetching account: {e}"),
}
}
// Private key tree accounts
for (id, chain_index) in key_chain.private_account_ids() {
println!(
"{}",
format_with_label(AccountIdWithPrivacy::Private(id), chain_index)
);
match wallet_core.get_account_private(id) {
Some(account) if account != Account::default() => {
let (description, json_view) = format_account_details(&account);
println!(" {description}");
println!(" {json_view}");
}
Some(_) => println!(" Uninitialized"),
None => println!(" Not found in local storage"),
}
}
Ok(SubcommandReturnValue::Empty)
}
async fn handle_label(
account_id: CliAccountMention,
label: Label,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let account_id = account_id.resolve(wallet_core.storage())?;
wallet_core
.storage_mut()
.add_label(label.clone(), account_id)?;
wallet_core.store_persistent_data()?;
println!("Label '{label}' set for account {account_id}");
Ok(SubcommandReturnValue::Empty)
}
async fn handle_show_keys(
account_id: CliAccountMention,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let resolved = account_id.resolve(wallet_core.storage())?;
let AccountIdWithPrivacy::Private(account_id) = resolved else {
anyhow::bail!(
"wallet::cli::account::AccountSubcommand::ShowKeys: show-keys is only available for private accounts"
);
};
let entry = wallet_core
.storage()
.key_chain()
.private_account(account_id)
.ok_or_else(|| anyhow::anyhow!("wallet::cli::account::AccountSubcommand::ShowKeys: private account not found in wallet"))?;
println!("{}", hex::encode(entry.key_chain.nullifier_public_key.0));
println!(
"{}",
hex::encode(entry.key_chain.viewing_public_key.to_bytes())
);
Ok(SubcommandReturnValue::Empty)
}
}
impl WalletSubcommand for AccountSubcommand {
async fn handle_subcommand(
self,
@ -293,178 +516,21 @@ impl WalletSubcommand for AccountSubcommand {
raw,
keys,
account_id,
} => {
let resolved = account_id.resolve(wallet_core.storage())?;
wallet_core
.storage()
.labels_for_account(resolved)
.for_each(|label| {
println!("Label: {label}");
});
let account = wallet_core.get_account(resolved).await?;
// Helper closure to display keys for the account
let display_keys = |wallet_core: &WalletCore| -> Result<()> {
match resolved {
AccountIdWithPrivacy::Public(account_id) => {
let private_key = wallet_core
.storage
.key_chain()
.pub_account_signing_key(account_id)
.context("Public account not found in storage")?;
let public_key = PublicKey::new_from_private_key(private_key);
println!("pk {}", hex::encode(public_key.value()));
}
AccountIdWithPrivacy::Private(account_id) => {
let acc = wallet_core
.storage
.key_chain()
.private_account(account_id)
.context("Private account not found in storage")?;
println!("npk {}", hex::encode(acc.key_chain.nullifier_public_key.0));
println!(
"vpk {}",
hex::encode(acc.key_chain.viewing_public_key.to_bytes())
);
}
}
Ok(())
};
if account == Account::default() {
println!("Account is Uninitialized");
if keys {
display_keys(wallet_core)?;
}
return Ok(SubcommandReturnValue::Empty);
}
if raw {
let account_hr: HumanReadableAccount = account.into();
println!("{account_hr}");
return Ok(SubcommandReturnValue::Empty);
}
let (description, json_view) = format_account_details(&account);
println!("{description}");
println!("{json_view}");
if keys {
display_keys(wallet_core)?;
}
Ok(SubcommandReturnValue::Empty)
}
} => Self::handle_get(raw, keys, account_id, wallet_core).await,
Self::New(new_subcommand) => new_subcommand.handle_subcommand(wallet_core).await,
Self::SyncPrivate => {
let curr_last_block = wallet_core.sync_to_latest_block().await?;
Ok(SubcommandReturnValue::SyncedToBlock(curr_last_block))
}
Self::List { long } => {
let key_chain = &wallet_core.storage.key_chain();
let storage = wallet_core.storage();
let format_with_label =
|id: AccountIdWithPrivacy, chain_index: Option<&ChainIndex>| {
let id_str =
chain_index.map_or_else(|| id.to_string(), |cci| format!("{cci} {id}"));
let labels = storage.labels_for_account(id).format(", ").to_string();
if labels.is_empty() {
id_str
} else {
format!("{id_str} [{labels}]")
}
};
if !long {
let accounts = key_chain
.account_ids()
.map(|(id, idx)| format_with_label(id, idx))
.format("\n");
println!("{accounts}");
return Ok(SubcommandReturnValue::Empty);
}
// Detailed listing with --long flag
// Public key tree accounts
for (id, chain_index) in key_chain.public_account_ids() {
println!(
"{}",
format_with_label(AccountIdWithPrivacy::Public(id), chain_index)
);
match wallet_core.get_account_public(id).await {
Ok(account) if account != Account::default() => {
let (description, json_view) = format_account_details(&account);
println!(" {description}");
println!(" {json_view}");
}
Ok(_) => println!(" Uninitialized"),
Err(e) => println!(" Error fetching account: {e}"),
}
}
// Private key tree accounts
for (id, chain_index) in key_chain.private_account_ids() {
println!(
"{}",
format_with_label(AccountIdWithPrivacy::Private(id), chain_index)
);
match wallet_core.get_account_private(id) {
Some(account) if account != Account::default() => {
let (description, json_view) = format_account_details(&account);
println!(" {description}");
println!(" {json_view}");
}
Some(_) => println!(" Uninitialized"),
None => println!(" Not found in local storage"),
}
}
Ok(SubcommandReturnValue::Empty)
}
Self::List { long } => Self::handle_list(long, wallet_core).await,
Self::Label { account_id, label } => {
let account_id = account_id.resolve(wallet_core.storage())?;
wallet_core
.storage_mut()
.add_label(label.clone(), account_id)?;
wallet_core.store_persistent_data()?;
println!("Label '{label}' set for account {account_id}");
Ok(SubcommandReturnValue::Empty)
Self::handle_label(account_id, label, wallet_core).await
}
Self::Import(import_subcommand) => {
import_subcommand.handle_subcommand(wallet_core).await
}
Self::ShowKeys { account_id } => {
let resolved = account_id.resolve(wallet_core.storage())?;
let AccountIdWithPrivacy::Private(account_id) = resolved else {
anyhow::bail!(
"wallet::cli::account::AccountSubcommand::ShowKeys: show-keys is only available for private accounts"
);
};
let entry = wallet_core
.storage()
.key_chain()
.private_account(account_id)
.ok_or_else(|| anyhow::anyhow!("wallet::cli::account::AccountSubcommand::ShowKeys: private account not found in wallet"))?;
println!("{}", hex::encode(entry.key_chain.nullifier_public_key.0));
println!(
"{}",
hex::encode(entry.key_chain.viewing_public_key.to_bytes())
);
Ok(SubcommandReturnValue::Empty)
Self::handle_show_keys(account_id, wallet_core).await
}
}
}

View File

@ -23,122 +23,147 @@ pub enum ConfigSubcommand {
Description { key: String },
}
impl WalletSubcommand for ConfigSubcommand {
async fn handle_subcommand(
self,
impl ConfigSubcommand {
async fn handle_get(
all: bool,
key: Option<String>,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let config = wallet_core.config();
match self {
Self::Get { all, key } => {
if all {
let config_str = serde_json::to_string_pretty(&config)?;
if all {
let config_str = serde_json::to_string_pretty(&config)?;
println!("{config_str}");
} else if let Some(key) = key {
match key.as_str() {
"sequencer_addr" => {
println!("{}", config.sequencer_addr);
}
"seq_poll_timeout" => {
println!("{:?}", config.seq_poll_timeout);
}
"seq_tx_poll_max_blocks" => {
println!("{}", config.seq_tx_poll_max_blocks);
}
"seq_poll_max_retries" => {
println!("{}", config.seq_poll_max_retries);
}
"seq_block_poll_max_amount" => {
println!("{}", config.seq_block_poll_max_amount);
}
"basic_auth" => {
if let Some(basic_auth) = &config.basic_auth {
println!("{basic_auth}");
} else {
println!("Not set");
}
}
_ => {
println!("Unknown field");
}
}
} else {
println!("Please provide a key or use --all flag");
}
}
Self::Set { key, value } => {
let mut config = config.clone();
match key.as_str() {
"sequencer_addr" => {
config.sequencer_addr = value.parse()?;
}
"seq_poll_timeout" => {
config.seq_poll_timeout = humantime::parse_duration(&value)
.map_err(|e| anyhow::anyhow!("Invalid duration: {e}"))?;
}
"seq_tx_poll_max_blocks" => {
config.seq_tx_poll_max_blocks = value.parse()?;
}
"seq_poll_max_retries" => {
config.seq_poll_max_retries = value.parse()?;
}
"seq_block_poll_max_amount" => {
config.seq_block_poll_max_amount = value.parse()?;
}
"basic_auth" => {
config.basic_auth = Some(value.parse()?);
}
"initial_accounts" => {
anyhow::bail!("Setting this field from wallet is not supported");
}
_ => {
anyhow::bail!("Unknown field");
}
}
wallet_core.set_config(config);
wallet_core.store_config_changes().await?;
}
Self::Description { key } => match key.as_str() {
"override_rust_log" => {
println!("Value of variable RUST_LOG to override, affects logging");
}
println!("{config_str}");
} else if let Some(key) = key {
match key.as_str() {
"sequencer_addr" => {
println!("HTTP V4 account_id of sequencer");
println!("{}", config.sequencer_addr);
}
"seq_poll_timeout" => {
println!(
"Sequencer client retry variable: how much time to wait between retries (human readable duration)"
);
println!("{:?}", config.seq_poll_timeout);
}
"seq_tx_poll_max_blocks" => {
println!(
"Sequencer client polling variable: max number of blocks to poll to find a transaction"
);
println!("{}", config.seq_tx_poll_max_blocks);
}
"seq_poll_max_retries" => {
println!(
"Sequencer client retry variable: max number of retries before failing(can be zero)"
);
println!("{}", config.seq_poll_max_retries);
}
"seq_block_poll_max_amount" => {
println!(
"Sequencer client polling variable: max number of blocks to request in one polling call"
);
}
"initial_accounts" => {
println!("List of initial accounts' keys(both public and private)");
println!("{}", config.seq_block_poll_max_amount);
}
"basic_auth" => {
println!("Basic authentication credentials for sequencer HTTP requests");
if let Some(basic_auth) = &config.basic_auth {
println!("{basic_auth}");
} else {
println!("Not set");
}
}
_ => {
println!("Unknown field");
}
},
}
} else {
println!("Please provide a key or use --all flag");
}
Ok(SubcommandReturnValue::Empty)
}
async fn handle_set(
key: String,
value: String,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let mut config = wallet_core.config().clone();
match key.as_str() {
"sequencer_addr" => {
config.sequencer_addr = value.parse()?;
}
"seq_poll_timeout" => {
config.seq_poll_timeout = humantime::parse_duration(&value)
.map_err(|e| anyhow::anyhow!("Invalid duration: {e}"))?;
}
"seq_tx_poll_max_blocks" => {
config.seq_tx_poll_max_blocks = value.parse()?;
}
"seq_poll_max_retries" => {
config.seq_poll_max_retries = value.parse()?;
}
"seq_block_poll_max_amount" => {
config.seq_block_poll_max_amount = value.parse()?;
}
"basic_auth" => {
config.basic_auth = Some(value.parse()?);
}
"initial_accounts" => {
anyhow::bail!("Setting this field from wallet is not supported");
}
_ => {
anyhow::bail!("Unknown field");
}
}
wallet_core.set_config(config);
wallet_core.store_config_changes().await?;
Ok(SubcommandReturnValue::Empty)
}
async fn handle_description(
key: String,
_wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
match key.as_str() {
"override_rust_log" => {
println!("Value of variable RUST_LOG to override, affects logging");
}
"sequencer_addr" => {
println!("HTTP V4 account_id of sequencer");
}
"seq_poll_timeout" => {
println!(
"Sequencer client retry variable: how much time to wait between retries (human readable duration)"
);
}
"seq_tx_poll_max_blocks" => {
println!(
"Sequencer client polling variable: max number of blocks to poll to find a transaction"
);
}
"seq_poll_max_retries" => {
println!(
"Sequencer client retry variable: max number of retries before failing(can be zero)"
);
}
"seq_block_poll_max_amount" => {
println!(
"Sequencer client polling variable: max number of blocks to request in one polling call"
);
}
"initial_accounts" => {
println!("List of initial accounts' keys(both public and private)");
}
"basic_auth" => {
println!("Basic authentication credentials for sequencer HTTP requests");
}
_ => {
println!("Unknown field");
}
}
Ok(SubcommandReturnValue::Empty)
}
}
impl WalletSubcommand for ConfigSubcommand {
async fn handle_subcommand(
self,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
match self {
Self::Get { all, key } => Self::handle_get(all, key, wallet_core).await,
Self::Set { key, value } => Self::handle_set(key, value, wallet_core).await,
Self::Description { key } => Self::handle_description(key, wallet_core).await,
}
}
}

View File

@ -49,127 +49,144 @@ pub enum GroupSubcommand {
NewSealingKey,
}
impl GroupSubcommand {
async fn handle_new(name: Label, wallet_core: &mut WalletCore) -> Result<SubcommandReturnValue> {
if wallet_core
.storage()
.key_chain()
.group_key_holder(&name)
.is_some()
{
anyhow::bail!("Group '{name}' already exists");
}
let holder = GroupKeyHolder::new();
wallet_core.insert_group_key_holder(name.clone(), holder);
wallet_core.store_persistent_data()?;
println!("Created group '{name}'");
Ok(SubcommandReturnValue::Empty)
}
async fn handle_list(wallet_core: &mut WalletCore) -> Result<SubcommandReturnValue> {
let mut empty = true;
let holders_iter = wallet_core.storage().key_chain().group_key_holders_iter();
for (name, _) in holders_iter {
empty = false;
println!("{name}");
}
if empty {
println!("No groups found");
}
Ok(SubcommandReturnValue::Empty)
}
async fn handle_remove(name: Label, wallet_core: &mut WalletCore) -> Result<SubcommandReturnValue> {
if wallet_core.remove_group_key_holder(&name).is_none() {
anyhow::bail!("Group '{name}' not found");
}
wallet_core.store_persistent_data()?;
println!("Removed group '{name}'");
Ok(SubcommandReturnValue::Empty)
}
async fn handle_invite(
name: Label,
key: String,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let holder = wallet_core
.storage()
.key_chain()
.group_key_holder(&name)
.context(format!("Group '{name}' not found"))?;
let key_bytes = hex::decode(&key).context("Invalid key hex")?;
let recipient_key =
key_protocol::key_management::group_key_holder::SealingPublicKey::from_bytes(
key_bytes,
);
let sealed = holder.seal_for(&recipient_key);
println!("{}", hex::encode(&sealed));
Ok(SubcommandReturnValue::Empty)
}
async fn handle_join(
name: Label,
sealed: String,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
if wallet_core
.storage()
.key_chain()
.group_key_holder(&name)
.is_some()
{
anyhow::bail!("Group '{name}' already exists");
}
let sealing_key = wallet_core
.storage()
.key_chain()
.sealing_secret_key()
.context("No sealing key found. Run 'wallet group new-sealing-key' first.")?;
let sealed_bytes = hex::decode(&sealed).context("Invalid sealed hex")?;
let holder = GroupKeyHolder::unseal(&sealed_bytes, sealing_key)
.map_err(|e| anyhow::anyhow!("Failed to unseal: {e:?}"))?;
wallet_core.insert_group_key_holder(name.clone(), holder);
wallet_core.store_persistent_data()?;
println!("Joined group '{name}'");
Ok(SubcommandReturnValue::Empty)
}
async fn handle_new_sealing_key(wallet_core: &mut WalletCore) -> Result<SubcommandReturnValue> {
if wallet_core
.storage()
.key_chain()
.sealing_secret_key()
.is_some()
{
anyhow::bail!("Sealing key already exists. Each wallet has one sealing key.");
}
let mut d = [0_u8; 32];
let mut r = [0_u8; 32];
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut d);
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut r);
let secret = ViewingSecretKey::new(d, r);
let ek_bytes = lee_core::encryption::ViewingPublicKey::from_seed(&d, &r)
.to_bytes()
.to_vec();
let public_key = SealingPublicKey::from_bytes(ek_bytes);
wallet_core.set_sealing_secret_key(secret);
wallet_core.store_persistent_data()?;
println!("Sealing key generated.");
println!("Public key: {}", hex::encode(public_key.to_bytes()));
println!("Share this public key with group members so they can seal GMS for you.");
Ok(SubcommandReturnValue::Empty)
}
}
impl WalletSubcommand for GroupSubcommand {
async fn handle_subcommand(
self,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
match self {
Self::New { name } => {
if wallet_core
.storage()
.key_chain()
.group_key_holder(&name)
.is_some()
{
anyhow::bail!("Group '{name}' already exists");
}
let holder = GroupKeyHolder::new();
wallet_core.insert_group_key_holder(name.clone(), holder);
wallet_core.store_persistent_data()?;
println!("Created group '{name}'");
Ok(SubcommandReturnValue::Empty)
}
Self::List => {
let mut empty = true;
let holders_iter = wallet_core.storage().key_chain().group_key_holders_iter();
for (name, _) in holders_iter {
empty = false;
println!("{name}");
}
if empty {
println!("No groups found");
}
Ok(SubcommandReturnValue::Empty)
}
Self::Remove { name } => {
if wallet_core.remove_group_key_holder(&name).is_none() {
anyhow::bail!("Group '{name}' not found");
}
wallet_core.store_persistent_data()?;
println!("Removed group '{name}'");
Ok(SubcommandReturnValue::Empty)
}
Self::Invite { name, key } => {
let holder = wallet_core
.storage()
.key_chain()
.group_key_holder(&name)
.context(format!("Group '{name}' not found"))?;
let key_bytes = hex::decode(&key).context("Invalid key hex")?;
let recipient_key =
key_protocol::key_management::group_key_holder::SealingPublicKey::from_bytes(
key_bytes,
);
let sealed = holder.seal_for(&recipient_key);
println!("{}", hex::encode(&sealed));
Ok(SubcommandReturnValue::Empty)
}
Self::Join { name, sealed } => {
if wallet_core
.storage()
.key_chain()
.group_key_holder(&name)
.is_some()
{
anyhow::bail!("Group '{name}' already exists");
}
let sealing_key = wallet_core
.storage()
.key_chain()
.sealing_secret_key()
.context("No sealing key found. Run 'wallet group new-sealing-key' first.")?;
let sealed_bytes = hex::decode(&sealed).context("Invalid sealed hex")?;
let holder = GroupKeyHolder::unseal(&sealed_bytes, sealing_key)
.map_err(|e| anyhow::anyhow!("Failed to unseal: {e:?}"))?;
wallet_core.insert_group_key_holder(name.clone(), holder);
wallet_core.store_persistent_data()?;
println!("Joined group '{name}'");
Ok(SubcommandReturnValue::Empty)
}
Self::NewSealingKey => {
if wallet_core
.storage()
.key_chain()
.sealing_secret_key()
.is_some()
{
anyhow::bail!("Sealing key already exists. Each wallet has one sealing key.");
}
let mut d = [0_u8; 32];
let mut r = [0_u8; 32];
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut d);
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut r);
let secret = ViewingSecretKey::new(d, r);
let ek_bytes = lee_core::encryption::ViewingPublicKey::from_seed(&d, &r)
.to_bytes()
.to_vec();
let public_key = SealingPublicKey::from_bytes(ek_bytes);
wallet_core.set_sealing_secret_key(secret);
wallet_core.store_persistent_data()?;
println!("Sealing key generated.");
println!("Public key: {}", hex::encode(public_key.to_bytes()));
println!("Share this public key with group members so they can seal GMS for you.");
Ok(SubcommandReturnValue::Empty)
}
Self::New { name } => Self::handle_new(name, wallet_core).await,
Self::List => Self::handle_list(wallet_core).await,
Self::Remove { name } => Self::handle_remove(name, wallet_core).await,
Self::Invite { name, key } => Self::handle_invite(name, key, wallet_core).await,
Self::Join { name, sealed } => Self::handle_join(name, sealed, wallet_core).await,
Self::NewSealingKey => Self::handle_new_sealing_key(wallet_core).await,
}
}
}

View File

@ -37,144 +37,165 @@ pub enum KeycardSubcommand {
},
}
impl KeycardSubcommand {
async fn handle_available(_wallet_core: &mut WalletCore) -> Result<SubcommandReturnValue> {
Python::attach(|py| {
python_path::add_python_path(py)
.expect("`wallet::keycard::available`: unable to setup python path");
let wallet = KeycardWallet::new(py)
.expect("`wallet::keycard::available`: invalid data received for pin");
let available = wallet.is_unpaired_keycard_available(py).expect(
"`wallet::keycard::available`: received invalid data from Keycard wrapper",
);
if available {
println!("\u{2705} Keycard is available.");
} else {
println!("\u{274c} Keycard is not available.");
}
});
Ok(SubcommandReturnValue::Empty)
}
async fn handle_connect(_wallet_core: &mut WalletCore) -> Result<SubcommandReturnValue> {
let pin = read_pin()?;
Python::attach(|py| {
python_path::add_python_path(py)
.expect("`wallet::keycard::connect`: unable to setup python path");
let wallet = KeycardWallet::new(py)
.expect("`wallet::keycard::connect`: invalid keycard wallet provided");
wallet
.connect(py, &pin)
.expect("`wallet::keycard::connect`: failed to connect to keycard");
println!("\u{2705} Keycard paired and ready.");
drop(wallet.close_session(py));
});
Ok(SubcommandReturnValue::Empty)
}
async fn handle_disconnect(_wallet_core: &mut WalletCore) -> Result<SubcommandReturnValue> {
let pin = read_pin()?;
Python::attach(|py| {
python_path::add_python_path(py)
.expect("`wallet::keycard::disconnect`: unable to setup python path");
let wallet = KeycardWallet::new(py)
.expect("`wallet::keycard::disconnect`: invalid keycard wallet provided");
wallet
.connect(py, &pin)
.expect("`wallet::keycard::disconnect`: failed to open session");
wallet
.disconnect(py)
.expect("`wallet::keycard::disconnect`: failed to unpair keycard");
clear_pairing();
println!("\u{2705} Keycard unpaired and pairing cleared.");
});
Ok(SubcommandReturnValue::Empty)
}
async fn handle_init(_wallet_core: &mut WalletCore) -> Result<SubcommandReturnValue> {
let pin = read_pin()?;
Python::attach(|py| {
python_path::add_python_path(py)
.expect("`wallet::keycard::init`: unable to setup python path");
let wallet = KeycardWallet::new(py)
.expect("`wallet::keycard::init`: invalid keycard wallet provided");
let initialized = wallet
.initialize(py, &pin)
.expect("`wallet::keycard::init`: failed to initialize keycard");
if initialized {
clear_pairing();
println!("\u{2705} Keycard initialized successfully.");
}
});
Ok(SubcommandReturnValue::Empty)
}
async fn handle_load(_wallet_core: &mut WalletCore) -> Result<SubcommandReturnValue> {
let pin = read_pin()?;
let mnemonic = read_mnemonic()?;
Python::attach(|py| {
python_path::add_python_path(py)
.expect("`wallet::keycard::load`: unable to setup python path");
let wallet = KeycardWallet::new(py)
.expect("`wallet::keycard::load`: invalid keycard wallet provided");
wallet
.connect(py, &pin)
.expect("`wallet::keycard::load`: failed to connect to keycard");
println!("\u{2705} Keycard is now connected to wallet.");
if wallet.load_mnemonic(py, &mnemonic).is_ok() {
println!("\u{2705} Mnemonic phrase loaded successfully.");
} else {
println!("\u{274c} Failed to load mnemonic phrase.");
}
drop(wallet.close_session(py));
});
Ok(SubcommandReturnValue::Empty)
}
#[cfg(feature = "keycard-debug")]
async fn handle_get_private_keys(
key_path: String,
reveal: bool,
_wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
if !reveal {
eprintln!(
"WARNING: pass --reveal to print NSK and VSK. \
Disclosing either key fully compromises the account's privacy."
);
return Ok(SubcommandReturnValue::Empty);
}
eprintln!(
"WARNING: NSK and VSK are being printed to stdout. \
Any terminal log, scrollback, or screen recording captures these keys."
);
let pin = read_pin()?;
let (nsk, vsk) =
KeycardWallet::get_private_keys_for_path_with_connect(&pin, &key_path)
.map_err(anyhow::Error::from)?;
println!("NSK: {}", hex::encode(*nsk));
println!("VSK: {}", hex::encode(*vsk));
Ok(SubcommandReturnValue::Empty)
}
}
impl WalletSubcommand for KeycardSubcommand {
async fn handle_subcommand(
self,
_wallet_core: &mut WalletCore,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
match self {
Self::Available => {
Python::attach(|py| {
python_path::add_python_path(py)
.expect("`wallet::keycard::available`: unable to setup python path");
let wallet = KeycardWallet::new(py)
.expect("`wallet::keycard::available`: invalid data received for pin");
let available = wallet.is_unpaired_keycard_available(py).expect(
"`wallet::keycard::available`: received invalid data from Keycard wrapper",
);
if available {
println!("\u{2705} Keycard is available.");
} else {
println!("\u{274c} Keycard is not available.");
}
});
Ok(SubcommandReturnValue::Empty)
}
Self::Connect => {
let pin = read_pin()?;
Python::attach(|py| {
python_path::add_python_path(py)
.expect("`wallet::keycard::connect`: unable to setup python path");
let wallet = KeycardWallet::new(py)
.expect("`wallet::keycard::connect`: invalid keycard wallet provided");
wallet
.connect(py, &pin)
.expect("`wallet::keycard::connect`: failed to connect to keycard");
println!("\u{2705} Keycard paired and ready.");
drop(wallet.close_session(py));
});
Ok(SubcommandReturnValue::Empty)
}
Self::Disconnect => {
let pin = read_pin()?;
Python::attach(|py| {
python_path::add_python_path(py)
.expect("`wallet::keycard::disconnect`: unable to setup python path");
let wallet = KeycardWallet::new(py)
.expect("`wallet::keycard::disconnect`: invalid keycard wallet provided");
wallet
.connect(py, &pin)
.expect("`wallet::keycard::disconnect`: failed to open session");
wallet
.disconnect(py)
.expect("`wallet::keycard::disconnect`: failed to unpair keycard");
clear_pairing();
println!("\u{2705} Keycard unpaired and pairing cleared.");
});
Ok(SubcommandReturnValue::Empty)
}
Self::Init => {
let pin = read_pin()?;
Python::attach(|py| {
python_path::add_python_path(py)
.expect("`wallet::keycard::init`: unable to setup python path");
let wallet = KeycardWallet::new(py)
.expect("`wallet::keycard::init`: invalid keycard wallet provided");
let initialized = wallet
.initialize(py, &pin)
.expect("`wallet::keycard::init`: failed to initialize keycard");
if initialized {
clear_pairing();
println!("\u{2705} Keycard initialized successfully.");
}
});
Ok(SubcommandReturnValue::Empty)
}
Self::Load => {
let pin = read_pin()?;
let mnemonic = read_mnemonic()?;
Python::attach(|py| {
python_path::add_python_path(py)
.expect("`wallet::keycard::load`: unable to setup python path");
let wallet = KeycardWallet::new(py)
.expect("`wallet::keycard::load`: invalid keycard wallet provided");
wallet
.connect(py, &pin)
.expect("`wallet::keycard::load`: failed to connect to keycard");
println!("\u{2705} Keycard is now connected to wallet.");
if wallet.load_mnemonic(py, &mnemonic).is_ok() {
println!("\u{2705} Mnemonic phrase loaded successfully.");
} else {
println!("\u{274c} Failed to load mnemonic phrase.");
}
drop(wallet.close_session(py));
});
Ok(SubcommandReturnValue::Empty)
}
Self::Available => Self::handle_available(wallet_core).await,
Self::Connect => Self::handle_connect(wallet_core).await,
Self::Disconnect => Self::handle_disconnect(wallet_core).await,
Self::Init => Self::handle_init(wallet_core).await,
Self::Load => Self::handle_load(wallet_core).await,
#[cfg(feature = "keycard-debug")]
Self::GetPrivateKeys { key_path, reveal } => {
if !reveal {
eprintln!(
"WARNING: pass --reveal to print NSK and VSK. \
Disclosing either key fully compromises the account's privacy."
);
return Ok(SubcommandReturnValue::Empty);
}
eprintln!(
"WARNING: NSK and VSK are being printed to stdout. \
Any terminal log, scrollback, or screen recording captures these keys."
);
let pin = read_pin()?;
let (nsk, vsk) =
KeycardWallet::get_private_keys_for_path_with_connect(&pin, &key_path)
.map_err(anyhow::Error::from)?;
println!("NSK: {}", hex::encode(*nsk));
println!("VSK: {}", hex::encode(*vsk));
Ok(SubcommandReturnValue::Empty)
Self::handle_get_private_keys(key_path, reveal, wallet_core).await
}
}
}

View File

@ -341,6 +341,26 @@ pub fn read_keys_file(path: &str) -> Result<(Vec<u8>, Vec<u8>)> {
Ok((npk, vpk))
}
pub(crate) fn decode_npk_vpk(
npk_hex: &str,
vpk_hex: &str,
) -> Result<(
lee_core::NullifierPublicKey,
lee_core::encryption::ViewingPublicKey,
)> {
let npk_bytes: [u8; 32] = hex::decode(npk_hex)
.context("npk must be valid hex")?
.try_into()
.map_err(|v: Vec<u8>| anyhow::anyhow!("npk must be exactly 32 bytes, got {}", v.len()))?;
let vpk = lee_core::encryption::ViewingPublicKey::from_bytes(
hex::decode(vpk_hex).context("vpk must be valid hex")?,
)
.map_err(|e| anyhow::anyhow!("{e}"))?;
Ok((lee_core::NullifierPublicKey(npk_bytes), vpk))
}
pub fn read_mnemonic_from_stdin() -> Result<Mnemonic> {
let mut phrase = String::new();

View File

@ -118,6 +118,187 @@ pub enum AmmProgramAgnosticSubcommand {
},
}
impl AmmProgramAgnosticSubcommand {
async fn handle_new(
user_holding_a: CliAccountMention,
user_holding_b: CliAccountMention,
user_holding_lp: CliAccountMention,
balance_a: u128,
balance_b: u128,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let a_id = user_holding_a.resolve(wallet_core.storage())?;
let b_id = user_holding_b.resolve(wallet_core.storage())?;
let lp_id = user_holding_lp.resolve(wallet_core.storage())?;
match (a_id, b_id, lp_id) {
(
AccountIdWithPrivacy::Public(a),
AccountIdWithPrivacy::Public(b),
AccountIdWithPrivacy::Public(lp),
) => {
let tx_hash = Amm(wallet_core)
.send_new_definition(
user_holding_a.into_public_identity(a),
user_holding_b.into_public_identity(b),
user_holding_lp.into_public_identity(lp),
balance_a,
balance_b,
)
.await?;
wallet_core
.poll_and_finalize_public_transaction(tx_hash)
.await
}
_ => {
// ToDo: Implement after private multi-chain calls is available
anyhow::bail!("Only public execution allowed for Amm calls");
}
}
}
async fn handle_swap_exact_input(
user_holding_a: CliAccountMention,
user_holding_b: CliAccountMention,
amount_in: u128,
min_amount_out: u128,
token_definition: AccountId,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let a_id = user_holding_a.resolve(wallet_core.storage())?;
let b_id = user_holding_b.resolve(wallet_core.storage())?;
match (a_id, b_id) {
(AccountIdWithPrivacy::Public(a), AccountIdWithPrivacy::Public(b)) => {
let tx_hash = Amm(wallet_core)
.send_swap_exact_input(
user_holding_a.into_public_identity(a),
user_holding_b.into_public_identity(b),
amount_in,
min_amount_out,
token_definition,
)
.await?;
wallet_core
.poll_and_finalize_public_transaction(tx_hash)
.await
}
_ => {
// ToDo: Implement after private multi-chain calls is available
anyhow::bail!("Only public execution allowed for Amm calls");
}
}
}
async fn handle_swap_exact_output(
user_holding_a: CliAccountMention,
user_holding_b: CliAccountMention,
exact_amount_out: u128,
max_amount_in: u128,
token_definition: AccountId,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let a_id = user_holding_a.resolve(wallet_core.storage())?;
let b_id = user_holding_b.resolve(wallet_core.storage())?;
match (a_id, b_id) {
(AccountIdWithPrivacy::Public(a), AccountIdWithPrivacy::Public(b)) => {
let tx_hash = Amm(wallet_core)
.send_swap_exact_output(
user_holding_a.into_public_identity(a),
user_holding_b.into_public_identity(b),
exact_amount_out,
max_amount_in,
token_definition,
)
.await?;
wallet_core
.poll_and_finalize_public_transaction(tx_hash)
.await
}
_ => {
// ToDo: Implement after private multi-chain calls is available
anyhow::bail!("Only public execution allowed for Amm calls");
}
}
}
async fn handle_add_liquidity(
user_holding_a: CliAccountMention,
user_holding_b: CliAccountMention,
user_holding_lp: CliAccountMention,
min_amount_lp: u128,
max_amount_a: u128,
max_amount_b: u128,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let a_id = user_holding_a.resolve(wallet_core.storage())?;
let b_id = user_holding_b.resolve(wallet_core.storage())?;
let lp_id = user_holding_lp.resolve(wallet_core.storage())?;
match (a_id, b_id, lp_id) {
(
AccountIdWithPrivacy::Public(a),
AccountIdWithPrivacy::Public(b),
AccountIdWithPrivacy::Public(lp),
) => {
let tx_hash = Amm(wallet_core)
.send_add_liquidity(
user_holding_a.into_public_identity(a),
user_holding_b.into_public_identity(b),
user_holding_lp.into_public_identity(lp),
min_amount_lp,
max_amount_a,
max_amount_b,
)
.await?;
wallet_core
.poll_and_finalize_public_transaction(tx_hash)
.await
}
_ => {
// ToDo: Implement after private multi-chain calls is available
anyhow::bail!("Only public execution allowed for Amm calls");
}
}
}
async fn handle_remove_liquidity(
user_holding_a: CliAccountMention,
user_holding_b: CliAccountMention,
user_holding_lp: CliAccountMention,
balance_lp: u128,
min_amount_a: u128,
min_amount_b: u128,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let a_id = user_holding_a.resolve(wallet_core.storage())?;
let b_id = user_holding_b.resolve(wallet_core.storage())?;
let lp_id = user_holding_lp.resolve(wallet_core.storage())?;
match (a_id, b_id, lp_id) {
(
AccountIdWithPrivacy::Public(a),
AccountIdWithPrivacy::Public(b),
AccountIdWithPrivacy::Public(lp),
) => {
let tx_hash = Amm(wallet_core)
.send_remove_liquidity(
a,
b,
user_holding_lp.into_public_identity(lp),
balance_lp,
min_amount_a,
min_amount_b,
)
.await?;
wallet_core
.poll_and_finalize_public_transaction(tx_hash)
.await
}
_ => {
// ToDo: Implement after private multi-chain calls is available
anyhow::bail!("Only public execution allowed for Amm calls");
}
}
}
}
impl WalletSubcommand for AmmProgramAgnosticSubcommand {
async fn handle_subcommand(
self,
@ -131,35 +312,15 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand {
balance_a,
balance_b,
} => {
let a_id = user_holding_a.resolve(wallet_core.storage())?;
let b_id = user_holding_b.resolve(wallet_core.storage())?;
let lp_id = user_holding_lp.resolve(wallet_core.storage())?;
match (a_id, b_id, lp_id) {
(
AccountIdWithPrivacy::Public(a),
AccountIdWithPrivacy::Public(b),
AccountIdWithPrivacy::Public(lp),
) => {
let tx_hash = Amm(wallet_core)
.send_new_definition(
user_holding_a.into_public_identity(a),
user_holding_b.into_public_identity(b),
user_holding_lp.into_public_identity(lp),
balance_a,
balance_b,
)
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
_ => {
// ToDo: Implement after private multi-chain calls is available
anyhow::bail!("Only public execution allowed for Amm calls");
}
}
Self::handle_new(
user_holding_a,
user_holding_b,
user_holding_lp,
balance_a,
balance_b,
wallet_core,
)
.await
}
Self::SwapExactInput {
user_holding_a,
@ -168,30 +329,15 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand {
min_amount_out,
token_definition,
} => {
let a_id = user_holding_a.resolve(wallet_core.storage())?;
let b_id = user_holding_b.resolve(wallet_core.storage())?;
match (a_id, b_id) {
(AccountIdWithPrivacy::Public(a), AccountIdWithPrivacy::Public(b)) => {
let tx_hash = Amm(wallet_core)
.send_swap_exact_input(
user_holding_a.into_public_identity(a),
user_holding_b.into_public_identity(b),
amount_in,
min_amount_out,
token_definition,
)
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
_ => {
// ToDo: Implement after private multi-chain calls is available
anyhow::bail!("Only public execution allowed for Amm calls");
}
}
Self::handle_swap_exact_input(
user_holding_a,
user_holding_b,
amount_in,
min_amount_out,
token_definition,
wallet_core,
)
.await
}
Self::SwapExactOutput {
user_holding_a,
@ -200,30 +346,15 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand {
max_amount_in,
token_definition,
} => {
let a_id = user_holding_a.resolve(wallet_core.storage())?;
let b_id = user_holding_b.resolve(wallet_core.storage())?;
match (a_id, b_id) {
(AccountIdWithPrivacy::Public(a), AccountIdWithPrivacy::Public(b)) => {
let tx_hash = Amm(wallet_core)
.send_swap_exact_output(
user_holding_a.into_public_identity(a),
user_holding_b.into_public_identity(b),
exact_amount_out,
max_amount_in,
token_definition,
)
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
_ => {
// ToDo: Implement after private multi-chain calls is available
anyhow::bail!("Only public execution allowed for Amm calls");
}
}
Self::handle_swap_exact_output(
user_holding_a,
user_holding_b,
exact_amount_out,
max_amount_in,
token_definition,
wallet_core,
)
.await
}
Self::AddLiquidity {
user_holding_a,
@ -233,36 +364,16 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand {
max_amount_a,
max_amount_b,
} => {
let a_id = user_holding_a.resolve(wallet_core.storage())?;
let b_id = user_holding_b.resolve(wallet_core.storage())?;
let lp_id = user_holding_lp.resolve(wallet_core.storage())?;
match (a_id, b_id, lp_id) {
(
AccountIdWithPrivacy::Public(a),
AccountIdWithPrivacy::Public(b),
AccountIdWithPrivacy::Public(lp),
) => {
let tx_hash = Amm(wallet_core)
.send_add_liquidity(
user_holding_a.into_public_identity(a),
user_holding_b.into_public_identity(b),
user_holding_lp.into_public_identity(lp),
min_amount_lp,
max_amount_a,
max_amount_b,
)
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
_ => {
// ToDo: Implement after private multi-chain calls is available
anyhow::bail!("Only public execution allowed for Amm calls");
}
}
Self::handle_add_liquidity(
user_holding_a,
user_holding_b,
user_holding_lp,
min_amount_lp,
max_amount_a,
max_amount_b,
wallet_core,
)
.await
}
Self::RemoveLiquidity {
user_holding_a,
@ -272,36 +383,16 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand {
min_amount_a,
min_amount_b,
} => {
let a_id = user_holding_a.resolve(wallet_core.storage())?;
let b_id = user_holding_b.resolve(wallet_core.storage())?;
let lp_id = user_holding_lp.resolve(wallet_core.storage())?;
match (a_id, b_id, lp_id) {
(
AccountIdWithPrivacy::Public(a),
AccountIdWithPrivacy::Public(b),
AccountIdWithPrivacy::Public(lp),
) => {
let tx_hash = Amm(wallet_core)
.send_remove_liquidity(
a,
b,
user_holding_lp.into_public_identity(lp),
balance_lp,
min_amount_a,
min_amount_b,
)
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
_ => {
// ToDo: Implement after private multi-chain calls is available
anyhow::bail!("Only public execution allowed for Amm calls");
}
}
Self::handle_remove_liquidity(
user_holding_a,
user_holding_b,
user_holding_lp,
balance_lp,
min_amount_a,
min_amount_b,
wallet_core,
)
.await
}
}
}

View File

@ -1,6 +1,5 @@
use anyhow::Result;
use clap::Subcommand;
use common::transaction::LeeTransaction;
use lee::{Account, AccountId};
use token_core::TokenHolding;
@ -69,6 +68,161 @@ pub enum AtaSubcommand {
},
}
impl AtaSubcommand {
async fn handle_address(
owner: AccountId,
token_definition: AccountId,
_wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let ata_program_id = programs::ata().id();
let ata_id = associated_token_account_core::get_associated_token_account_id(
&ata_program_id,
&associated_token_account_core::compute_ata_seed(owner, token_definition),
);
println!("{ata_id}");
Ok(SubcommandReturnValue::Empty)
}
async fn handle_create(
owner: CliAccountMention,
token_definition: AccountId,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let owner_resolved = owner.resolve(wallet_core.storage())?;
let definition_id = token_definition;
match owner_resolved {
AccountIdWithPrivacy::Public(owner_id) => {
let tx_hash = Ata(wallet_core)
.send_create(owner.into_public_identity(owner_id), definition_id)
.await?;
wallet_core
.poll_and_finalize_public_transaction(tx_hash)
.await
}
AccountIdWithPrivacy::Private(owner_id) => {
let (tx_hash, secret) = Ata(wallet_core)
.send_create_private_owner(owner_id, definition_id)
.await?;
wallet_core
.poll_and_finalize_pp_transaction(tx_hash, &[
Decode(secret, owner_id),
])
.await
}
}
}
async fn handle_send(
from: CliAccountMention,
token_definition: AccountId,
to: AccountId,
amount: u128,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let from_resolved = from.resolve(wallet_core.storage())?;
let definition_id = token_definition;
let to_id = to;
match from_resolved {
AccountIdWithPrivacy::Public(from_id) => {
let tx_hash = Ata(wallet_core)
.send_transfer(
from.into_public_identity(from_id),
definition_id,
to_id,
amount,
)
.await?;
wallet_core
.poll_and_finalize_public_transaction(tx_hash)
.await
}
AccountIdWithPrivacy::Private(from_id) => {
let (tx_hash, secret) = Ata(wallet_core)
.send_transfer_private_owner(from_id, definition_id, to_id, amount)
.await?;
wallet_core
.poll_and_finalize_pp_transaction(tx_hash, &[
Decode(secret, from_id),
])
.await
}
}
}
async fn handle_burn(
holder: CliAccountMention,
token_definition: AccountId,
amount: u128,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let holder_resolved = holder.resolve(wallet_core.storage())?;
let definition_id = token_definition;
match holder_resolved {
AccountIdWithPrivacy::Public(holder_id) => {
let tx_hash = Ata(wallet_core)
.send_burn(
holder.into_public_identity(holder_id),
definition_id,
amount,
)
.await?;
wallet_core
.poll_and_finalize_public_transaction(tx_hash)
.await
}
AccountIdWithPrivacy::Private(holder_id) => {
let (tx_hash, secret) = Ata(wallet_core)
.send_burn_private_owner(holder_id, definition_id, amount)
.await?;
wallet_core
.poll_and_finalize_pp_transaction(tx_hash, &[
Decode(secret, holder_id),
])
.await
}
}
}
async fn handle_list(
owner: AccountId,
token_definition: Vec<AccountId>,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let ata_program_id = programs::ata().id();
for def in &token_definition {
let ata_id = associated_token_account_core::get_associated_token_account_id(
&ata_program_id,
&associated_token_account_core::compute_ata_seed(owner, *def),
);
let account = wallet_core.get_account_public(ata_id).await?;
if account == Account::default() {
println!("No ATA for definition {def}");
} else {
let holding = TokenHolding::try_from(&account.data)?;
match holding {
TokenHolding::Fungible { balance, .. } => {
println!("ATA {ata_id} (definition {def}): balance {balance}");
}
TokenHolding::NftMaster { .. }
| TokenHolding::NftPrintedCopy { .. } => {
println!("ATA {ata_id} (definition {def}): unsupported token type");
}
}
}
}
Ok(SubcommandReturnValue::Empty)
}
}
impl WalletSubcommand for AtaSubcommand {
async fn handle_subcommand(
self,
@ -78,173 +232,26 @@ impl WalletSubcommand for AtaSubcommand {
Self::Address {
owner,
token_definition,
} => {
let ata_program_id = programs::ata().id();
let ata_id = associated_token_account_core::get_associated_token_account_id(
&ata_program_id,
&associated_token_account_core::compute_ata_seed(owner, token_definition),
);
println!("{ata_id}");
Ok(SubcommandReturnValue::Empty)
}
} => Self::handle_address(owner, token_definition, wallet_core).await,
Self::Create {
owner,
token_definition,
} => {
let owner_resolved = owner.resolve(wallet_core.storage())?;
let definition_id = token_definition;
match owner_resolved {
AccountIdWithPrivacy::Public(owner_id) => {
let tx_hash = Ata(wallet_core)
.send_create(owner.into_public_identity(owner_id), definition_id)
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
AccountIdWithPrivacy::Private(owner_id) => {
let (tx_hash, secret) = Ata(wallet_core)
.send_create_private_owner(owner_id, definition_id)
.await?;
println!("Transaction hash is {tx_hash}");
let tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
if let LeeTransaction::PrivacyPreserving(tx) = tx {
wallet_core.decode_insert_privacy_preserving_transaction_results(
&tx,
&[Decode(secret, owner_id)],
)?;
}
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
}
}
} => Self::handle_create(owner, token_definition, wallet_core).await,
Self::Send {
from,
token_definition,
to,
amount,
} => {
let from_resolved = from.resolve(wallet_core.storage())?;
let definition_id = token_definition;
let to_id = to;
match from_resolved {
AccountIdWithPrivacy::Public(from_id) => {
let tx_hash = Ata(wallet_core)
.send_transfer(
from.into_public_identity(from_id),
definition_id,
to_id,
amount,
)
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
AccountIdWithPrivacy::Private(from_id) => {
let (tx_hash, secret) = Ata(wallet_core)
.send_transfer_private_owner(from_id, definition_id, to_id, amount)
.await?;
println!("Transaction hash is {tx_hash}");
let tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
if let LeeTransaction::PrivacyPreserving(tx) = tx {
wallet_core.decode_insert_privacy_preserving_transaction_results(
&tx,
&[Decode(secret, from_id)],
)?;
}
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
}
}
} => Self::handle_send(from, token_definition, to, amount, wallet_core).await,
Self::Burn {
holder,
token_definition,
amount,
} => {
let holder_resolved = holder.resolve(wallet_core.storage())?;
let definition_id = token_definition;
match holder_resolved {
AccountIdWithPrivacy::Public(holder_id) => {
let tx_hash = Ata(wallet_core)
.send_burn(
holder.into_public_identity(holder_id),
definition_id,
amount,
)
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
AccountIdWithPrivacy::Private(holder_id) => {
let (tx_hash, secret) = Ata(wallet_core)
.send_burn_private_owner(holder_id, definition_id, amount)
.await?;
println!("Transaction hash is {tx_hash}");
let tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
if let LeeTransaction::PrivacyPreserving(tx) = tx {
wallet_core.decode_insert_privacy_preserving_transaction_results(
&tx,
&[Decode(secret, holder_id)],
)?;
}
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
}
}
} => Self::handle_burn(holder, token_definition, amount, wallet_core).await,
Self::List {
owner,
token_definition,
} => {
let ata_program_id = programs::ata().id();
for def in &token_definition {
let ata_id = associated_token_account_core::get_associated_token_account_id(
&ata_program_id,
&associated_token_account_core::compute_ata_seed(owner, *def),
);
let account = wallet_core.get_account_public(ata_id).await?;
if account == Account::default() {
println!("No ATA for definition {def}");
} else {
let holding = TokenHolding::try_from(&account.data)?;
match holding {
TokenHolding::Fungible { balance, .. } => {
println!("ATA {ata_id} (definition {def}): balance {balance}");
}
TokenHolding::NftMaster { .. }
| TokenHolding::NftPrintedCopy { .. } => {
println!("ATA {ata_id} (definition {def}): unsupported token type");
}
}
}
}
Ok(SubcommandReturnValue::Empty)
}
} => Self::handle_list(owner, token_definition, wallet_core).await,
}
}
}

View File

@ -1,6 +1,5 @@
use anyhow::{Context as _, Result};
use anyhow::Result;
use clap::Subcommand;
use common::transaction::LeeTransaction;
use lee::AccountId;
use crate::{
@ -53,147 +52,153 @@ pub enum AuthTransferSubcommand {
},
}
impl AuthTransferSubcommand {
async fn handle_init(
account_id: CliAccountMention,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let resolved = account_id.resolve(wallet_core.storage())?;
match resolved {
AccountIdWithPrivacy::Public(pub_account_id) => {
let tx_hash = NativeTokenTransfer(wallet_core)
.register_account(account_id.into_public_identity(pub_account_id))
.await?;
wallet_core
.poll_and_finalize_public_transaction(tx_hash)
.await
}
AccountIdWithPrivacy::Private(account_id) => {
let (tx_hash, secret) = NativeTokenTransfer(wallet_core)
.register_account_private(account_id)
.await?;
wallet_core
.poll_and_finalize_pp_transaction(tx_hash, &[
Decode(secret, account_id),
])
.await
}
}
}
async fn handle_send(
from_account: CliAccountMention,
to_account: Option<CliAccountMention>,
to_npk: Option<String>,
to_vpk: Option<String>,
to_keys: Option<String>,
to_identifier: Option<u128>,
amount: u128,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
// Resolve --to-keys into --to-npk / --to-vpk equivalents.
let (to_npk, to_vpk) = if let Some(path) = to_keys {
let (npk_bytes, vpk_bytes) = crate::cli::read_keys_file(&path)?;
(Some(hex::encode(npk_bytes)), Some(hex::encode(vpk_bytes)))
} else {
(to_npk, to_vpk)
};
let from = from_account.resolve(wallet_core.storage())?;
let to = to_account
.as_ref()
.map(|m| m.resolve(wallet_core.storage()))
.transpose()?;
let underlying_subcommand = match (to, to_npk, to_vpk) {
(None, None, None) => {
anyhow::bail!(
"Provide either account account_id of receiver or their public keys"
);
}
(Some(_), Some(_), Some(_)) => {
anyhow::bail!(
"Provide only one variant: either account account_id of receiver or their public keys"
);
}
(_, Some(_), None) | (_, None, Some(_)) => {
anyhow::bail!("List of public keys is uncomplete");
}
(Some(to), None, None) => match (from, to) {
(AccountIdWithPrivacy::Public(from), AccountIdWithPrivacy::Public(to)) => {
let to_mention = to_account.expect("matched Some branch");
NativeTokenTransferProgramSubcommand::Public {
from: Some(from_account.into_public_identity(from)),
to: Some(to_mention.into_public_identity(to)),
amount,
}
}
(
AccountIdWithPrivacy::Private(from),
AccountIdWithPrivacy::Private(to),
) => NativeTokenTransferProgramSubcommand::Private(
NativeTokenTransferProgramSubcommandPrivate::PrivateOwned {
from,
to,
amount,
},
),
(AccountIdWithPrivacy::Private(from), AccountIdWithPrivacy::Public(to)) => {
NativeTokenTransferProgramSubcommand::Deshielded { from, to, amount }
}
(AccountIdWithPrivacy::Public(from), AccountIdWithPrivacy::Private(to)) => {
NativeTokenTransferProgramSubcommand::Shielded(
NativeTokenTransferProgramSubcommandShielded::ShieldedOwned {
from: Some(from_account.into_public_identity(from)),
to,
amount,
},
)
}
},
(None, Some(to_npk), Some(to_vpk)) => match from {
AccountIdWithPrivacy::Private(from) => {
NativeTokenTransferProgramSubcommand::Private(
NativeTokenTransferProgramSubcommandPrivate::PrivateForeign {
from,
to_npk,
to_vpk,
to_identifier,
amount,
},
)
}
AccountIdWithPrivacy::Public(from) => {
NativeTokenTransferProgramSubcommand::Shielded(
NativeTokenTransferProgramSubcommandShielded::ShieldedForeign {
from: Some(from_account.into_public_identity(from)),
to_npk,
to_vpk,
to_identifier,
amount,
},
)
}
},
};
underlying_subcommand.handle_subcommand(wallet_core).await
}
}
impl WalletSubcommand for AuthTransferSubcommand {
async fn handle_subcommand(
self,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
match self {
Self::Init { account_id } => {
let resolved = account_id.resolve(wallet_core.storage())?;
match resolved {
AccountIdWithPrivacy::Public(pub_account_id) => {
let tx_hash = NativeTokenTransfer(wallet_core)
.register_account(account_id.into_public_identity(pub_account_id))
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
}
AccountIdWithPrivacy::Private(account_id) => {
let (tx_hash, secret) = NativeTokenTransfer(wallet_core)
.register_account_private(account_id)
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
if let LeeTransaction::PrivacyPreserving(tx) = transfer_tx {
let acc_decode_data = vec![Decode(secret, account_id)];
wallet_core.decode_insert_privacy_preserving_transaction_results(
&tx,
&acc_decode_data,
)?;
}
wallet_core.store_persistent_data()?;
}
}
Ok(SubcommandReturnValue::Empty)
}
Self::Init { account_id } => Self::handle_init(account_id, wallet_core).await,
Self::Send {
from: from_account,
to: to_account,
from,
to,
to_npk,
to_vpk,
to_keys,
to_identifier,
amount,
} => {
// Resolve --to-keys into --to-npk / --to-vpk equivalents.
let (to_npk, to_vpk) = if let Some(path) = to_keys {
let (npk_bytes, vpk_bytes) = crate::cli::read_keys_file(&path)?;
(Some(hex::encode(npk_bytes)), Some(hex::encode(vpk_bytes)))
} else {
(to_npk, to_vpk)
};
let from = from_account.resolve(wallet_core.storage())?;
let to = to_account
.as_ref()
.map(|m| m.resolve(wallet_core.storage()))
.transpose()?;
let underlying_subcommand = match (to, to_npk, to_vpk) {
(None, None, None) => {
anyhow::bail!(
"Provide either account account_id of receiver or their public keys"
);
}
(Some(_), Some(_), Some(_)) => {
anyhow::bail!(
"Provide only one variant: either account account_id of receiver or their public keys"
);
}
(_, Some(_), None) | (_, None, Some(_)) => {
anyhow::bail!("List of public keys is uncomplete");
}
(Some(to), None, None) => match (from, to) {
(AccountIdWithPrivacy::Public(from), AccountIdWithPrivacy::Public(to)) => {
let to_mention = to_account.expect("matched Some branch");
NativeTokenTransferProgramSubcommand::Public {
from: Some(from_account.into_public_identity(from)),
to: Some(to_mention.into_public_identity(to)),
amount,
}
}
(
AccountIdWithPrivacy::Private(from),
AccountIdWithPrivacy::Private(to),
) => NativeTokenTransferProgramSubcommand::Private(
NativeTokenTransferProgramSubcommandPrivate::PrivateOwned {
from,
to,
amount,
},
),
(AccountIdWithPrivacy::Private(from), AccountIdWithPrivacy::Public(to)) => {
NativeTokenTransferProgramSubcommand::Deshielded { from, to, amount }
}
(AccountIdWithPrivacy::Public(from), AccountIdWithPrivacy::Private(to)) => {
NativeTokenTransferProgramSubcommand::Shielded(
NativeTokenTransferProgramSubcommandShielded::ShieldedOwned {
from: Some(from_account.into_public_identity(from)),
to,
amount,
},
)
}
},
(None, Some(to_npk), Some(to_vpk)) => match from {
AccountIdWithPrivacy::Private(from) => {
NativeTokenTransferProgramSubcommand::Private(
NativeTokenTransferProgramSubcommandPrivate::PrivateForeign {
from,
to_npk,
to_vpk,
to_identifier,
amount,
},
)
}
AccountIdWithPrivacy::Public(from) => {
NativeTokenTransferProgramSubcommand::Shielded(
NativeTokenTransferProgramSubcommandShielded::ShieldedForeign {
from: Some(from_account.into_public_identity(from)),
to_npk,
to_vpk,
to_identifier,
amount,
},
)
}
},
};
underlying_subcommand.handle_subcommand(wallet_core).await
Self::handle_send(from, to, to_npk, to_vpk, to_keys, to_identifier, amount, wallet_core)
.await
}
}
}
@ -315,6 +320,53 @@ pub enum NativeTokenTransferProgramSubcommandPrivate {
},
}
impl NativeTokenTransferProgramSubcommandPrivate {
async fn handle_private_owned(
from: AccountId,
to: AccountId,
amount: u128,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let (tx_hash, [secret_from, secret_to]) = NativeTokenTransfer(wallet_core)
.send_private_transfer_to_owned_account(from, to, amount)
.await?;
wallet_core
.poll_and_finalize_pp_transaction(tx_hash, &[
Decode(secret_from, from),
Decode(secret_to, to),
])
.await
}
async fn handle_private_foreign(
from: AccountId,
to_npk: String,
to_vpk: String,
to_identifier: Option<u128>,
amount: u128,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let (to_npk, to_vpk) = crate::cli::decode_npk_vpk(&to_npk, &to_vpk)?;
let (tx_hash, [secret_from, _]) = NativeTokenTransfer(wallet_core)
.send_private_transfer_to_outer_account(
from,
to_npk,
to_vpk,
to_identifier.unwrap_or_else(rand::random),
amount,
)
.await?;
wallet_core
.poll_and_finalize_pp_transaction(tx_hash, &[
Decode(secret_from, from),
])
.await
}
}
impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate {
async fn handle_subcommand(
self,
@ -322,26 +374,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate {
) -> Result<SubcommandReturnValue> {
match self {
Self::PrivateOwned { from, to, amount } => {
let (tx_hash, [secret_from, secret_to]) = NativeTokenTransfer(wallet_core)
.send_private_transfer_to_owned_account(from, to, amount)
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
if let LeeTransaction::PrivacyPreserving(tx) = transfer_tx {
let acc_decode_data = vec![Decode(secret_from, from), Decode(secret_to, to)];
wallet_core.decode_insert_privacy_preserving_transaction_results(
&tx,
&acc_decode_data,
)?;
}
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash })
Self::handle_private_owned(from, to, amount, wallet_core).await
}
Self::PrivateForeign {
from,
@ -350,47 +383,63 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate {
to_identifier,
amount,
} => {
let to_npk_res = hex::decode(to_npk)?;
let mut to_npk = [0; 32];
to_npk.copy_from_slice(&to_npk_res);
let to_npk = lee_core::NullifierPublicKey(to_npk);
let to_vpk_res = hex::decode(&to_vpk)
.context("wallet::cli::programs::native_token_transfer: to_vpk must be a valid hex string")?;
let to_vpk = lee_core::encryption::ViewingPublicKey::from_bytes(to_vpk_res)
.map_err(|e| anyhow::anyhow!("{e}"))?;
let (tx_hash, [secret_from, _]) = NativeTokenTransfer(wallet_core)
.send_private_transfer_to_outer_account(
from,
to_npk,
to_vpk,
to_identifier.unwrap_or_else(rand::random),
amount,
)
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
if let LeeTransaction::PrivacyPreserving(tx) = transfer_tx {
let acc_decode_data = vec![Decode(secret_from, from)];
wallet_core.decode_insert_privacy_preserving_transaction_results(
&tx,
&acc_decode_data,
)?;
}
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash })
Self::handle_private_foreign(from, to_npk, to_vpk, to_identifier, amount, wallet_core)
.await
}
}
}
}
impl NativeTokenTransferProgramSubcommandShielded {
async fn handle_shielded_owned(
from: Option<AccountIdentity>,
to: AccountId,
amount: u128,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let (tx_hash, secret) = NativeTokenTransfer(wallet_core)
.send_shielded_transfer(
from.expect("from set during Send dispatch"),
to,
amount,
)
.await?;
wallet_core
.poll_and_finalize_pp_transaction(tx_hash, &[
Decode(secret, to),
])
.await
}
async fn handle_shielded_foreign(
from: Option<AccountIdentity>,
to_npk: String,
to_vpk: String,
to_identifier: Option<u128>,
amount: u128,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let (to_npk, to_vpk) = crate::cli::decode_npk_vpk(&to_npk, &to_vpk)?;
let (tx_hash, _) = NativeTokenTransfer(wallet_core)
.send_shielded_transfer_to_outer_account(
from.expect("from set during Send dispatch"),
to_npk,
to_vpk,
to_identifier.unwrap_or_else(rand::random),
amount,
)
.await?;
println!("Transaction hash is {tx_hash}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash })
}
}
impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded {
async fn handle_subcommand(
self,
@ -398,30 +447,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded {
) -> Result<SubcommandReturnValue> {
match self {
Self::ShieldedOwned { from, to, amount } => {
let (tx_hash, secret) = NativeTokenTransfer(wallet_core)
.send_shielded_transfer(
from.expect("from set during Send dispatch"),
to,
amount,
)
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
if let LeeTransaction::PrivacyPreserving(tx) = transfer_tx {
let acc_decode_data = vec![Decode(secret, to)];
wallet_core.decode_insert_privacy_preserving_transaction_results(
&tx,
&acc_decode_data,
)?;
}
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash })
Self::handle_shielded_owned(from, to, amount, wallet_core).await
}
Self::ShieldedForeign {
from,
@ -430,36 +456,51 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded {
to_identifier,
amount,
} => {
let to_npk_res = hex::decode(to_npk)?;
let mut to_npk = [0; 32];
to_npk.copy_from_slice(&to_npk_res);
let to_npk = lee_core::NullifierPublicKey(to_npk);
let to_vpk_res = hex::decode(&to_vpk)
.context("wallet::cli::programs::native_token_transfer: to_vpk must be a valid hex string")?;
let to_vpk = lee_core::encryption::ViewingPublicKey::from_bytes(to_vpk_res)
.map_err(|e| anyhow::anyhow!("{e}"))?;
let (tx_hash, _) = NativeTokenTransfer(wallet_core)
.send_shielded_transfer_to_outer_account(
from.expect("from set during Send dispatch"),
to_npk,
to_vpk,
to_identifier.unwrap_or_else(rand::random),
amount,
)
.await?;
println!("Transaction hash is {tx_hash}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash })
Self::handle_shielded_foreign(from, to_npk, to_vpk, to_identifier, amount, wallet_core)
.await
}
}
}
}
impl NativeTokenTransferProgramSubcommand {
async fn handle_deshielded(
from: AccountId,
to: AccountId,
amount: u128,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let (tx_hash, secret) = NativeTokenTransfer(wallet_core)
.send_deshielded_transfer(from, to, amount)
.await?;
wallet_core
.poll_and_finalize_pp_transaction(tx_hash, &[
Decode(secret, from),
])
.await
}
async fn handle_public(
from: Option<AccountIdentity>,
to: Option<AccountIdentity>,
amount: u128,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
let tx_hash = NativeTokenTransfer(wallet_core)
.send_public_transfer(
from.expect("from is set during Send dispatch"),
to.expect("to is set during Send dispatch"),
amount,
)
.await?;
wallet_core
.poll_and_finalize_public_transaction(tx_hash)
.await
}
}
impl WalletSubcommand for NativeTokenTransferProgramSubcommand {
async fn handle_subcommand(
self,
@ -473,45 +514,10 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommand {
shielded_subcommand.handle_subcommand(wallet_core).await
}
Self::Deshielded { from, to, amount } => {
let (tx_hash, secret) = NativeTokenTransfer(wallet_core)
.send_deshielded_transfer(from, to, amount)
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
if let LeeTransaction::PrivacyPreserving(tx) = transfer_tx {
let acc_decode_data = vec![Decode(secret, from)];
wallet_core.decode_insert_privacy_preserving_transaction_results(
&tx,
&acc_decode_data,
)?;
}
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash })
Self::handle_deshielded(from, to, amount, wallet_core).await
}
Self::Public { from, to, amount } => {
let tx_hash = NativeTokenTransfer(wallet_core)
.send_public_transfer(
from.expect("from is set during Send dispatch"),
to.expect("to is set during Send dispatch"),
amount,
)
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
Self::handle_public(from, to, amount, wallet_core).await
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -544,6 +544,32 @@ impl WalletCore {
Ok(())
}
pub(crate) async fn poll_and_finalize_public_transaction(
&mut self,
tx_hash: HashType,
) -> Result<cli::SubcommandReturnValue> {
println!("Transaction hash is {tx_hash}");
let transfer_tx = self.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
self.store_persistent_data()?;
Ok(cli::SubcommandReturnValue::Empty)
}
/// Pass an empty slice when the recipient is foreign and no accounts need decoding.
pub(crate) async fn poll_and_finalize_pp_transaction(
&mut self,
tx_hash: HashType,
acc_decode_data: &[AccDecodeData],
) -> Result<cli::SubcommandReturnValue> {
println!("Transaction hash is {tx_hash}");
let transfer_tx = self.poll_native_token_transfer(tx_hash).await?;
if let common::transaction::LeeTransaction::PrivacyPreserving(tx) = transfer_tx {
self.decode_insert_privacy_preserving_transaction_results(&tx, acc_decode_data)?;
}
self.store_persistent_data()?;
Ok(cli::SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash })
}
pub async fn send_privacy_preserving_tx(
&self,
accounts: Vec<AccountIdentity>,