1use std::{
5 cell::RefCell,
6 path::PathBuf,
7 str::{self, FromStr},
8};
9
10use crate::cli::humantoken::TokenAmountPretty as _;
11use crate::key_management::{Key, KeyInfo};
12use crate::{
13 ENCRYPTED_KEYSTORE_NAME,
14 cli::humantoken,
15 message::SignedMessage,
16 rpc::{
17 mpool::{MpoolGetNonce, MpoolPush, MpoolPushMessage},
18 types::ApiTipsetKey,
19 },
20 shim::address::Address,
21};
22use crate::{KeyStore, lotus_json::LotusJson};
23use crate::{
24 KeyStoreConfig,
25 shim::{
26 address::StrictAddress,
27 crypto::{Signature, SignatureType},
28 econ::TokenAmount,
29 message::{METHOD_SEND, Message},
30 },
31};
32use crate::{
33 lotus_json::HasLotusJson as _,
34 rpc::{self, prelude::*},
35};
36use anyhow::{Context as _, bail};
37use base64::{Engine, prelude::BASE64_STANDARD};
38use clap::{Subcommand, arg};
39use dialoguer::{Password, console::Term, theme::ColorfulTheme};
40use directories::ProjectDirs;
41use num::Zero as _;
42
43struct WalletBackend {
48 pub remote: rpc::Client,
49 pub local: Option<KeyStore>,
50}
51
52impl WalletBackend {
53 fn new_remote(client: rpc::Client) -> Self {
54 WalletBackend {
55 remote: client,
56 local: None,
57 }
58 }
59
60 fn new_local(client: rpc::Client, want_encryption: bool) -> anyhow::Result<Self> {
61 let Some(dir) = ProjectDirs::from("com", "ChainSafe", "Forest-Wallet") else {
62 bail!("Failed to find wallet directory");
63 };
64
65 let wallet_dir = dir.data_dir().to_path_buf();
66
67 let is_encrypted = wallet_dir.join(ENCRYPTED_KEYSTORE_NAME).exists();
68
69 let keystore = if is_encrypted || want_encryption {
72 input_password_to_load_encrypted_keystore(wallet_dir)?
73 } else {
74 KeyStore::new(KeyStoreConfig::Persistent(wallet_dir.to_path_buf()))?
75 };
76
77 Ok(WalletBackend {
78 remote: client,
79 local: Some(keystore),
80 })
81 }
82
83 async fn list_addrs(&self) -> anyhow::Result<Vec<Address>> {
84 if let Some(keystore) = &self.local {
85 Ok(crate::key_management::list_addrs(keystore)?)
86 } else {
87 Ok(WalletList::call(&self.remote, ()).await?)
88 }
89 }
90
91 async fn wallet_export(&self, address: Address) -> anyhow::Result<KeyInfo> {
92 if let Some(keystore) = &self.local {
93 Ok(crate::key_management::export_key_info(&address, keystore)?)
94 } else {
95 Ok(WalletExport::call(&self.remote, (address,)).await?)
96 }
97 }
98
99 async fn wallet_import(&mut self, key_info: KeyInfo) -> anyhow::Result<String> {
100 if let Some(keystore) = &mut self.local {
101 let key = Key::try_from(key_info)?;
102 let addr = format!("wallet-{}", key.address);
103
104 keystore.put(&addr, key.key_info)?;
105 Ok(key.address.to_string())
106 } else {
107 Ok(WalletImport::call(&self.remote, (key_info,))
108 .await?
109 .to_string())
110 }
111 }
112
113 async fn wallet_has(&self, address: Address) -> anyhow::Result<bool> {
114 if let Some(keystore) = &self.local {
115 Ok(crate::key_management::find_key(&address, keystore).is_ok())
116 } else {
117 Ok(WalletHas::call(&self.remote, (address,)).await?)
118 }
119 }
120
121 async fn wallet_delete(&mut self, address: Address) -> anyhow::Result<()> {
122 if let Some(keystore) = &mut self.local {
123 Ok(crate::key_management::remove_key(&address, keystore)?)
124 } else {
125 Ok(WalletDelete::call(&self.remote, (address,)).await?)
126 }
127 }
128
129 async fn wallet_new(&mut self, signature_type: SignatureType) -> anyhow::Result<String> {
130 if let Some(keystore) = &mut self.local {
131 let key = crate::key_management::generate_key(signature_type)?;
132
133 let addr = format!("wallet-{}", key.address);
134 keystore.put(&addr, key.key_info.clone())?;
135 let value = keystore.get("default");
136 if value.is_err() {
137 keystore.put("default", key.key_info)?
138 }
139
140 Ok(key.address.to_string())
141 } else {
142 Ok(WalletNew::call(&self.remote, (signature_type,))
143 .await?
144 .to_string())
145 }
146 }
147
148 async fn wallet_default_address(&self) -> anyhow::Result<Option<String>> {
149 if let Some(keystore) = &self.local {
150 Ok(crate::key_management::get_default(keystore)?.map(|s| s.to_string()))
151 } else {
152 Ok(WalletDefaultAddress::call(&self.remote, ())
153 .await?
154 .map(|it| it.to_string()))
155 }
156 }
157
158 async fn wallet_set_default(&mut self, address: Address) -> anyhow::Result<()> {
159 if let Some(keystore) = &mut self.local {
160 let addr_string = format!("wallet-{address}");
161 let key_info = keystore.get(&addr_string)?;
162 keystore.remove("default")?; keystore.put("default", key_info)?;
164 Ok(())
165 } else {
166 Ok(WalletSetDefault::call(&self.remote, (address,)).await?)
167 }
168 }
169
170 async fn wallet_sign(&self, address: Address, message: String) -> anyhow::Result<Signature> {
171 if let Some(keystore) = &self.local {
172 let key = crate::key_management::find_key(&address, keystore)?;
173
174 Ok(crate::key_management::sign(
175 *key.key_info.key_type(),
176 key.key_info.private_key(),
177 &BASE64_STANDARD.decode(message)?,
178 )?)
179 } else {
180 Ok(WalletSign::call(&self.remote, (address, message.into_bytes())).await?)
181 }
182 }
183
184 async fn wallet_verify(
185 &self,
186 address: Address,
187 msg: Vec<u8>,
188 signature: Signature,
189 ) -> anyhow::Result<bool> {
190 if self.local.is_some() {
191 Ok(signature.verify(&msg, &address).is_ok())
192 } else {
193 Ok(WalletVerify::call(&self.remote, (address, msg, signature)).await?)
195 }
196 }
197}
198
199#[derive(Debug, Subcommand)]
200pub enum WalletCommands {
201 New {
203 #[arg(default_value = "secp256k1")]
205 signature_type: SignatureType,
206 },
207 Balance {
209 address: String,
211 #[arg(long, alias = "exact-balance")]
215 no_round: bool,
216 #[arg(long, alias = "fixed-unit")]
219 no_abbrev: bool,
220 },
221 Default,
223 Export {
225 address: String,
227 },
228 Has {
230 key: String,
232 },
233 Import {
235 path: Option<String>,
237 },
238 List {
240 #[arg(long, alias = "exact-balance")]
244 no_round: bool,
245 #[arg(long, alias = "fixed-unit")]
248 no_abbrev: bool,
249 },
250 SetDefault {
252 key: String,
254 },
255 Sign {
257 #[arg(short)]
259 message: String,
260 #[arg(short)]
262 address: String,
263 },
264 ValidateAddress {
266 address: String,
268 },
269 Verify {
272 #[arg(short)]
274 address: String,
275 #[arg(short)]
277 message: String,
278 #[arg(short)]
280 signature: String,
281 },
282 Delete {
284 address: String,
286 },
287 Send {
289 #[arg(long)]
292 from: Option<String>,
293 target_address: String,
294 #[arg(value_parser = humantoken::parse)]
295 amount: TokenAmount,
296 #[arg(long, value_parser = humantoken::parse, default_value_t = TokenAmount::zero())]
297 gas_feecap: TokenAmount,
298 #[arg(long, default_value_t = 0)]
300 gas_limit: i64,
301 #[arg(long, value_parser = humantoken::parse, default_value_t = TokenAmount::zero())]
302 gas_premium: TokenAmount,
303 },
304}
305impl WalletCommands {
306 pub async fn run(
307 self,
308 client: rpc::Client,
309 remote_wallet: bool,
310 encrypt: bool,
311 ) -> anyhow::Result<()> {
312 let mut backend = if remote_wallet {
313 WalletBackend::new_remote(client)
314 } else {
315 WalletBackend::new_local(client, encrypt)?
316 };
317 match self {
318 Self::New { signature_type } => {
319 let addr: String = backend.wallet_new(signature_type).await?;
320 println!("{addr}");
321 Ok(())
322 }
323 Self::Balance {
324 address,
325 no_round,
326 no_abbrev,
327 } => {
328 let StrictAddress(address) = StrictAddress::from_str(&address)
329 .with_context(|| format!("Invalid address: {address}"))?;
330 let balance = WalletBalance::call(&backend.remote, (address,)).await?;
331 println!("{}", format_balance(&balance, no_round, no_abbrev));
332 Ok(())
333 }
334 Self::Default => {
335 let default_addr = backend
336 .wallet_default_address()
337 .await?
338 .context("No default wallet address set")?;
339 println!("{default_addr}");
340 Ok(())
341 }
342 Self::Export {
343 address: address_string,
344 } => {
345 let StrictAddress(address) = StrictAddress::from_str(&address_string)
346 .with_context(|| format!("Invalid address: {address_string}"))?;
347 let key_info = backend.wallet_export(address).await?;
348 let encoded_key = key_info.into_lotus_json_string()?;
349 println!("{}", hex::encode(encoded_key));
350 Ok(())
351 }
352 Self::Has { key } => {
353 let StrictAddress(address) = StrictAddress::from_str(&key)
354 .with_context(|| format!("Invalid address: {key}"))?;
355
356 println!("{response}", response = backend.wallet_has(address).await?);
357 Ok(())
358 }
359 Self::Delete { address } => {
360 let StrictAddress(address) = StrictAddress::from_str(&address)
361 .with_context(|| format!("Invalid address: {address}"))?;
362
363 backend.wallet_delete(address).await?;
364 println!("deleted {address}.");
365 Ok(())
366 }
367 Self::Import { path } => {
368 let key = match path {
369 Some(path) => std::fs::read_to_string(path)?,
370 _ => {
371 let term = Term::stderr();
372 if term.is_term() {
373 tokio::task::spawn_blocking(|| {
374 Password::with_theme(&ColorfulTheme::default())
375 .allow_empty_password(true)
376 .with_prompt("Enter the private key")
377 .interact()
378 })
379 .await??
380 } else {
381 let mut buffer = String::new();
382 std::io::stdin().read_line(&mut buffer)?;
383 buffer
384 }
385 }
386 };
387
388 let key = key.trim();
389
390 let decoded_key = hex::decode(key).context("Key must be hex encoded")?;
391
392 let key_str = str::from_utf8(&decoded_key)?;
393
394 let LotusJson(key_info) = serde_json::from_str::<LotusJson<KeyInfo>>(key_str)
395 .context("invalid key format")?;
396
397 let key = backend.wallet_import(key_info).await?;
398
399 println!("{key}");
400 Ok(())
401 }
402 Self::List {
403 no_round,
404 no_abbrev,
405 } => {
406 let key_pairs = backend.list_addrs().await?;
407 let default = backend.wallet_default_address().await?;
408
409 let max_addr_len = key_pairs
410 .iter()
411 .map(|addr| addr.to_string().len())
412 .max()
413 .unwrap_or(42);
414
415 println!(
416 "{:<width_addr$} {:<width_default$} Balance",
417 "Address",
418 "Default",
419 width_addr = max_addr_len,
420 width_default = 7,
421 );
422
423 for address in key_pairs {
424 let default_address_mark = if default.as_ref() == Some(&address.to_string()) {
425 "X"
426 } else {
427 ""
428 };
429
430 let balance_token_amount =
431 WalletBalance::call(&backend.remote, (address,)).await?;
432
433 let balance_string = format_balance(&balance_token_amount, no_round, no_abbrev);
434
435 println!(
436 "{:<width_addr$} {:<width_default$} {}",
437 address.to_string(),
438 default_address_mark,
439 balance_string,
440 width_addr = max_addr_len,
441 width_default = 7,
442 );
443 }
444 Ok(())
445 }
446 Self::SetDefault { key } => {
447 let StrictAddress(key) = StrictAddress::from_str(&key)
448 .with_context(|| format!("Invalid address: {key}"))?;
449
450 backend.wallet_set_default(key).await
451 }
452 Self::Sign { address, message } => {
453 let StrictAddress(address) = StrictAddress::from_str(&address)
454 .with_context(|| format!("Invalid address: {address}"))?;
455
456 let message = hex::decode(message).context("Message has to be a hex string")?;
457 let message = BASE64_STANDARD.encode(message);
458
459 let signature = backend.wallet_sign(address, message).await?;
460 println!("{}", hex::encode(signature.to_bytes()));
461 Ok(())
462 }
463 Self::ValidateAddress { address } => {
464 let response = WalletValidateAddress::call(&backend.remote, (address,)).await?;
465 println!("{response}");
466 Ok(())
467 }
468 Self::Verify {
469 message,
470 address,
471 signature,
472 } => {
473 let sig_bytes =
474 hex::decode(signature).context("Signature has to be a hex string")?;
475 let StrictAddress(address) = StrictAddress::from_str(&address)
476 .with_context(|| format!("Invalid address: {address}"))?;
477 let msg = hex::decode(message).context("Message has to be a hex string")?;
478
479 let signature = Signature::from_bytes(sig_bytes)?;
480 let is_valid = backend.wallet_verify(address, msg, signature).await?;
481
482 println!("{is_valid}");
483 Ok(())
484 }
485 Self::Send {
486 from,
487 target_address,
488 amount,
489 gas_feecap,
490 gas_limit,
491 gas_premium,
492 } => {
493 let from: Address = if let Some(from) = from {
494 StrictAddress::from_str(&from)?.into()
495 } else {
496 StrictAddress::from_str(&backend.wallet_default_address().await?.context(
497 "No default wallet address selected. Please set a default address.",
498 )?)?
499 .into()
500 };
501
502 let message = Message {
503 from,
504 to: StrictAddress::from_str(&target_address)?.into(),
505 value: amount,
506 method_num: METHOD_SEND,
507 gas_limit: gas_limit as u64,
508 gas_fee_cap: gas_feecap,
509 gas_premium,
510 ..Default::default()
511 };
512
513 let signed_msg = if let Some(keystore) = &backend.local {
514 let spec = None;
515 let mut message = GasEstimateMessageGas::call(
516 &backend.remote,
517 (message, spec, ApiTipsetKey(None)),
518 )
519 .await?;
520
521 if message.gas_premium > message.gas_fee_cap {
522 anyhow::bail!("After estimation, gas premium is greater than gas fee cap")
523 }
524
525 message.sequence = MpoolGetNonce::call(&backend.remote, (from,)).await?;
526
527 let key = crate::key_management::find_key(&from, keystore)?;
528 let sig = crate::key_management::sign(
529 *key.key_info.key_type(),
530 key.key_info.private_key(),
531 message.cid().to_bytes().as_slice(),
532 )?;
533
534 let smsg = SignedMessage::new_from_parts(message, sig)?;
535
536 MpoolPush::call(&backend.remote, (smsg.clone(),)).await?;
537 smsg
538 } else {
539 MpoolPushMessage::call(&backend.remote, (message, None)).await?
540 };
541
542 println!("{}", signed_msg.cid());
543
544 Ok(())
545 }
546 }
547 }
548}
549
550fn input_password_to_load_encrypted_keystore(data_dir: PathBuf) -> dialoguer::Result<KeyStore> {
554 let keystore = RefCell::new(None);
555 let term = Term::stderr();
556
557 if !term.is_term() {
561 return Err(std::io::Error::new(
562 std::io::ErrorKind::NotConnected,
563 "cannot read password from non-terminal",
564 )
565 .into());
566 }
567
568 dialoguer::Password::new()
569 .with_prompt("Enter the password for the wallet keystore")
570 .allow_empty_password(true) .validate_with(|input: &String| {
572 KeyStore::new(KeyStoreConfig::Encrypted(data_dir.clone(), input.clone()))
573 .map(|created| *keystore.borrow_mut() = Some(created))
574 .context(
575 "Error: couldn't load keystore with this password. Try again or press Ctrl+C to abort.",
576 )
577 })
578 .interact_on(&term)?;
579
580 Ok(keystore
581 .into_inner()
582 .expect("validation succeeded, so keystore must be emplaced"))
583}
584
585fn format_balance(balance: &TokenAmount, no_round: bool, no_abbrev: bool) -> String {
586 match (no_round, no_abbrev) {
587 (true, true) => format!("{:#}", balance.pretty()),
589 (true, false) => format!("{}", balance.pretty()),
591 (false, true) => format!("{:#.4}", balance.pretty()),
593 (false, false) => format!("{:.4}", balance.pretty()),
595 }
596}