mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-06-29 18:39:30 +00:00
add helper functions
This commit is contained in:
parent
e37876a640
commit
c8c9ced421
62
lez/explorer_service/src/components/account_nonce_list.rs
Normal file
62
lez/explorer_service/src/components/account_nonce_list.rs
Normal 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>
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
|
||||
97
lez/explorer_service/src/components/search_results.rs
Normal file
97
lez/explorer_service/src/components/search_results.rs
Normal 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>
|
||||
}
|
||||
}
|
||||
165
lez/explorer_service/src/components/transaction_details.rs
Normal file
165
lez/explorer_service/src/components/transaction_details.rs
Normal 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>
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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! {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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.")?;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()))
|
||||
})
|
||||
|
||||
@ -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::*;
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
@ -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>,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user