Skip to content

Commit

Permalink
Add 'sent' to wallet info output (#288) (#575)
Browse files Browse the repository at this point in the history
  • Loading branch information
heunglee authored and yeastplume committed Jan 7, 2018
1 parent 5d280f4 commit c1f8600
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 5 deletions.
1 change: 1 addition & 0 deletions wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ tokio-retry="~0.1.0"
router = "~0.5.1"
prettytable-rs = "^0.6"
term = "~0.4.6"
time = "^0.1"
grin_api = { path = "../api" }
grin_core = { path = "../core" }
grin_keychain = { path = "../keychain" }
Expand Down
53 changes: 52 additions & 1 deletion wallet/src/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
use checker;
use keychain::Keychain;
use core::core::amount_to_hr_string;
use types::{WalletConfig, WalletData, OutputStatus};
use types::{WalletConfig, WalletData, OutputStatus, StatsData, format_transfer_timestamp};
use prettytable;
use term;
use std::io::prelude::*;
use std::cmp::Reverse;

pub fn show_info(config: &WalletConfig, keychain: &Keychain) {
let result = checker::refresh_outputs(&config, &keychain);
Expand Down Expand Up @@ -83,5 +84,55 @@ pub fn show_info(config: &WalletConfig, keychain: &Keychain) {
The above is from local cache and possibly invalid! \
(is your `grin server` offline or broken?)"
);
} else {
show_stats(config);
}
}

/// Display the history of transfers of sending and receiving.
/// Each row shows transfer type (Sent, Received), amount, Sent or Received at and receiving wallet address.
fn show_stats(config: &WalletConfig) {
let _ = StatsData::read_stats(&config.data_file_dir, |stats_data| {
let total_transfers = stats_data.transfers.len();
let title=format!("Grin Coin Transfer List - Total Transfers: {}", total_transfers);

println!();
let mut t = term::stdout().unwrap();
t.fg(term::color::MAGENTA).unwrap();
writeln!(t, "{}", title).unwrap();
t.reset().unwrap();
println!("Please note that 'sent or received at' indicates the date & time\n your wallet sent or received grin coins at.");

let mut stats = stats_data
.transfers
.values()
.collect::<Vec<_>>();
stats.sort_by_key(|stat| Reverse(stat.sent_or_received_at));

let mut table = table!();
// Set table titles.
table.add_row(row![
bFB->"Transfer",
bFB->"Amount",
bFB->"Sent or Received at",
bFB->"Receiving Wallet Address"
]);

// Add a row per time
for txr in stats {
let tx_type = format!("{:?}", txr.tx_type);
let amount = format!("{}", amount_to_hr_string(txr.amount));
let sent_or_received_at = format_transfer_timestamp(txr.sent_or_received_at);
table.add_row(row![
bFG->tx_type,
bFM->amount,
bFd->sent_or_received_at,
bFd->txr.receiving_wallet_address
]);
};

table.printstd();
println!();
});

}
1 change: 1 addition & 0 deletions wallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ extern crate serde_derive;
extern crate serde_json;
#[macro_use]
extern crate slog;
extern crate time;
#[macro_use]
extern crate prettytable;
extern crate term;
Expand Down
19 changes: 19 additions & 0 deletions wallet/src/receiver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,5 +237,24 @@ fn receive_transaction(
derivation,
);

// append transfer record of received coins into stats.dat
// only if the transaction is completed.
StatsData::append(&config.data_file_dir, |stats_data| {
let current_time_sec = get_transfer_timestamp();
let (ins, outs) = get_transfer_inouts(&tx_final);
let port = config.api_listen_port.to_string();
let addr = format!("{}:{}", "localhost".to_string(), port);
stats_data.add_transfer(
StatsTransferData {
amount: amount,
receiving_wallet_address: addr,
tx_type: StatsTransferType::Received,
inputs: ins,
outputs: outs,
sent_or_received_at: current_time_sec,
}
);
})?;

Ok((tx_final, key_id))
}
21 changes: 20 additions & 1 deletion wallet/src/sender.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ pub fn issue_send_tx(
selection_strategy,
)?;

let partial_tx = build_partial_tx(amount, blind_sum, tx);
let partial_tx = build_partial_tx(amount, blind_sum, &tx);

// Closure to acquire wallet lock and lock the coins being spent
// so we avoid accidental double spend attempt.
Expand Down Expand Up @@ -95,6 +95,25 @@ pub fn issue_send_tx(
} else {
panic!("dest not in expected format: {}", dest);
}

// Append "Sent" transfer record into stats.dat
// only if the transaction is completed.
StatsData::append(&config.data_file_dir, |stats_data| {
let current_time_sec = get_transfer_timestamp();
let (ins, outs) = get_transfer_inouts(&tx);
let addr = dest.replace("http://", "");
stats_data.add_transfer(
StatsTransferData {
amount: amount,
receiving_wallet_address: addr,
tx_type: StatsTransferType::Sent,
inputs: ins,
outputs: outs,
sent_or_received_at: current_time_sec,
}
);
})?;

Ok(())
}

Expand Down
153 changes: 150 additions & 3 deletions wallet/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,20 @@ use tokio_core::reactor;
use tokio_retry::Retry;
use tokio_retry::strategy::FibonacciBackoff;


use api;
use core::consensus;
use core::core::{transaction, Transaction};
use core::ser;
use keychain;
use time;
use time::Timespec;
use util;
use util::LOGGER;

const DAT_FILE: &'static str = "wallet.dat";
const LOCK_FILE: &'static str = "wallet.lock";
const SEED_FILE: &'static str = "wallet.seed";
const STATS_FILE: &'static str = "wallet.stats";

const DEFAULT_BASE_FEE: u64 = consensus::MILLI_GRIN;

Expand Down Expand Up @@ -276,6 +278,40 @@ impl OutputData {
}
}

/// Types of a transfer that's being tracked by the wallet.
/// Can either be Sent, or Received.
#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)]
pub enum StatsTransferType {
Sent,
Received,
}

impl fmt::Display for StatsTransferType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
StatsTransferType::Sent => write!(f, "Sent"),
StatsTransferType::Received => write!(f, "Received"),
}
}
}

/// Minimum information about coin transfer that's being tracked by the wallet.
#[derive(Serialize, Deserialize, Hash, Eq, PartialEq, Debug, Clone)]
pub struct StatsTransferData {
/// Amount sent or received
pub amount: u64,
/// Recipient's wallet address
pub receiving_wallet_address: String,
/// Transfer type of current transaction
pub tx_type: StatsTransferType,
/// Set of inputs spent by the transaction.
pub inputs: Vec<String>,
/// Set of outputs the transaction produces.
pub outputs: Vec<String>,
/// Time in seconds after epoch at which fund transfer is sent or received.
pub sent_or_received_at: i64,
}

#[derive(Clone, PartialEq)]
pub struct WalletSeed([u8; 32]);

Expand Down Expand Up @@ -601,6 +637,91 @@ impl WalletData {
}
}

/// Wallet stats information tracking all transfers.
/// This data structure is directly based on the JSON representation stored
/// on disk.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct StatsData {
pub transfers: HashMap<String, StatsTransferData>,
}

impl StatsData {
/// Allows for reading stats data (without needing to acquire the write
/// lock).
pub fn read_stats<T, F>(stats_file_dir: &str, f: F) -> Result<T, Error>
where
F: FnOnce(&StatsData) -> T,
{
// open the wallet readonly and do what needs to be done with it
let stats_file_path = &format!("{}{}{}", stats_file_dir, MAIN_SEPARATOR, STATS_FILE);
let sdat = StatsData::read_or_create(stats_file_path)?;
let res = f(&sdat);
Ok(res)
}

/// Read the stats data or created a brand new one if it doesn't exist yet
fn read_or_create(stats_file_path: &str) -> Result<StatsData, Error> {
if Path::new(stats_file_path).exists() {
StatsData::read(stats_file_path)
} else {
// just create a new instance, it will get written afterward
Ok(StatsData {
transfers: HashMap::new(),
})
}
}

/// Read the stats data from disk.
fn read(stats_file_path: &str) -> Result<StatsData, Error> {
let stats_file = File::open(stats_file_path).map_err(|e| {
Error::WalletData(format!("Could not open {}: {}", stats_file_path, e))
})?;
serde_json::from_reader(stats_file).map_err(|e| {
Error::WalletData(format!("Error reading {}: {}", stats_file_path, e))
})
}

/// Append a new transfer record to the stats data.
pub fn add_transfer(&mut self, tx: StatsTransferData) {
let k = tx.sent_or_received_at.to_string();
self.transfers.insert(k, tx.clone());
}

pub fn append<F>(data_file_dir: &str, f: F) -> Result<(), Error>
where
F: FnOnce(&mut StatsData),
{
// create directory if it doesn't exist
fs::create_dir_all(data_file_dir).unwrap_or_else(|why| {
info!(LOGGER, "! {:?}", why.kind());
});

let stats_file_path = &format!("{}{}{}", data_file_dir, MAIN_SEPARATOR, STATS_FILE);
// Need read the existing json data from wallet.stats to append new transfer record.
let mut stats = StatsData::read_or_create(stats_file_path)?;
f(&mut stats);
stats.write(stats_file_path)
}

/// Write the wallet data to disk.
fn write(&self, stats_file_path: &str) -> Result<(), Error> {
let mut stats_file = OpenOptions::new()
.write(true)
.create(true)
.open(stats_file_path)
.map_err(|e| {
Error::WalletData(format!("Could not create {}: {}", stats_file_path, e))
})?;
let res_json = serde_json::to_vec_pretty(self).map_err(|e| {
Error::WalletData(format!("Error serializing stats data: {}", e))
})?;
stats_file.write_all(res_json.as_slice()).map_err(|e| {
Error::WalletData(format!("Error writing {}: {}", stats_file_path, e))
})
}

}

/// Helper in serializing the information a receiver requires to build a
/// transaction.
#[derive(Serialize, Deserialize, Debug, Clone)]
Expand All @@ -614,12 +735,12 @@ pub struct PartialTx {
pub fn build_partial_tx(
receive_amount: u64,
blind_sum: keychain::BlindingFactor,
tx: Transaction,
tx: &Transaction,
) -> PartialTx {
PartialTx {
amount: receive_amount,
blind_sum: util::to_hex(blind_sum.secret_key().as_ref().to_vec()),
tx: util::to_hex(ser::ser_vec(&tx).unwrap()),
tx: util::to_hex(ser::ser_vec(tx).unwrap()),
}
}

Expand Down Expand Up @@ -667,3 +788,29 @@ pub struct CbData {
pub kernel: String,
pub key_id: String,
}

/// Get the date and time in seconds after the beginning of epoch.
pub fn get_transfer_timestamp() -> i64 {
let current_utc = time::now_utc();
current_utc.to_timespec().sec
}

/// Format transferred_at in seconds after the beginning of epock into rfc 822 format.
/// Example: "Thu, 22 Mar 2012 14:53:18 GMT"
pub fn format_transfer_timestamp(timestamp_sec: i64) -> String {
let transfer_tm = time::at(Timespec::new(timestamp_sec, 0));
transfer_tm.rfc822().to_string()
}

/// Retrieve commitments of inputs and outputs of transaction.
pub fn get_transfer_inouts(tx: &Transaction) -> (Vec<String>, Vec<String>) {
let ins = tx.inputs
.iter()
.map(|input| util::to_hex((input.0).0.to_vec()))
.collect();
let outs = tx.outputs
.iter()
.map(|output| util::to_hex(output.commit.0.to_vec()))
.collect();
(ins, outs)
}

0 comments on commit c1f8600

Please sign in to comment.