Store payment outputs in wallet database (#1)

* store receiver's output into the sender's database

* rustfmt

* payment output refresh

* fix the test code in libwallet

* rustfmt

* fix wallet_command_line test

* modify the warning message for self sending

* a bit of unit test according to review comments

* display unknown value for the case of multiple outputs on single receiver, normally it's not the case of this wallet implementation

* rustfmt

* fix merge missing parts

* rustfmt

* use PaymentCommitMapping struct instead of a tuple

* rustfmt

* fix the test

* fix the owner api rpc test
This commit is contained in:
Gary Yu 2019-04-22 12:40:58 +08:00 committed by GitHub
parent 1a01655526
commit 12909c9262
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 597 additions and 21 deletions

View file

@ -26,8 +26,8 @@ use crate::keychain::{Identifier, Keychain};
use crate::libwallet::api_impl::owner;
use crate::libwallet::slate::Slate;
use crate::libwallet::types::{
AcctPathMapping, InitTxArgs, NodeClient, NodeHeightResult, OutputCommitMapping, TxLogEntry,
WalletBackend, WalletInfo,
AcctPathMapping, InitTxArgs, NodeClient, NodeHeightResult, OutputCommitMapping,
PaymentCommitMapping, TxLogEntry, WalletBackend, WalletInfo,
};
use crate::libwallet::{Error, ErrorKind};
@ -304,6 +304,56 @@ where
res
}
/// Returns a list of payment outputs from the active account in the wallet.
///
/// # Arguments
/// * `refresh_from_node` - If true, the wallet will attempt to contact
/// a node (via the [`NodeClient`](../grin_wallet_libwallet/types/trait.NodeClient.html)
/// provided during wallet instantiation). If `false`, the results will
/// contain output information that may be out-of-date (from the last time
/// the wallet's output set was refreshed against the node).
/// * `tx_id` - If `Some(i)`, only return the outputs associated with
/// the transaction log entry of id `i`.
///
/// # Returns
/// * `(bool, Vec<PaymentCommitMapping>)` - A tuple:
/// * The first `bool` element indicates whether the data was successfully
/// refreshed from the node (note this may be false even if the `refresh_from_node`
/// argument was set to `true`.
/// * The second element contains a vector of
/// [PaymentCommitMapping](../grin_wallet_libwallet/types/struct.PaymentCommitMapping.html)
/// of which each element is a mapping between the wallet's internal
/// [PaymentData](../grin_wallet_libwallet/types/struct.Output.html)
/// and the Output commitment
///
/// # Example
/// Set up as in [`new`](struct.Owner.html#method.new) method above.
/// ```
/// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config);
///
/// let api_owner = Owner::new(wallet.clone());
/// let update_from_node = true;
/// let tx_id = None;
///
/// let result = api_owner.retrieve_payments(update_from_node, tx_id);
///
/// if let Ok((was_updated, payment_mappings)) = result {
/// //...
/// }
/// ```
pub fn retrieve_payments(
&self,
refresh_from_node: bool,
tx_id: Option<Uuid>,
) -> Result<(bool, Vec<PaymentCommitMapping>), Error> {
let mut w = self.wallet.lock();
w.open_with_credentials()?;
let res = owner::retrieve_payments(&mut *w, refresh_from_node, tx_id);
w.close()?;
res
}
/// Returns a list of [Transaction Log Entries](../grin_wallet_libwallet/types/struct.TxLogEntry.html)
/// from the active account in the wallet.
///

View file

@ -162,6 +162,7 @@ pub trait OwnerRpc {
"mmr_index": null,
"n_child": 0,
"root_key_id": "0200000000000000000000000000000000",
"slate_id": null,
"status": "Unspent",
"tx_log_entry": 0,
"value": "60000000000"
@ -178,6 +179,7 @@ pub trait OwnerRpc {
"mmr_index": null,
"n_child": 1,
"root_key_id": "0200000000000000000000000000000000",
"slate_id": null,
"status": "Unspent",
"tx_log_entry": 1,
"value": "60000000000"

View file

@ -433,6 +433,20 @@ pub fn outputs(
Ok(())
}
pub fn payments(
wallet: Arc<Mutex<WalletInst<impl NodeClient + 'static, keychain::ExtKeychain>>>,
g_args: &GlobalArgs,
dark_scheme: bool,
) -> Result<(), Error> {
controller::owner_single_use(wallet.clone(), |api| {
let res = api.node_height()?;
let (validated, outputs) = api.retrieve_payments(true, None)?;
display::payments(&g_args.account, res.height, validated, outputs, dark_scheme)?;
Ok(())
})?;
Ok(())
}
/// Txs command args
pub struct TxsArgs {
pub id: Option<u32>,
@ -462,8 +476,22 @@ pub fn txs(
let (_, outputs) = api.retrieve_outputs(true, false, args.id)?;
display::outputs(&g_args.account, res.height, validated, outputs, dark_scheme)?;
// should only be one here, but just in case
for tx in txs {
display::tx_messages(&tx, dark_scheme)?;
for tx in &txs {
let (_, outputs) = api.retrieve_payments(true, tx.tx_slate_id)?;
if outputs.len() > 0 {
display::payments(
&g_args.account,
res.height,
validated,
outputs,
dark_scheme,
)?;
}
}
// should only be one here, but just in case
for tx in &txs {
display::tx_messages(tx, dark_scheme)?;
}
};
Ok(())

View file

@ -15,7 +15,8 @@
use crate::core::core::{self, amount_to_hr_string};
use crate::core::global;
use crate::libwallet::types::{
AcctPathMapping, OutputCommitMapping, OutputStatus, TxLogEntry, WalletInfo,
AcctPathMapping, OutputCommitMapping, OutputStatus, PaymentCommitMapping, TxLogEntry,
WalletInfo,
};
use crate::libwallet::Error;
use crate::util;
@ -119,6 +120,89 @@ pub fn outputs(
Ok(())
}
/// Display payments in a pretty way
pub fn payments(
account: &str,
cur_height: u64,
validated: bool,
outputs: Vec<PaymentCommitMapping>,
dark_background_color_scheme: bool,
) -> Result<(), Error> {
let title = format!(
"Wallet Payments - Account '{}' - Block Height: {}",
account, cur_height
);
println!();
let mut t = term::stdout().unwrap();
t.fg(term::color::MAGENTA).unwrap();
writeln!(t, "{}", title).unwrap();
t.reset().unwrap();
let mut table = table!();
table.set_titles(row![
bMG->"Output Commitment",
bMG->"Block Height",
bMG->"Locked Until",
bMG->"Status",
bMG->"# Confirms",
bMG->"Value",
bMG->"Shared Transaction Id"
]);
for payment in outputs {
let commit = format!("{}", util::to_hex(payment.commit.as_ref().to_vec()));
let out = payment.output;
let height = format!("{}", out.height);
let lock_height = format!("{}", out.lock_height);
let status = format!("{}", out.status);
let num_confirmations = format!("{}", out.num_confirmations(cur_height));
let value = if out.value == 0 {
"unknown".to_owned()
} else {
format!("{}", core::amount_to_hr_string(out.value, false))
};
let slate_id = format!("{}", out.slate_id);
if dark_background_color_scheme {
table.add_row(row![
bFC->commit,
bFB->height,
bFB->lock_height,
bFR->status,
bFB->num_confirmations,
bFG->value,
bFC->slate_id,
]);
} else {
table.add_row(row![
bFD->commit,
bFB->height,
bFB->lock_height,
bFR->status,
bFB->num_confirmations,
bFG->value,
bFD->slate_id,
]);
}
}
table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP);
table.printstd();
println!();
if !validated {
println!(
"\nWARNING: Wallet failed to verify data. \
The above is from local cache and possibly invalid! \
(is your `grin server` offline or broken?)"
);
}
Ok(())
}
/// Display transaction log in a pretty way
pub fn txs(
account: &str,

View file

@ -430,6 +430,16 @@ fn tx_rollback(test_dir: &str) -> Result<(), libwallet::Error> {
assert_eq!(output_mappings.len(), 3);
assert_eq!(locked_count, 2);
assert_eq!(unconfirmed_count, 1);
// check the payments are as expected
unconfirmed_count = 0;
let (_, payments) = api.retrieve_payments(false, tx.unwrap().tx_slate_id)?;
for p in &payments {
if p.output.status == OutputStatus::Unconfirmed {
unconfirmed_count = unconfirmed_count + 1;
}
}
assert_eq!(payments.len(), 1);
assert_eq!(unconfirmed_count, 1);
Ok(())
})?;
@ -450,6 +460,16 @@ fn tx_rollback(test_dir: &str) -> Result<(), libwallet::Error> {
}
assert_eq!(outputs.len(), 1);
assert_eq!(unconfirmed_count, 1);
// check the payments are as expected: receiver don't have this.
unconfirmed_count = 0;
let (_, payments) = api.retrieve_payments(false, tx.unwrap().tx_slate_id)?;
for p in &payments {
if p.output.status == OutputStatus::Unconfirmed {
unconfirmed_count = unconfirmed_count + 1;
}
}
assert_eq!(payments.len(), 0);
assert_eq!(unconfirmed_count, 0);
let (refreshed, wallet2_info) = api.retrieve_summary_info(true, 1)?;
assert!(refreshed);
assert_eq!(wallet2_info.amount_currently_spendable, 0,);

View file

@ -42,6 +42,8 @@ pub const DB_DIR: &'static str = "db";
pub const TX_SAVE_DIR: &'static str = "saved_txs";
const OUTPUT_PREFIX: u8 = 'o' as u8;
const PAYMENT_PREFIX: u8 = 'P' as u8;
const PAYMENT_COMMITS_PREFIX: u8 = 'Q' as u8;
const DERIV_PREFIX: u8 = 'd' as u8;
const CONFIRMED_HEIGHT_PREFIX: u8 = 'c' as u8;
const PRIVATE_TX_CONTEXT_PREFIX: u8 = 'p' as u8;
@ -238,6 +240,20 @@ where
Box::new(self.db.iter(&[OUTPUT_PREFIX]).unwrap())
}
fn get_payment_log_commits(&self, u: &Uuid) -> Result<Option<PaymentCommits>, Error> {
let key = to_key(PAYMENT_COMMITS_PREFIX, &mut u.as_bytes().to_vec());
self.db.get_ser(&key).map_err(|e| e.into())
}
fn get_payment_log_entry(&self, commit: String) -> Result<Option<PaymentData>, Error> {
let key = to_key(PAYMENT_PREFIX, &mut commit.as_bytes().to_vec());
self.db.get_ser(&key).map_err(|e| e.into())
}
fn payment_log_iter<'a>(&'a self) -> Box<dyn Iterator<Item = PaymentData> + 'a> {
Box::new(self.db.iter(&[PAYMENT_PREFIX]).unwrap())
}
fn get_tx_log_entry(&self, u: &Uuid) -> Result<Option<TxLogEntry>, Error> {
let key = to_key(TX_LOG_ENTRY_PREFIX, &mut u.as_bytes().to_vec());
self.db.get_ser(&key).map_err(|e| e.into())
@ -380,7 +396,7 @@ where
}
fn save(&mut self, out: OutputData) -> Result<(), Error> {
// Save the output data to the db.
// Save the self output data to the db.
{
let key = match out.mmr_index {
Some(i) => to_key_u64(OUTPUT_PREFIX, &mut out.key_id.to_bytes().to_vec(), i),
@ -392,6 +408,27 @@ where
Ok(())
}
fn save_payment_commits(&mut self, u: &Uuid, commits: PaymentCommits) -> Result<(), Error> {
// Save the payment commits list data to the db.
{
let key = to_key(PAYMENT_COMMITS_PREFIX, &mut u.as_bytes().to_vec());
self.db.borrow().as_ref().unwrap().put_ser(&key, &commits)?;
}
Ok(())
}
fn save_payment(&mut self, out: PaymentData) -> Result<(), Error> {
// Save the payment output data to the db.
{
let commit = out.commit.clone();
let key = to_key(PAYMENT_PREFIX, &mut commit.as_bytes().to_vec());
self.db.borrow().as_ref().unwrap().put_ser(&key, &out)?;
}
Ok(())
}
fn get(&self, id: &Identifier, mmr_index: &Option<u64>) -> Result<OutputData, Error> {
let key = match mmr_index {
Some(i) => to_key_u64(OUTPUT_PREFIX, &mut id.to_bytes().to_vec(), *i),
@ -404,6 +441,24 @@ where
.map_err(|e| e.into())
}
fn get_payment_commits(&self, u: &Uuid) -> Result<PaymentCommits, Error> {
let key = to_key(PAYMENT_COMMITS_PREFIX, &mut u.as_bytes().to_vec());
option_to_not_found(
self.db.borrow().as_ref().unwrap().get_ser(&key),
&format!("slate_id: {}", u.to_string()),
)
.map_err(|e| e.into())
}
fn get_payment_log_entry(&self, commit: String) -> Result<PaymentData, Error> {
let key = to_key(PAYMENT_PREFIX, &mut commit.as_bytes().to_vec());
option_to_not_found(
self.db.borrow().as_ref().unwrap().get_ser(&key),
&format!("key: {:?}", commit),
)
.map_err(|e| e.into())
}
fn iter(&self) -> Box<dyn Iterator<Item = OutputData>> {
Box::new(
self.db

View file

@ -43,6 +43,8 @@ pub const DB_DIR: &'static str = "db";
pub const TX_SAVE_DIR: &'static str = "saved_txs";
const OUTPUT_PREFIX: u8 = 'o' as u8;
const PAYMENT_PREFIX: u8 = 'P' as u8;
const PAYMENT_COMMITS_PREFIX: u8 = 'Q' as u8;
const DERIV_PREFIX: u8 = 'd' as u8;
const CONFIRMED_HEIGHT_PREFIX: u8 = 'c' as u8;
const PRIVATE_TX_CONTEXT_PREFIX: u8 = 'p' as u8;
@ -239,6 +241,20 @@ where
Box::new(self.db.iter(&[OUTPUT_PREFIX]).unwrap().map(|o| o.1))
}
fn get_payment_log_commits(&self, u: &Uuid) -> Result<Option<PaymentCommits>, Error> {
let key = to_key(PAYMENT_COMMITS_PREFIX, &mut u.as_bytes().to_vec());
self.db.get_ser(&key).map_err(|e| e.into())
}
fn get_payment_log_entry(&self, commit: String) -> Result<Option<PaymentData>, Error> {
let key = to_key(PAYMENT_PREFIX, &mut commit.as_bytes().to_vec());
self.db.get_ser(&key).map_err(|e| e.into())
}
fn payment_log_iter<'a>(&'a self) -> Box<dyn Iterator<Item = PaymentData> + 'a> {
Box::new(self.db.iter(&[PAYMENT_PREFIX]).unwrap().map(|o| o.1))
}
fn get_tx_log_entry(&self, u: &Uuid) -> Result<Option<TxLogEntry>, Error> {
let key = to_key(TX_LOG_ENTRY_PREFIX, &mut u.as_bytes().to_vec());
self.db.get_ser(&key).map_err(|e| e.into())
@ -386,7 +402,7 @@ where
}
fn save(&mut self, out: OutputData) -> Result<(), Error> {
// Save the output data to the db.
// Save the self output data to the db.
{
let key = match out.mmr_index {
Some(i) => to_key_u64(OUTPUT_PREFIX, &mut out.key_id.to_bytes().to_vec(), i),
@ -398,6 +414,27 @@ where
Ok(())
}
fn save_payment_commits(&mut self, u: &Uuid, commits: PaymentCommits) -> Result<(), Error> {
// Save the payment commits list data to the db.
{
let key = to_key(PAYMENT_COMMITS_PREFIX, &mut u.as_bytes().to_vec());
self.db.borrow().as_ref().unwrap().put_ser(&key, &commits)?;
}
Ok(())
}
fn save_payment(&mut self, out: PaymentData) -> Result<(), Error> {
// Save the payment output data to the db.
{
let commit = out.commit.clone();
let key = to_key(PAYMENT_PREFIX, &mut commit.as_bytes().to_vec());
self.db.borrow().as_ref().unwrap().put_ser(&key, &out)?;
}
Ok(())
}
fn get(&self, id: &Identifier, mmr_index: &Option<u64>) -> Result<OutputData, Error> {
let key = match mmr_index {
Some(i) => to_key_u64(OUTPUT_PREFIX, &mut id.to_bytes().to_vec(), *i),
@ -410,6 +447,24 @@ where
.map_err(|e| e.into())
}
fn get_payment_commits(&self, u: &Uuid) -> Result<PaymentCommits, Error> {
let key = to_key(PAYMENT_COMMITS_PREFIX, &mut u.as_bytes().to_vec());
option_to_not_found(
self.db.borrow().as_ref().unwrap().get_ser(&key),
&format!("slate_id: {}", u.to_string()),
)
.map_err(|e| e.into())
}
fn get_payment_log_entry(&self, commit: String) -> Result<PaymentData, Error> {
let key = to_key(PAYMENT_PREFIX, &mut commit.as_bytes().to_vec());
option_to_not_found(
self.db.borrow().as_ref().unwrap().get_ser(&key),
&format!("key: {:?}", commit),
)
.map_err(|e| e.into())
}
fn iter(&self) -> Box<dyn Iterator<Item = OutputData>> {
Box::new(
self.db

View file

@ -25,8 +25,8 @@ use crate::grin_keychain::{Identifier, Keychain};
use crate::internal::{keys, selection, tx, updater};
use crate::slate::Slate;
use crate::types::{
AcctPathMapping, InitTxArgs, NodeClient, NodeHeightResult, OutputCommitMapping, TxLogEntry,
TxWrapper, WalletBackend, WalletInfo,
AcctPathMapping, InitTxArgs, NodeClient, NodeHeightResult, OutputCommitMapping,
PaymentCommitMapping, TxLogEntry, TxWrapper, WalletBackend, WalletInfo,
};
use crate::{Error, ErrorKind};
@ -83,10 +83,29 @@ where
Ok((
validated,
updater::retrieve_outputs(&mut *w, include_spent, tx_id, Some(&parent_key_id))?,
updater::retrieve_outputs(&mut *w, include_spent, tx_id, None, Some(&parent_key_id))?,
))
}
/// Returns a list of payment outputs from the active account in the wallet.
pub fn retrieve_payments<T: ?Sized, C, K>(
w: &mut T,
refresh_from_node: bool,
tx_id: Option<Uuid>,
) -> Result<(bool, Vec<PaymentCommitMapping>), Error>
where
T: WalletBackend<C, K>,
C: NodeClient,
K: Keychain,
{
let mut validated = false;
if refresh_from_node {
validated = update_outputs(w, false);
}
Ok((validated, updater::retrieve_payments(w, tx_id)?))
}
/// Retrieve txs
pub fn retrieve_txs<T: ?Sized, C, K>(
w: &mut T,

View file

@ -215,6 +215,7 @@ where
lock_height: output.lock_height,
is_coinbase: output.is_coinbase,
tx_log_entry: Some(log_id),
slate_id: None,
});
let max_child_index = found_parents.get(&parent_key_id).unwrap().clone();
@ -283,7 +284,7 @@ where
// Now, get all outputs owned by this wallet (regardless of account)
let wallet_outputs = {
let res = updater::retrieve_outputs(&mut *wallet, true, None, None)?;
let res = updater::retrieve_outputs(&mut *wallet, true, None, None, None)?;
res
};

View file

@ -155,6 +155,7 @@ where
lock_height: 0,
is_coinbase: false,
tx_log_entry: Some(log_id),
slate_id: Some(slate_id.clone()),
})?;
}
batch.save_tx_log_entry(t.clone(), &parent_key_id)?;
@ -224,6 +225,7 @@ where
lock_height: 0,
is_coinbase: false,
tx_log_entry: Some(log_id),
slate_id: Some(slate_id),
})?;
batch.save_tx_log_entry(t, &parent_key_id)?;
batch.commit()?;

View file

@ -17,10 +17,14 @@
use uuid::Uuid;
use crate::grin_keychain::{Identifier, Keychain};
use crate::grin_util::secp::pedersen;
use crate::grin_util::to_hex;
use crate::grin_util::Mutex;
use crate::internal::{selection, updater};
use crate::slate::Slate;
use crate::types::{Context, NodeClient, TxLogEntryType, WalletBackend};
use crate::types::{
Context, NodeClient, OutputStatus, PaymentCommits, PaymentData, TxLogEntryType, WalletBackend,
};
use crate::{Error, ErrorKind};
/// static for incrementing test UUIDs
@ -220,6 +224,63 @@ where
)?;
// Final transaction can be built by anyone at this stage
slate.finalize(wallet.keychain())?;
let parent_key_id = Some(&context.parent_key_id);
// Get the change output/s from database
let changes = updater::retrieve_outputs(wallet, false, None, Some(slate.id), parent_key_id)?;
let change_commits = changes
.iter()
.map(|oc| oc.commit.clone())
.collect::<Vec<pedersen::Commitment>>();
// Find the payment output/s
let mut outputs: Vec<pedersen::Commitment> = Vec::new();
for output in slate.tx.outputs() {
if !change_commits.contains(&output.commit) {
outputs.insert(0, output.commit.clone());
}
}
// sender save the payment output
let mut batch = wallet.batch()?;
batch.save_payment_commits(
&slate.id,
PaymentCommits {
commits: outputs
.iter()
.map(|c| to_hex(c.as_ref().to_vec()))
.collect::<Vec<String>>(),
slate_id: slate.id,
},
)?;
// todo: multiple receivers transaction
if outputs.len() > 1 {
for output in outputs {
let payment_output = to_hex(output.clone().as_ref().to_vec());
batch.save_payment(PaymentData {
commit: payment_output,
value: 0, // '0' means unknown here, since '0' value is impossible for an output.
status: OutputStatus::Unconfirmed,
height: slate.height,
lock_height: 0,
slate_id: slate.id,
})?;
}
} else if outputs.len() == 1 {
let payment_output = to_hex(outputs.first().clone().unwrap().as_ref().to_vec());
batch.save_payment(PaymentData {
commit: payment_output,
value: slate.amount,
status: OutputStatus::Unconfirmed,
height: slate.height,
lock_height: 0,
slate_id: slate.id,
})?;
} else {
warn!("complete_tx - no 'payment' output! is this a sending to self for test purpose?");
}
batch.commit()?;
Ok(())
}
@ -253,7 +314,7 @@ where
return Err(ErrorKind::TransactionNotCancellable(tx_id_string))?;
}
// get outputs associated with tx
let res = updater::retrieve_outputs(wallet, false, Some(tx.id), Some(&parent_key_id))?;
let res = updater::retrieve_outputs(wallet, false, Some(tx.id), None, Some(&parent_key_id))?;
let outputs = res.iter().map(|m| m.output.clone()).collect();
updater::cancel_tx_and_outputs(wallet, tx, outputs, parent_key_id)?;
Ok(())

View file

@ -28,15 +28,16 @@ use crate::grin_util as util;
use crate::grin_util::secp::pedersen;
use crate::internal::keys;
use crate::types::{
BlockFees, CbData, NodeClient, OutputCommitMapping, OutputData, OutputStatus, TxLogEntry,
TxLogEntryType, WalletBackend, WalletInfo,
BlockFees, CbData, NodeClient, OutputCommitMapping, OutputData, OutputStatus,
PaymentCommitMapping, TxLogEntry, TxLogEntryType, WalletBackend, WalletInfo,
};
/// Retrieve all of the outputs (doesn't attempt to update from node)
/// Retrieve all of the self outputs (doesn't attempt to update from node)
pub fn retrieve_outputs<T: ?Sized, C, K>(
wallet: &mut T,
show_spent: bool,
tx_id: Option<u32>,
slate_id: Option<Uuid>,
parent_key_id: Option<&Identifier>,
) -> Result<Vec<OutputCommitMapping>, Error>
where
@ -58,6 +59,14 @@ where
.collect::<Vec<_>>();
}
// only include outputs with a given slate_id if provided
if let Some(id) = slate_id {
outputs = outputs
.into_iter()
.filter(|out| out.slate_id == Some(id))
.collect::<Vec<_>>();
}
if let Some(k) = parent_key_id {
outputs = outputs
.iter()
@ -82,6 +91,38 @@ where
Ok(res)
}
/// Retrieve all of the payment outputs (doesn't attempt to update from node)
pub fn retrieve_payments<T: ?Sized, C, K>(
wallet: &mut T,
tx_id: Option<Uuid>,
) -> Result<Vec<PaymentCommitMapping>, Error>
where
T: WalletBackend<C, K>,
C: NodeClient,
K: Keychain,
{
// just read the wallet here, no need for a write lock
let mut outputs = wallet.payment_log_iter().collect::<Vec<_>>();
// only include outputs with a given tx_id if provided
if let Some(id) = tx_id {
outputs = outputs
.into_iter()
.filter(|out| out.slate_id == id)
.collect::<Vec<_>>();
}
let res = outputs
.into_iter()
.map(|output| {
let commit =
pedersen::Commitment::from_vec(util::from_hex(output.commit.clone()).unwrap());
PaymentCommitMapping { output, commit }
})
.collect();
Ok(res)
}
/// Retrieve all of the transaction entries, or a particular entry
/// if `parent_key_id` is set, only return entries from that key
pub fn retrieve_txs<T: ?Sized, C, K>(
@ -291,6 +332,21 @@ where
t.confirmed = true;
batch.save_tx_log_entry(t, &parent_key_id)?;
}
// if there's a related payment output being confirmed, refresh that payment log
if let Some(slate_id) = output.slate_id {
if let Ok(commits) = batch.get_payment_commits(&slate_id) {
for commit in commits.commits {
if let Ok(mut payment) =
batch.get_payment_log_entry(commit.clone())
{
payment.height = o.1;
payment.mark_confirmed();
batch.save_payment(payment)?;
}
}
}
}
}
output.height = o.1;
output.mark_unspent();
@ -413,6 +469,7 @@ where
locked_total += out.value;
}
OutputStatus::Spent => {}
OutputStatus::Confirmed => {}
}
}
@ -490,6 +547,7 @@ where
lock_height: lock_height,
is_coinbase: true,
tx_log_entry: None,
slate_id: None,
})?;
batch.commit()?;
}

View file

@ -85,10 +85,19 @@ where
/// return the parent path
fn parent_key_id(&mut self) -> Identifier;
/// Iterate over all output data stored by the backend
/// Iterate over all self output data stored by the backend
fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = OutputData> + 'a>;
/// Get output data by id
/// Get payment commits list by slate id
fn get_payment_log_commits(&self, u: &Uuid) -> Result<Option<PaymentCommits>, Error>;
/// Get payment output data by commit
fn get_payment_log_entry(&self, commit: String) -> Result<Option<PaymentData>, Error>;
/// Iterate over all payment output data stored by the backend
fn payment_log_iter<'a>(&'a self) -> Box<dyn Iterator<Item = PaymentData> + 'a>;
/// Get self owned output data by id
fn get(&self, id: &Identifier, mmr_index: &Option<u64>) -> Result<OutputData, Error>;
/// Get an (Optional) tx log entry by uuid
@ -140,12 +149,24 @@ where
/// Return the keychain being used
fn keychain(&mut self) -> &mut K;
/// Add or update data about an output to the backend
/// Add or update data about a self owned output to the backend
fn save(&mut self, out: OutputData) -> Result<(), Error>;
/// Gets output data by id
/// Add or update data about a payment output to the backend
fn save_payment(&mut self, out: PaymentData) -> Result<(), Error>;
/// Add or update data about a payment commits list to the backend
fn save_payment_commits(&mut self, u: &Uuid, commits: PaymentCommits) -> Result<(), Error>;
/// Gets self owned output data by id
fn get(&self, id: &Identifier, mmr_index: &Option<u64>) -> Result<OutputData, Error>;
/// Gets payment commits list by slate id
fn get_payment_commits(&self, u: &Uuid) -> Result<PaymentCommits, Error>;
/// Gets payment output data by commit
fn get_payment_log_entry(&self, commit: String) -> Result<PaymentData, Error>;
/// Iterate over all output data stored by the backend
fn iter(&self) -> Box<dyn Iterator<Item = OutputData>>;
@ -270,6 +291,8 @@ pub struct OutputData {
pub is_coinbase: bool,
/// Optional corresponding internal entry in tx entry log
pub tx_log_entry: Option<u32>,
/// Unique transaction ID, selected by sender
pub slate_id: Option<Uuid>,
}
impl ser::Writeable for OutputData {
@ -346,6 +369,94 @@ impl OutputData {
}
}
}
/// Information about a payment output that's being tracked by the wallet.
/// It belongs to the receiver, and it's paid by this wallet.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd, Eq, Ord)]
pub struct PaymentData {
/// The actual commit
pub commit: String,
/// Value of the output
pub value: u64,
/// Current status of the output
pub status: OutputStatus,
/// Height of the output
pub height: u64,
/// Height we are locked until
pub lock_height: u64,
/// Unique transaction ID, selected by sender
pub slate_id: Uuid,
}
impl ser::Writeable for PaymentData {
fn write<W: ser::Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?)
}
}
impl ser::Readable for PaymentData {
fn read(reader: &mut dyn ser::Reader) -> Result<PaymentData, ser::Error> {
let data = reader.read_bytes_len_prefix()?;
serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData)
}
}
impl PaymentData {
/// How many confirmations has this output received?
/// If height == 0 then we are either Unconfirmed or the output was
/// cut-through
/// so we do not actually know how many confirmations this output had (and
/// never will).
pub fn num_confirmations(&self, current_height: u64) -> u64 {
if self.height > current_height {
return 0;
}
if self.status == OutputStatus::Unconfirmed {
0
} else {
// if an output has height n and we are at block n
// then we have a single confirmation (the block it originated in)
1 + (current_height - self.height)
}
}
/// Marks this output as confirmed if it was previously unconfirmed
pub fn mark_confirmed(&mut self) {
match self.status {
OutputStatus::Unconfirmed => self.status = OutputStatus::Confirmed,
_ => (),
}
}
}
/// Information about the payment commit/s in one tx that's being tracked by the wallet.
/// They belong to the receiver/s, and they're paid by this wallet.
///
/// Note: because lmdb can't have multiple keys to same value, we have to use this to find
/// the commit lists by the slate id, in case we support multiple receivers in one tx in the future.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PaymentCommits {
/// The actual commit/s
pub commits: Vec<String>,
/// Unique transaction ID, selected by sender
pub slate_id: Uuid,
}
impl ser::Writeable for PaymentCommits {
fn write<W: ser::Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?)
}
}
impl ser::Readable for PaymentCommits {
fn read(reader: &mut dyn ser::Reader) -> Result<PaymentCommits, ser::Error> {
let data = reader.read_bytes_len_prefix()?;
serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData)
}
}
/// Status of an output that's being tracked by the wallet. Can either be
/// unconfirmed, spent, unspent, or locked (when it's been used to generate
/// a transaction but we don't have confirmation that the transaction was
@ -360,6 +471,8 @@ pub enum OutputStatus {
Locked,
/// Spent
Spent,
/// Confirmed
Confirmed,
}
impl fmt::Display for OutputStatus {
@ -369,6 +482,7 @@ impl fmt::Display for OutputStatus {
OutputStatus::Unspent => write!(f, "Unspent"),
OutputStatus::Locked => write!(f, "Locked"),
OutputStatus::Spent => write!(f, "Spent"),
OutputStatus::Confirmed => write!(f, "Confirmed"),
}
}
}
@ -853,6 +967,19 @@ pub struct OutputCommitMapping {
pub commit: pedersen::Commitment,
}
/// Map PaymentData to commits
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PaymentCommitMapping {
/// Payment Data
pub output: PaymentData,
/// The commit
#[serde(
serialize_with = "secp_ser::as_hex",
deserialize_with = "secp_ser::commitment_from_hex"
)]
pub commit: pedersen::Commitment,
}
/// Node height result
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct NodeHeightResult {

View file

@ -624,6 +624,14 @@ pub fn wallet_command(
&global_wallet_args,
wallet_config.dark_background_color_scheme.unwrap_or(true),
),
("payments", Some(_)) => {
info!("payments command received");
command::payments(
inst_wallet(),
&global_wallet_args,
wallet_config.dark_background_color_scheme.unwrap_or(true),
)
}
("txs", Some(args)) => {
let a = arg_parse!(parse_txs_args(&args));
command::txs(

View file

@ -501,6 +501,10 @@ mod wallet_tests {
let arg_vec = vec!["grin-wallet", "-p", "password", "-a", "mining", "outputs"];
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
// payments
let arg_vec = vec!["grin-wallet", "-p", "password", "-a", "mining", "payments"];
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
// let logging finish
thread::sleep(Duration::from_millis(200));
Ok(())

View file

@ -163,7 +163,9 @@ subcommands:
short: f
long: fluff
- outputs:
about: Raw wallet output info (list of outputs)
about: Raw wallet self owned output info (list of outputs)
- payments:
about: Raw wallet payment output info (list of outputs)
- txs:
about: Display transaction information
args: