Skip to content

Commit f3deed8

Browse files
committed
Refactor(handlers): Define app context states
- Define Init state for when an execution does not need either the wallet or client for execution - Define the Offline wallet operations state for app context when an execution envt needs a wallet - Define the online wallet operations state for appcontext when an execution needs both the wallet and client - make app context generic over the state - make the app command and async app command generic over the execution context - deleted shorten fn as it was applicable to the pretty flag - removed tests for pretty flag
1 parent 5ced5aa commit f3deed8

6 files changed

Lines changed: 161 additions & 100 deletions

File tree

src/client.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ pub(crate) enum BlockchainClient {
7979
impl BlockchainClient {
8080
pub async fn broadcast(&self, tx: Transaction) -> Result<Txid, Error> {
8181
match self {
82-
#[cfg(feature = "electrum")]
82+
// #[cfg(feature = "electrum")]
8383
Self::Electrum { client, .. } => client
8484
.transaction_broadcast(&tx)
8585
.map_err(|e| Error::Generic(e.to_string())),

src/handlers/mod.rs

Lines changed: 67 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -17,66 +17,93 @@ use std::path::PathBuf;
1717
use crate::{error::BDKCliError as Error, utils::output::FormatOutput};
1818
use bdk_wallet::{Wallet, bitcoin::Network};
1919

20-
/// The shared environment for all commands
21-
pub struct AppContext<'a> {
20+
// The state for no wallet, no client.
21+
pub struct Init;
22+
23+
/// Offline wallet operations.
24+
/// Requires only a wallet.
25+
pub struct OfflineOperations<'a> {
26+
pub wallet: &'a mut Wallet,
27+
}
28+
29+
#[cfg(any(
30+
feature = "electrum",
31+
feature = "esplora",
32+
feature = "rpc",
33+
feature = "cbf"
34+
))]
35+
/// Online wallet operations.
36+
/// Requires a wallet and a client.
37+
pub struct OnlineOperations<'a> {
38+
pub wallet: &'a mut Wallet,
39+
pub client: &'a BlockchainClient,
40+
}
41+
42+
/// The generic context
43+
pub struct AppContext<S> {
2244
pub network: Network,
2345
pub datadir: PathBuf,
24-
pub wallet: Option<&'a mut Wallet>,
25-
#[cfg(any(
26-
feature = "electrum",
27-
feature = "esplora",
28-
feature = "rpc",
29-
feature = "cbf"
30-
))]
31-
pub client: Option<&'a BlockchainClient>,
46+
pub state: S,
3247
}
3348

34-
impl<'a> AppContext<'a> {
49+
/// Construct for a specific state.
50+
impl AppContext<Init> {
3551
pub fn new(network: Network, datadir: PathBuf) -> Self {
3652
Self {
3753
network,
3854
datadir,
39-
wallet: None,
40-
#[cfg(any(
41-
feature = "electrum",
42-
feature = "esplora",
43-
feature = "rpc",
44-
feature = "cbf"
45-
))]
46-
client: None,
55+
state: Init,
4756
}
4857
}
58+
}
4959

50-
/// Attach a mutable wallet reference to the context.
51-
pub fn with_wallet(mut self, wallet: &'a mut Wallet) -> Self {
52-
self.wallet = Some(wallet);
53-
self
60+
impl<'a> AppContext<OfflineOperations<'a>> {
61+
pub fn new_offline_wallet(network: Network, datadir: PathBuf, wallet: &'a mut Wallet) -> Self {
62+
Self {
63+
network,
64+
datadir,
65+
state: OfflineOperations { wallet },
66+
}
5467
}
68+
}
5569

56-
/// Attach a client reference to the context.
57-
#[cfg(any(
58-
feature = "electrum",
59-
feature = "esplora",
60-
feature = "rpc",
61-
feature = "cbf"
62-
))]
63-
pub fn with_client(mut self, client: &'a BlockchainClient) -> Self {
64-
self.client = Some(client);
65-
self
70+
#[cfg(any(
71+
feature = "electrum",
72+
feature = "esplora",
73+
feature = "rpc",
74+
feature = "cbf"
75+
))]
76+
impl<'a> AppContext<OnlineOperations<'a>> {
77+
pub fn new_online_wallet(
78+
network: Network,
79+
datadir: PathBuf,
80+
wallet: &'a mut Wallet,
81+
client: &'a BlockchainClient,
82+
) -> Self {
83+
Self {
84+
network,
85+
datadir,
86+
state: OnlineOperations { wallet, client },
87+
}
6688
}
6789
}
6890

69-
pub trait AsyncCommand {
91+
pub trait AppCommand<C> {
7092
type Output: FormatOutput;
71-
async fn execute(&self, ctx: &mut AppContext<'_>) -> Result<Self::Output, Error>;
93+
94+
fn execute(&self, ctx: &mut C) -> Result<Self::Output, Error>;
7295
}
7396

74-
/// The command trait
75-
pub trait AppCommand {
97+
#[cfg(any(
98+
feature = "electrum",
99+
feature = "esplora",
100+
feature = "rpc",
101+
feature = "cbf"
102+
))]
103+
pub trait AsyncAppCommand<C> {
76104
type Output: FormatOutput;
77-
78-
/// The execution logic
79-
fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error>;
105+
106+
async fn execute(&self, ctx: &mut C) -> Result<Self::Output, Error>;
80107
}
81108

82109
// context for online and online

src/main.rs

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,8 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> {
110110

111111
let client = new_blockchain_client(&wallet_opts, &wallet, database_path)?;
112112

113-
let mut ctx = AppContext::new(network, home_dir)
114-
.with_wallet(&mut wallet)
115-
.with_client(&client);
113+
let mut ctx =
114+
AppContext::new_online_wallet(network, home_dir, &mut wallet, &client);
116115

117116
online_cmd.execute(&mut ctx).await?;
118117
}
@@ -142,7 +141,7 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> {
142141

143142
let mut wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?;
144143

145-
let mut ctx = AppContext::new(network, home_dir).with_wallet(&mut wallet);
144+
let mut ctx = AppContext::new_offline_wallet(network, home_dir, &mut wallet);
146145

147146
offline_cmd.execute(&mut ctx)?;
148147
}
@@ -169,7 +168,86 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> {
169168
let mut ctx = AppContext::new(cli_opts.network, home_dir);
170169
cmd.execute(&mut ctx)?.write_out(std::io::stdout())?;
171170
}
172-
CliSubCommand::Repl { wallet: _ } => todo!(),
171+
CliSubCommand::Repl {
172+
wallet: wallet_name,
173+
} => {
174+
#[cfg(feature = "repl")]
175+
{
176+
let (wallet_opts, network) = load_wallet_config(&home_dir, &wallet_name)?;
177+
let database_path = prepare_wallet_db_dir(&home_dir, &wallet_name)?;
178+
179+
#[cfg(any(feature = "sqlite", feature = "redb"))]
180+
let mut persister: Persister = match &wallet_opts.database_type {
181+
#[cfg(feature = "sqlite")]
182+
crate::persister::DatabaseType::Sqlite => {
183+
let db_file = database_path.join("wallet.sqlite");
184+
let connection = bdk_wallet::rusqlite::Connection::open(db_file)?;
185+
Persister::Connection(connection)
186+
}
187+
#[cfg(feature = "redb")]
188+
crate::persister::DatabaseType::Redb => {
189+
use crate::persister::Persister;
190+
let db = std::sync::Arc::new(bdk_redb::redb::Database::create(
191+
home_dir.join("wallet.redb"),
192+
)?);
193+
let store = RedbStore::new(db, wallet_name.clone())?;
194+
Persister::RedbStore(store)
195+
}
196+
};
197+
198+
let mut wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?;
199+
200+
#[cfg(any(
201+
feature = "electrum",
202+
feature = "esplora",
203+
feature = "rpc",
204+
feature = "cbf"
205+
))]
206+
let client = Some(new_blockchain_client(&wallet_opts, &wallet, database_path)?);
207+
208+
println!(
209+
"Entering REPL mode for wallet '{}'. Type 'exit' to quit.",
210+
wallet_name
211+
);
212+
213+
loop {
214+
let line = crate::handlers::repl::readline()?;
215+
if line.trim().is_empty() {
216+
continue;
217+
}
218+
219+
// Pass it to our newly refactored respond function
220+
let should_exit = crate::handlers::repl::respond(
221+
network,
222+
&mut wallet,
223+
#[cfg(any(
224+
feature = "electrum",
225+
feature = "esplora",
226+
feature = "rpc",
227+
feature = "cbf"
228+
))]
229+
client.as_ref(),
230+
&line,
231+
home_dir.clone(),
232+
&cli_opts,
233+
)
234+
.await
235+
.map_err(Error::Generic)?;
236+
237+
// Break the loop if the user typed `exit`
238+
if should_exit {
239+
break;
240+
}
241+
}
242+
}
243+
244+
#[cfg(not(feature = "repl"))]
245+
{
246+
return Err(Error::Generic(
247+
"The 'repl' feature is not enabled in this build.".into(),
248+
));
249+
}
250+
}
173251
CliSubCommand::Completions { shell } => {
174252
shell;
175253
}

src/utils/common.rs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ use bdk_wallet::bitcoin::{Address, Network, OutPoint, ScriptBuf};
1515
use bdk_sp::encoding::SilentPaymentCode;
1616

1717
use std::{
18-
fmt::Display,
1918
path::{Path, PathBuf},
2019
str::FromStr,
2120
};
@@ -50,18 +49,6 @@ pub(crate) fn is_final(psbt: &Psbt) -> Result<(), Error> {
5049
Ok(())
5150
}
5251

53-
pub(crate) fn shorten(displayable: impl Display, start: u8, end: u8) -> String {
54-
let displayable = displayable.to_string();
55-
56-
if displayable.len() <= (start + end) as usize {
57-
return displayable;
58-
}
59-
60-
let start_str: &str = &displayable[0..start as usize];
61-
let end_str: &str = &displayable[displayable.len() - end as usize..];
62-
format!("{start_str}...{end_str}")
63-
}
64-
6552
/// Parse the recipient (Address,Amount) argument from cli input.
6653
pub(crate) fn parse_recipient(s: &str) -> Result<(ScriptBuf, u64), String> {
6754
let parts: Vec<_> = s.split(':').collect();

src/utils/types.rs

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
use std::collections::HashMap;
22

33
use crate::config::WalletConfigInner;
4-
use crate::utils::shorten;
54
use bdk_wallet::Balance;
65
use bdk_wallet::bitcoin::{
7-
Address, Network, Psbt, Transaction, base64::Engine, consensus::encode::serialize_hex,
6+
Network, Psbt, Transaction, base64::Engine, consensus::encode::serialize_hex,
87
};
9-
use bdk_wallet::{AddressInfo, LocalOutput, chain::ChainPosition};
8+
use bdk_wallet::{AddressInfo, LocalOutput};
109
use serde::Serialize;
1110
use serde_json::json;
1211

@@ -58,24 +57,7 @@ pub struct UnspentDetails {
5857
}
5958

6059
impl UnspentDetails {
61-
pub fn from_local_output(utxo: &LocalOutput, network: Network) -> Self {
62-
let height = utxo.chain_position.confirmation_height_upper_bound();
63-
let height_display = height
64-
.map(|h| h.to_string())
65-
.unwrap_or_else(|| "Pending".to_string());
66-
67-
let (_, block_hash_display) = match &utxo.chain_position {
68-
ChainPosition::Confirmed { anchor, .. } => {
69-
let hash = anchor.block_id.hash.to_string();
70-
(Some(hash.clone()), shorten(&hash, 8, 8))
71-
}
72-
ChainPosition::Unconfirmed { .. } => (None, "Unconfirmed".to_string()),
73-
};
74-
75-
let address = Address::from_script(&utxo.txout.script_pubkey, network)
76-
.map(|a| a.to_string())
77-
.unwrap_or_else(|_| "Unknown Script".to_string());
78-
60+
pub fn from_local_output(utxo: &LocalOutput, _network: Network) -> Self {
7961
let outpoint_str = utxo.outpoint.to_string();
8062

8163
Self {
@@ -133,6 +115,7 @@ pub struct KeychainPair<T> {
133115
pub internal: T,
134116
}
135117

118+
#[cfg(feature = "bip322")]
136119
#[derive(Serialize, Debug, Default)]
137120
pub struct MessageResult {
138121
#[serde(skip_serializing_if = "Option::is_none")]
@@ -158,6 +141,12 @@ impl StatusResult {
158141
}
159142
}
160143

144+
#[cfg(any(
145+
feature = "electrum",
146+
feature = "esplora",
147+
feature = "cbf",
148+
feature = "rpc"
149+
))]
161150
#[derive(Serialize, Debug)]
162151
pub struct TransactionResult {
163152
pub txid: String,

tests/cli_flags.rs

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,3 @@ fn test_without_pretty_flag() {
2424
let stdout = String::from_utf8_lossy(&output.stdout);
2525
assert!(serde_json::from_str::<serde_json::Value>(&stdout).is_ok());
2626
}
27-
28-
#[test]
29-
fn test_pretty_flag_before_subcommand() {
30-
let output = Command::new("cargo")
31-
.args("run -- --pretty key generate".split_whitespace())
32-
.output()
33-
.unwrap();
34-
35-
assert!(output.status.success());
36-
}
37-
38-
#[test]
39-
fn test_pretty_flag_after_subcommand() {
40-
let output = Command::new("cargo")
41-
.args("run -- key generate --pretty".split_whitespace())
42-
.output()
43-
.unwrap();
44-
45-
assert!(output.status.success());
46-
}

0 commit comments

Comments
 (0)