Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transaction pagination + full query arguments #666

Merged
merged 11 commits into from
Dec 6, 2022

Conversation

yeastplume
Copy link
Member

@yeastplume yeastplume commented Nov 22, 2022

  • Addition of new structs in libwallet internal types, RetrieveTxQueryArgs, RetrieveTxQuerySortOrder, RetrieveTxQuerySortField
  • Update internal retrieve_txs API function to optionally accept a RetrieveTxQueryArgs struct
  • Implementation of filtering via apply_advanced_tx_list_filtering fn
  • Modify retrieve_tx implementation to use advanced filfering function if optional query args are supplied
  • Addition of unit tests to exercise all new query functionality and sorting
  • Add new JSON API Function query_txs, (note it was necessary to provide a separate function to avoid breaking existing json calls to retrieve_txs
  • Documentation and Doctesting additions for JSON API
  • Note that advanced query functionality is API only and not accessible from the command-line

While creating https://github.com/mimblewimble/grin-gui, it's become apparent that we need to implement much more granular transaction querying to better support pagination, sorting, etc. Instead of just implementing basic pagination, I'm taking the opportunity to add a reasonable number of advanced query options via a new query structure.

Details posted here for review and comment, I think this should cover most needs but happy to hear from anyone with suggestions or anything else that should be included in the query arguments:

pub struct RetrieveTxQueryArgs {
	/// Retrieve transactions with an id higher than or equal to the given
	/// If None, consider items from the first transaction and later
	pub min_id: Option<u32>,
	/// Retrieve tranactions with an id less than or equal to the given
	/// If None, consider items from the last transaction and earlier
	pub max_id: Option<u32>,
	/// The maximum number of transactions to return
	/// if both `before_id` and `after_id` are supplied, this will apply
	/// to the before and earlier set
	pub limit: Option<u32>,
	/// whether to exclude cancelled transactions in the returned set
	pub exclude_cancelled: Option<bool>,
	/// whether to only consider outstanding transactions
	pub include_outstanding_only: Option<bool>,
	/// whether to only consider confirmed-only transactions
	pub include_confirmed_only: Option<bool>,
	/// whether to only consider sent transactions
	pub include_sent_only: Option<bool>,
	/// whether to only consider received transactions
	pub include_received_only: Option<bool>,
	/// whether to only consider coinbase transactions
	pub include_coinbase_only: Option<bool>,
	/// whether to only consider reverted transactions
	pub include_reverted_only: Option<bool>,
	/// lower bound on the total amount (amount_credited - amount_debited), inclusive
	#[serde(with = "secp_ser::opt_string_or_u64")]
	#[serde(default)]
	pub min_amount: Option<u64>,
	/// higher bound on the total amount (amount_credited - amount_debited), inclusive
	#[serde(with = "secp_ser::opt_string_or_u64")]
	#[serde(default)]
	pub max_amount: Option<u64>,
	/// lower bound on the creation timestamp, inclusive
	pub min_creation_timestamp: Option<DateTime<Utc>>,
	/// higher bound on on the creation timestamp, inclusive
	pub max_creation_timestamp: Option<DateTime<Utc>>,
	/// lower bound on the confirmation timestamp, inclusive
	pub min_confirmed_timestamp: Option<DateTime<Utc>>,
	/// higher bound on the confirmation timestamp, inclusive
	pub max_confirmed_timestamp: Option<DateTime<Utc>>,
	/// Field within the tranasction list on which to sort
	/// defaults to ID if not present
	pub sort_field: Option<RetrieveTxQuerySortField>,
	/// Sort order, defaults to ASC if not present (earliest is first)
	pub sort_order: Option<RetrieveTxQuerySortOrder>,
}

Sort fields (and sort order) are:

/// Sort tx retrieval order
#[derive(Clone, Serialize, Deserialize)]
pub enum RetrieveTxQuerySortOrder {
	/// Ascending
	Asc,
	/// Descending
	Desc,
}

/// Valid sort fields for a transaction list retrieval query
#[derive(Clone, Serialize, Deserialize)]
pub enum RetrieveTxQuerySortField {
	/// Transaction Id
	Id,
	/// Creation Timestamp
	CreationTimestamp,
	/// Confirmation Timestamp
	ConfirmationTimestamp,
	/// TotalAmount (AmountCredited-AmountDebited)
	TotalAmount,
	/// Amount Credited
	AmountCredited,
	/// Amount Debited
	AmountDebited,
}

@yeastplume yeastplume changed the title [WIP] Transaction pagination + full query arguments Transaction pagination + full query arguments Nov 29, 2022
@yeastplume
Copy link
Member Author

This is now ready for review, changed original comments to reflect changes

/// to the before and earlier set
pub limit: Option<u32>,
/// whether to exclude cancelled transactions in the returned set
pub exclude_cancelled: Option<bool>,
Copy link
Member

@phyro phyro Dec 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific reason why we wrap all bools into options? Would removing these simplify the logical branching in the filtering code?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if they weren't options, you'd need to specify each and every one when passing in the struct from JSON, this allows json callers to leave them out

/// lower bound on the total amount (amount_credited - amount_debited), inclusive
#[serde(with = "secp_ser::opt_string_or_u64")]
#[serde(default)]
pub min_amount_inc: Option<u64>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably makes sense to remove the _inc here as well since the max_amount doesn't have it either

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missed that, thank you

.collect();
txs.sort_by_key(|tx| tx.creation_ts);
let mut txs;
// Adding in new tranasction list query logic. If `tx_id` or `tx_slate_id`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: typo tranasction

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

let mut txs;
// Adding in new tranasction list query logic. If `tx_id` or `tx_slate_id`
// is provided, then `query_args` is ignored and old logic is followed.
if query_args.is_some() && tx_id.is_none() && tx_slate_id.is_none() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FUTURE: Maybe we could consider returning an error if query_args is provided as well as a tx_id or slate_id i.e. you might filter to exclude cancelled txs, but the tx_id is a cancelled tx and will thus be returned.


let mut return_txs: Vec<TxLogEntry> = txs_iter.collect();

// Now apply requested sorting
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should apply sorting before we limit otherwise we end up with what I would consider an incorrect logic i.e. we sort things by credited amount, but only after we have cut off most of the txs meaning we won't get the most credited txs.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I'm with you here, sorting the entire data set on every query is very inefficient and should be avoided, I think. Can you give an example of where you'd get incorrect logic here? If you're interested in credited amount, you filter for it then optionally sort the results.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this is slower because the sorting takes place on a larger sequence. Here's an example of what I had in mind. Let's say I have 60 transactions in my transaction log. The first 20 entries have amount credited +1, next 20 entries have amount credited +20 and the last 20 entries have amount credited +10.

I make the following call

api.query_txs(limit=10, sort_order="Desc", sort_field="AmountCredited")

I would assume this returns the top10 transactions with the largest credited amount.

My understanding is that the limit will be applied on a sequence of all tx logs in this case and will take only the last 10 from the sequence (likely take the last 10 transactions) and then sort these. The result returned will be 10 transactions with amount credited +10. The user would probably expect to get 10 transactions with +20 instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I'm with you now. Good catch, I've updated the logic to apply limiting after sorting.

Copy link
Member

@phyro phyro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job! I played a bit with the python api call wrapper by adding the following:

    def query_txs(
            self, min_id=None, max_id=None, limit=None, exclude_cancelled=None, include_outstanding_only=None,
            include_confirmed_only=None, include_sent_only=None, include_received_only=None,
            include_coinbase_only=None, include_reverted_only=None,min_amount=None, max_amount=None,
            min_creation_ts=None, max_creation_ts=None, min_confirmed_ts=None, max_confirmed_ts=None,
            sort_field=None, sort_order=None, refresh=True
    ):
        params = {
            'token': self.token,
            'refresh_from_node': refresh,
            'query': {
                "min_id": min_id,
                "max_id": max_id,
                "limit": limit,
                "exclude_cancelled": exclude_cancelled,
                "include_outstanding_only": include_outstanding_only,
                "include_confirmed_only": include_confirmed_only,
                "include_sent_only": include_sent_only,
                "include_received_only": include_received_only,
                "include_coinbase_only": include_coinbase_only,
                "include_reverted_only": include_reverted_only,
                "min_amount_inc": min_amount,
                "max_amount": max_amount,
                "min_creation_timestamp": min_creation_ts,
                "max_creation_timestamp": max_creation_ts,
                "min_confirmed_timestamp": min_confirmed_ts,
                "max_confirmed_timestamp": max_confirmed_ts,
                "sort_field": sort_field,
                "sort_order": sort_order,
            },
        }
        resp = self.post_encrypted('query_txs', params)
        if refresh and not resp["result"]["Ok"][0]:
            # We requested refresh but data was not successfully refreshed
            raise WalletError("query_txs", params, None, "Failed to refresh data from the node")
        return resp["result"]["Ok"][1]


print([x["id"] for x in wallet_api.query_txs(min_id=100, max_id=200, limit=10, sort_order="Desc", sort_field="AmountCredited")])

I didn't manage to test all the fields, but from what I found, I'd say only the last comment is more relevant.

Copy link
Member

@phyro phyro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything has been addressed 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants