diff --git a/.gitignore b/.gitignore index f0e3bca..332008f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target -**/*.rs.bk \ No newline at end of file +**/*.rs.bk +.idea/ diff --git a/Cargo.toml b/Cargo.toml index 371b05a..1356bf4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,6 @@ num-traits = "0.2.6" text_io = "0.1.7" reqwest = "0.9.15" serde = { version="1.0.90", features = ["derive"] } +dirs = "1.0.5" +qrcode = "0.10.0" +colored = "1.7.0" diff --git a/src/display_qr.rs b/src/display_qr.rs new file mode 100644 index 0000000..a3b11ec --- /dev/null +++ b/src/display_qr.rs @@ -0,0 +1,45 @@ +use qrcode::QrCode; +use std::iter; +use colored::*; + +pub fn display(data: &[u8]) { + + let code = match QrCode::new(data) { + Ok(code) => code, + Err(_) => { + return; + } + }; + + let string = code.render() + .light_color(' ') + .dark_color('#') + .build(); + + let mut empty_str: String; + let mut line_buffer = String::new(); + let mut lines = string.lines().into_iter(); + + while let Some(line_top) = lines.next() { + let line_bottom = match lines.next() { + Some(l) => l, + None => { + empty_str = iter::repeat(' ').take(line_top.len()).collect(); + empty_str.as_str() + } + }; + + for (top, bottom) in line_top.chars().zip(line_bottom.chars()) { + let block = match (top, bottom) { + ('#', '#') => '█', + (' ', '#') => '▄', + ('#', ' ') => '▀', + _ => ' ', + }; + line_buffer.push(block); + } + + println!("{}", line_buffer.as_str().black().on_bright_white()); + line_buffer.clear(); + } +} diff --git a/src/main.rs b/src/main.rs index e3ef514..fca42a7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ pub mod address; pub mod outputs; pub mod wallet; pub mod trade; +pub mod display_qr; use std::io::{self, Write, Read}; use text_io::{read, try_read, try_scan}; @@ -19,25 +20,22 @@ use std::env; const WALLET_FILE_NAME: &str = "trade.dat"; +const SLP_AGORA_PATH: &str = ".slpagora"; fn ensure_wallet_interactive() -> Result> { - match std::fs::File::open(WALLET_FILE_NAME) { + let trades_dir = dirs::home_dir().unwrap_or(env::current_dir()?).join(SLP_AGORA_PATH); + let wallet_file_path = trades_dir.as_path().join(WALLET_FILE_NAME); + std::fs::create_dir_all(trades_dir)?; + match std::fs::File::open(&wallet_file_path) { Ok(mut file) => { + println!("Using wallet file at {}", wallet_file_path.display()); let mut secret_bytes = [0; 32]; file.read(&mut secret_bytes)?; Ok(wallet::Wallet::from_secret(&secret_bytes)?) }, Err(ref err) if err.kind() == io::ErrorKind::NotFound => { - println!("There's currently no wallet created. Press ENTER to create one at the \ - current working directory ({}/{}) or enter the path to the wallet file: ", - env::current_dir()?.display(), - WALLET_FILE_NAME); - io::stdout().flush()?; - let wallet_file_path: String = read!("{}\n"); - let wallet_file_path = - if wallet_file_path.len() != 0 { &wallet_file_path } - else {WALLET_FILE_NAME}; + println!("Creating wallet at {}", wallet_file_path.display()); use rand::RngCore; let mut rng = rand::rngs::OsRng::new().unwrap(); let mut secret_bytes = [0; 32]; @@ -50,27 +48,13 @@ fn ensure_wallet_interactive() -> Result> } } -pub fn show_qr(s: &str) { - use std::process::Command; - println!("{}", String::from_utf8( - Command::new("python3") - .args(&[ - "-c", - &format!( - "import pyqrcode; print(pyqrcode.create('{}').terminal())", - s, - ), - ]) - .output() - .unwrap().stdout - ).unwrap()); -} - fn show_balance(w: &wallet::Wallet) { let balance = w.get_balance(); println!("Your wallet's balance is: {} sats or {} BCH.", balance, balance as f64 / 100_000_000.0); + println!("Your wallet's address is: {}", w.address().cash_addr()); + display_qr::display(w.address().cash_addr().as_bytes()); } fn do_transaction(w: &wallet::Wallet) -> Result<(), Box> { @@ -78,10 +62,16 @@ fn do_transaction(w: &wallet::Wallet) -> Result<(), Box> { println!("Your wallet's balance is: {} sats or {} BCH.", balance, balance as f64 / 100_000_000.0); + if balance < w.dust_amount() { + println!("Your balance ({}) isn't sufficient to broadcast a transaction. Please fund some \ + BCH to your wallet's address: {}", balance, w.address().cash_addr()); + return Ok(()); + } print!("Enter the address to send to: "); io::stdout().flush()?; let addr_str: String = read!("{}\n"); - let receiving_addr = match address::Address::from_cash_addr(addr_str) { + let addr_str = addr_str.trim(); + let receiving_addr = match address::Address::from_cash_addr(addr_str.to_string()) { Ok(addr) => addr, Err(err) => { println!("Please enter a valid address: {:?}", err); @@ -96,22 +86,30 @@ fn do_transaction(w: &wallet::Wallet) -> Result<(), Box> { balance: "); io::stdout().flush()?; let send_amount_str: String = read!("{}\n"); - let send_amount = if send_amount_str.as_str() == "all" { + let send_amount_str = send_amount_str.trim(); + let send_amount = if send_amount_str == "all" { balance } else { send_amount_str.parse::()? }; - tx_build.add_output(&outputs::P2PKHOutput { + let mut output_send = outputs::P2PKHOutput { value: send_amount, address: receiving_addr, - }); + }; + let send_idx = tx_build.add_output(&output_send); let mut output_back_to_wallet = outputs::P2PKHOutput { value: 0, address: w.address().clone(), }; let back_to_wallet_idx = tx_build.add_output(&output_back_to_wallet); let estimated_size = tx_build.estimate_size(); - let send_back_to_wallet_amount = balance - (send_amount + estimated_size + 5); + let send_back_to_wallet_amount = if balance < send_amount + (estimated_size + 5) { + output_send.value = balance - (estimated_size + 5); + tx_build.replace_output(send_idx, &output_send); + 0 + } else { + balance - (send_amount + estimated_size + 5) + }; if send_back_to_wallet_amount < w.dust_amount() { tx_build.remove_output(back_to_wallet_idx); } else { @@ -119,7 +117,8 @@ fn do_transaction(w: &wallet::Wallet) -> Result<(), Box> { tx_build.replace_output(back_to_wallet_idx, &output_back_to_wallet); } let tx = tx_build.sign(); - w.send_tx(&tx)?; + let response = w.send_tx(&tx)?; + println!("Sent transaction. Transaction ID is: {}", response); Ok(()) } @@ -128,21 +127,26 @@ fn main() -> Result<(), Box> { let wallet = ensure_wallet_interactive()?; println!("Your wallet address is: {}", wallet.address().cash_addr()); - println!("Select an option from below:"); - println!("1: Show wallet balance"); - println!("2: Send BCH from this wallet to an address"); - println!("3: Create a new trade for a token on the BCH blockchain"); - println!("4: List all available token trades on the BCH blockchain"); - println!("Anything else: Exit"); - print!("Your choice: "); - io::stdout().flush()?; - let wallet_file_path: String = read!("{}\n"); - match wallet_file_path.as_str() { - "1" => show_balance(&wallet), - "2" => do_transaction(&wallet)?, - "3" => trade::create_trade_interactive(&wallet)?, - "4" => trade::accept_trades_interactive(&wallet)?, - _ => println!("Bye, have a great time!"), + loop { + println!("---------------------------------"); + println!("Select an option from below:"); + println!("1: Show wallet balance / fund wallet"); + println!("2: Send BCH from this wallet to an address"); + println!("3: Create a new trade for a token on the BCH blockchain"); + println!("4: List all available token trades on the BCH blockchain"); + println!("Anything else: Exit"); + print!("Your choice: "); + io::stdout().flush()?; + let choice: String = read!("{}\n"); + match choice.trim() { + "1" => show_balance(&wallet), + "2" => do_transaction(&wallet)?, + "3" => trade::create_trade_interactive(&wallet)?, + "4" => trade::accept_trades_interactive(&wallet)?, + _ => { + println!("Bye, have a great time!"); + return Ok(()); + }, + } } - Ok(()) } diff --git a/src/trade.rs b/src/trade.rs index dcf5509..d3d65d7 100644 --- a/src/trade.rs +++ b/src/trade.rs @@ -2,7 +2,7 @@ use crate::wallet::Wallet; use crate::outputs::{EnforceOutputsOutput, SLPSendOutput, P2PKHOutput, TradeOfferOutput, P2SHOutput}; use crate::address::{Address, AddressType}; use crate::hash::hash160; -use crate::incomplete_tx::{Output, Utxo}; +use crate::incomplete_tx::{IncompleteTx, Output, Utxo}; use crate::tx::{tx_hex_to_hash, TxOutpoint}; use crate::script::{Script, Op, OpCodeType}; use std::io::{self, Write, Cursor}; @@ -97,9 +97,16 @@ fn option_str(s: &Option) -> &str { } pub fn create_trade_interactive(wallet: &Wallet) -> Result<(), Box> { + let (tx_build, balance) = wallet.init_transaction(); + if balance < wallet.dust_amount() { + println!("Your balance ({}) isn't sufficient to broadcast a transaction. Please fund some \ + BCH to your wallet's address: {}", balance, wallet.address().cash_addr()); + return Ok(()); + } print!("Enter the token id or token name/symbol you want to sell: "); io::stdout().flush()?; let token_str: String = read!("{}\n"); + let token_str = token_str.trim().to_string(); let mut tokens_found = fetch_tokens(Some(&token_str))?; if tokens_found.len() == 0 { @@ -138,8 +145,8 @@ pub fn create_trade_interactive(wallet: &Wallet) -> Result<(), Box() { @@ -170,6 +177,7 @@ pub fn create_trade_interactive(wallet: &Wallet) -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box> { let mut token_id = [0; 32]; token_id.copy_from_slice(&hex::decode(&token.id)?); - let receiving_address = w.address().clone(); - let cancel_address = w.address().clone(); + let receiving_address = wallet.address().clone(); + let cancel_address = wallet.address().clone(); let output = EnforceOutputsOutput { value: 0, // ignored for script hash generation enforced_outputs: vec![ @@ -231,23 +244,26 @@ fn confirm_trade_interactive(w: &Wallet, AddressType::P2SH, pkh, ); + let already_existing = wallet.get_utxos(&addr_bch).into_iter() + .map(|utxo| utxo.txid) + .collect::>(); println!("--------------------------------------------------"); + crate::display_qr::display(addr_slp.cash_addr().as_bytes()); println!("Please send EXACTLY {} {} to the following address:", sell_amount_display, option_str(&token.symbol)); println!("{}", addr_slp.cash_addr()); - println!(); + println!("You can also scan the QR code above."); println!("Sending a different amount or incorrect token will likely burn the tokens."); println!("\nDO NOT CLOSE THIS PROGRAM YET BEFORE OR AFTER YOU SENT THE PAYMENT"); println!("Waiting for transaction..."); - let utxo = w.wait_for_transaction(&addr_bch); + let utxo = wallet.wait_for_transaction(&addr_bch, &already_existing); println!("Received tx: {}", utxo.txid); - let (mut tx_build, balance) = w.init_transaction(); tx_build.add_output(&TradeOfferOutput { tx_id: tx_hex_to_hash(&utxo.txid), output_idx: utxo.vout, @@ -259,14 +275,18 @@ fn confirm_trade_interactive(w: &Wallet, let size_so_far = tx_build.estimate_size(); let mut send_output = P2PKHOutput { value: 0, - address: w.address().clone(), + address: wallet.address().clone(), }; let size_output = send_output.script().to_vec().len() as u64; - send_output.value = balance - (size_so_far + size_output) - 20; + let total_spent = size_so_far + size_output + 20; + if total_spent > balance { + println!("The broadcast transaction cannot be sent due to insufficient funds"); + } + send_output.value = balance - total_spent; tx_build.add_output(&send_output); let tx = tx_build.sign(); - let result = w.send_tx(&tx)?; + let result = wallet.send_tx(&tx)?; println!("The trade listing transaction ID is: {}", result); Ok(()) @@ -418,10 +438,16 @@ pub fn accept_trades_interactive(wallet: &Wallet) -> Result<(), Box Result<(), Box addr, Err(err) => { println!("Please enter a valid address: {:?}", err); @@ -557,17 +584,21 @@ pub fn accept_trades_interactive(wallet: &Wallet) -> Result<(), Box { + let response = wallet.send_tx(&tx)?; + println!("Sent transaction. Transaction ID is: {}", response); + }, + "hex" => { + println!("{}", hex::encode(&tx_ser)); + }, + _ => {}, } Ok(()) diff --git a/src/wallet.rs b/src/wallet.rs index 031c757..f93e4ce 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -3,6 +3,7 @@ use serde::{Serialize, Deserialize}; use crate::incomplete_tx::{IncompleteTx, Utxo}; use crate::tx::{Tx, TxOutpoint, tx_hex_to_hash}; use crate::outputs::{P2PKHOutput}; +use std::collections::HashSet; pub struct Wallet { @@ -16,7 +17,6 @@ pub struct UtxoEntry { pub vout: u32, pub amount: f64, pub satoshis: u64, - pub confirmations: u32, } #[derive(Deserialize, Serialize, Debug)] @@ -51,11 +51,15 @@ impl Wallet { self.get_utxos(&self.address).iter().map(|utxo| utxo.satoshis).sum() } - pub fn wait_for_transaction(&self, address: &Address) -> UtxoEntry { + pub fn wait_for_transaction(&self, address: &Address, already_existing: &HashSet) + -> UtxoEntry { loop { - let mut utxos = self.get_utxos(address); - if utxos.len() > 0 { - return utxos.remove(0) + let utxos = self.get_utxos(address); + let mut remaining = utxos.into_iter() + .filter(|utxo| !already_existing.contains(&utxo.txid)) + .collect::>(); + if remaining.len() > 0 { + return remaining.remove(0) } std::thread::sleep(std::time::Duration::new(1, 0)); }