273 lines
9.2 KiB
Rust
273 lines
9.2 KiB
Rust
|
|
use base64::engine::general_purpose::STANDARD;
|
||
|
|
use base64::Engine;
|
||
|
|
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
|
||
|
|
use blockchain::common::network_startup::get_connections;
|
||
|
|
use blockchain::decode_image_and_extract_text;
|
||
|
|
use blockchain::decrypts;
|
||
|
|
use blockchain::env;
|
||
|
|
use blockchain::fs;
|
||
|
|
use blockchain::read_to_string;
|
||
|
|
use blockchain::records::memory::response_channels::generate_uid;
|
||
|
|
use blockchain::standalone_tools::connections::handshake;
|
||
|
|
use blockchain::stdout;
|
||
|
|
use blockchain::tilde;
|
||
|
|
use blockchain::wallets::structures::{SavedWallet, Wallet};
|
||
|
|
use blockchain::AsyncWriteExt;
|
||
|
|
use rustyline::completion::FilenameCompleter;
|
||
|
|
use rustyline::error::ReadlineError;
|
||
|
|
use rustyline::{history::DefaultHistory, CompletionType, Config, Editor};
|
||
|
|
use rustyline_derive::Completer;
|
||
|
|
use rustyline_derive::Helper as RustyHelper;
|
||
|
|
use rustyline_derive::Highlighter as RustyHighlighter;
|
||
|
|
use rustyline_derive::Hinter as RustyHinter;
|
||
|
|
use rustyline_derive::Validator as RustyValidator;
|
||
|
|
use std::path::PathBuf;
|
||
|
|
|
||
|
|
fn display_vanity_address(short_address: &str) -> Option<String> {
|
||
|
|
let (payload, network_suffix) = short_address.rsplit_once('.')?;
|
||
|
|
let trimmed_payload = payload.trim_start();
|
||
|
|
if trimmed_payload.is_empty() {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
Some(format!("{trimmed_payload}.{network_suffix}"))
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn lookup_registered_vanity_address(
|
||
|
|
short_address: &str,
|
||
|
|
long_address: &str,
|
||
|
|
private_key: &str,
|
||
|
|
) -> Option<String> {
|
||
|
|
let hashmap_key = generate_uid();
|
||
|
|
let rpc_command = 40;
|
||
|
|
let connections = get_connections().await;
|
||
|
|
|
||
|
|
for conn in connections {
|
||
|
|
let Ok(socket_address) = conn.parse() else {
|
||
|
|
continue;
|
||
|
|
};
|
||
|
|
|
||
|
|
let result = handshake::connect_and_handshake(
|
||
|
|
socket_address,
|
||
|
|
short_address.to_string(),
|
||
|
|
rpc_command,
|
||
|
|
handshake::HandshakeWallet::WalletParts {
|
||
|
|
long_address: long_address.to_string(),
|
||
|
|
private_key: private_key.to_string(),
|
||
|
|
},
|
||
|
|
hashmap_key,
|
||
|
|
)
|
||
|
|
.await;
|
||
|
|
|
||
|
|
if let Ok(response) = result {
|
||
|
|
if response.is_empty() {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let response_text = String::from_utf8_lossy(&response);
|
||
|
|
let trimmed = response_text.trim();
|
||
|
|
if trimmed.is_empty() || trimmed.starts_with("error:") {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
return display_vanity_address(trimmed);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
None
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn load_wallet(private_key_image: String, wallet_key: String) -> String {
|
||
|
|
let private_key: String;
|
||
|
|
if let Some(encrypted_text) = decode_image_and_extract_text(&private_key_image) {
|
||
|
|
// Decrypt the encrypted text to get the real private key
|
||
|
|
if let Some(decrypted_private_key) = decrypts(&encrypted_text, Some(&wallet_key)) {
|
||
|
|
// Update the wallet's private key with the decrypted private key
|
||
|
|
private_key = decrypted_private_key;
|
||
|
|
} else {
|
||
|
|
eprintln!("Decryption of private key failed.");
|
||
|
|
panic!();
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
eprintln!("Failed to decode the image and extract text.");
|
||
|
|
panic!();
|
||
|
|
}
|
||
|
|
private_key
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(RustyHelper, Completer, RustyHinter, RustyHighlighter, RustyValidator)]
|
||
|
|
struct PathHelper {
|
||
|
|
#[rustyline(Completer)]
|
||
|
|
completer: FilenameCompleter,
|
||
|
|
}
|
||
|
|
|
||
|
|
fn prompt_for_path(prompt: &str) -> Result<String, String> {
|
||
|
|
let config = Config::builder()
|
||
|
|
.completion_type(CompletionType::List)
|
||
|
|
.build();
|
||
|
|
let mut editor =
|
||
|
|
Editor::<PathHelper, DefaultHistory>::with_config(config).map_err(|e| e.to_string())?;
|
||
|
|
editor.set_helper(Some(PathHelper {
|
||
|
|
completer: FilenameCompleter::new(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
match editor.readline(prompt) {
|
||
|
|
Ok(line) => Ok(line.trim().to_string()),
|
||
|
|
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
|
||
|
|
Err("Input cancelled".to_string())
|
||
|
|
}
|
||
|
|
Err(err) => Err(format!("Failed to read file path: {err}")),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn prompt_for_filename(prompt: &str) -> Result<String, String> {
|
||
|
|
let config = Config::builder()
|
||
|
|
.completion_type(CompletionType::List)
|
||
|
|
.build();
|
||
|
|
let mut editor =
|
||
|
|
Editor::<PathHelper, DefaultHistory>::with_config(config).map_err(|e| e.to_string())?;
|
||
|
|
editor.set_helper(Some(PathHelper {
|
||
|
|
completer: FilenameCompleter::new(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
match editor.readline(prompt) {
|
||
|
|
Ok(line) => {
|
||
|
|
let trimmed = line.trim();
|
||
|
|
if trimmed.is_empty() {
|
||
|
|
Err("Filename cannot be empty".to_string())
|
||
|
|
} else {
|
||
|
|
Ok(trimmed.to_string())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
|
||
|
|
Err("Input cancelled".to_string())
|
||
|
|
}
|
||
|
|
Err(err) => Err(format!("Failed to read filename: {err}")),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn load_private_key_image_input(path: &str) -> Result<String, String> {
|
||
|
|
let expanded_path = tilde(path).to_string();
|
||
|
|
|
||
|
|
if expanded_path.to_ascii_lowercase().ends_with(".png") {
|
||
|
|
let image_bytes =
|
||
|
|
fs::read(&expanded_path).map_err(|e| format!("Failed to read PNG file: {e}"))?;
|
||
|
|
return Ok(STANDARD.encode(image_bytes));
|
||
|
|
}
|
||
|
|
|
||
|
|
let file_contents = read_to_string(&expanded_path)
|
||
|
|
.await
|
||
|
|
.map_err(|e| format!("Failed to read Base64 string file: {e}"))?;
|
||
|
|
|
||
|
|
Ok(file_contents.trim().to_string())
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::main]
|
||
|
|
async fn main() {
|
||
|
|
let mut stdout = stdout(); // Get Tokio's stdout
|
||
|
|
|
||
|
|
// Collect command-line arguments
|
||
|
|
let args: Vec<String> = env::args().collect();
|
||
|
|
|
||
|
|
let base64_file_path: String;
|
||
|
|
let output_path: String;
|
||
|
|
|
||
|
|
if args.len() > 1 && args.len() != 3 && args.len() != 4 {
|
||
|
|
println!(
|
||
|
|
"Usage: ./recreate_wallet_from_image <base64_or_png_file_path> <output_wallet_file>"
|
||
|
|
);
|
||
|
|
println!(" or: ./recreate_wallet_from_image <base64_or_png_file_path> <output_wallet_dir> <output_wallet_filename>");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if args.len() == 4 {
|
||
|
|
base64_file_path = args[1].clone();
|
||
|
|
output_path = PathBuf::from(&args[2])
|
||
|
|
.join(&args[3])
|
||
|
|
.to_string_lossy()
|
||
|
|
.to_string();
|
||
|
|
} else if args.len() == 3 {
|
||
|
|
base64_file_path = args[1].clone();
|
||
|
|
output_path = args[2].clone();
|
||
|
|
} else {
|
||
|
|
stdout.flush().await.expect("Failed to flush stdout");
|
||
|
|
base64_file_path =
|
||
|
|
match prompt_for_path("Please enter the path to the Base64 or PNG image file: ") {
|
||
|
|
Ok(path) => path,
|
||
|
|
Err(err) => {
|
||
|
|
eprintln!("{err}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
let output_dir_path = match prompt_for_path(
|
||
|
|
"Please enter the directory path where the rebuilt wallet JSON should be written: ",
|
||
|
|
) {
|
||
|
|
Ok(path) => path,
|
||
|
|
Err(err) => {
|
||
|
|
eprintln!("{err}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
let output_filename =
|
||
|
|
match prompt_for_filename("Please enter the filename for the rebuilt wallet JSON: ") {
|
||
|
|
Ok(filename) => filename,
|
||
|
|
Err(err) => {
|
||
|
|
eprintln!("{err}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
let output_dir = tilde(&output_dir_path).to_string();
|
||
|
|
output_path = PathBuf::from(output_dir)
|
||
|
|
.join(output_filename)
|
||
|
|
.to_string_lossy()
|
||
|
|
.to_string();
|
||
|
|
}
|
||
|
|
|
||
|
|
let wallet_key = prompt_hidden_nonempty(
|
||
|
|
"Please enter your encryption key: ",
|
||
|
|
"Wallet key cannot be empty. Please try again.",
|
||
|
|
)
|
||
|
|
.await;
|
||
|
|
|
||
|
|
let base64_string = match load_private_key_image_input(&base64_file_path).await {
|
||
|
|
Ok(contents) => contents,
|
||
|
|
Err(err) => {
|
||
|
|
eprintln!("{err}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
let expanded_output_path = tilde(&output_path).to_string();
|
||
|
|
|
||
|
|
// Generate private key from Base64 string
|
||
|
|
let private_key = load_wallet(base64_string.clone(), wallet_key);
|
||
|
|
|
||
|
|
match Wallet::regenerate_public_key(&private_key) {
|
||
|
|
Ok(public_key_bytes) => {
|
||
|
|
let public_key = blockchain::encode(public_key_bytes.clone());
|
||
|
|
let long_address = Wallet::generate_address(&public_key);
|
||
|
|
let long_address_bytes = Wallet::long_address_to_bytes(long_address.clone());
|
||
|
|
let short_address_bytes =
|
||
|
|
Wallet::long_address_bytes_to_short_address_bytes(&long_address_bytes)
|
||
|
|
.expect("Failed to derive short address bytes");
|
||
|
|
let short_address = Wallet::bytes_to_short_address(&short_address_bytes)
|
||
|
|
.expect("Failed to encode short address");
|
||
|
|
let vanity_address =
|
||
|
|
lookup_registered_vanity_address(&short_address, &long_address, &private_key).await;
|
||
|
|
let saved_wallet = SavedWallet {
|
||
|
|
long_address,
|
||
|
|
short_address,
|
||
|
|
vanity_address,
|
||
|
|
public_key,
|
||
|
|
private_key: base64_string,
|
||
|
|
};
|
||
|
|
let output_str = serde_json::to_string_pretty(&saved_wallet).unwrap();
|
||
|
|
fs::write(&expanded_output_path, output_str)
|
||
|
|
.expect("Failed to write rebuilt wallet file");
|
||
|
|
println!("Wallet written to {expanded_output_path}");
|
||
|
|
}
|
||
|
|
Err(e) => println!("Error: {e}"),
|
||
|
|
}
|
||
|
|
}
|