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

[WIP] add functions to wallet API #9464

Open
wants to merge 31 commits into
base: master
Choose a base branch
from

Conversation

SNeedlewoods
Copy link

Do not merge, this is still in development and just PRd for easier discussion.

This is an attempt to make the wallet API "feature complete" and it's part of this proposal.

The approach was to search for every wallet2 function in simplewallet and wallet_rpc_server, if it is used in either of those, but is not implemented in the API I considered it missing and it was added in this PR.
I believe wallet2_api.h now contains all missing functions, but some are not implemented yet. I'm waiting on feedback on those, if you want to have a look, things I need help with are marked as QUESTION, you can also comment on things marked TODO (those may become QUESTIONs if I'm unable(/unsure how) to solve them) or anything else.

Also gathered this collection of (mostly minor) issues found during API work.

@rbrunner7
Copy link
Contributor

As we can see from the table above, there are quite some things that wallet2 provides that are missing from current Wallet API. The additions will therefore be quite substantial.

I think we should not just rush headlong into adding those many things. After all, if we are successful, this API will be very important, will live for a long time, and will be used by many people. If the new resulting interface is not well designed, it will haunt us for years to come.

Problem is that there are too many ways how you could design it, it's not "naturally clear" what types and methods there have to be. My feeling is that you could do almost everything in a hundred different ways.

To contribute something useful in these quite difficult circumstances I try to come up with some general rules to follow when defining the API that will hopefully guide us towards a good solution.

@rbrunner7
Copy link
Contributor

Make it complete

This rule is pretty self-evident and clear, but I repeat it here nevertheless in the sake of completeness.

Goal is to provide a viable successor to the "API" that the public things in wallet2.h provide and eventually be able to completely remove that header file from the codebase. Each and every user of it, e.g. the CLI wallet and the wallet RPC server, will have to switch and use the Wallet API exclusivelly.

Because of this, we have to make sure that the Wallet API is feature complete. We must not lose any wallet functionality that is needed and in actual use.

@rbrunner7
Copy link
Contributor

Make it small

We should try, while respecting the first rule of completeness of course, to make the API small. The smaller it is, the faster will new devs be able to read into it, the easier it will be to find the right methods to use, the less work to document it, etc.

Where do we have potential to reduce the number of methods? One way I see is this:

Do not include methods from wallet2.h that would be trivial to implement by clients using other methods in the interface themselves with only a few lines of code. I would speak of essential methods, that being methods that do provide something that is not available through any other method and that clients can't build themselves because of this. So, include only such essential methods.

I have mentioned an example of such method in this earlier issue about wallet related methods: There is a variant of wallet2::get_payments that filters for payment ID. This method is not "essential" in the sense that I propose here because clients can easily use the general get_payments method and search themselves.

Of course use common sense: It may be essential that already the code of the Wallet API filters or sorts something because otherwise too much data must get transferred, or doing things in the client would be much, much slower.

@rbrunner7
Copy link
Contributor

Be careful about terminology

A good API uses a consistent terminology when naming things, and uses terms that are "expressive", hard to misunderstand and hard to confuse.

IMHO quite a number of things in wallet2 have sub-optimal names, maybe because not enough importance was given to terminology when originally naming things. Prime example for me is the mixup of the terms transaction, payment and transfer. I detailed some examples in this already mentioned issue.

This motivated me to propose to ban the term transfer outright exactly because it's so easy to misunderstand and easy to confuse. I detailed my proposal here.

Of course many things in the Wallet API already have the names they have, and we must let them stand and are not completely free how to name the new things we add. Still I see quite a degree of freedom left.

@rbrunner7
Copy link
Contributor

Limit the influence of existing clients

We have 2 main clients of wallet2.h in the existing codebase: The CLI wallet and the wallet RPC server. According to the plan we will have to migrate them to the Wallet API.

Of course, striclty only seen from the point of view of that migration, the more similar the Wallet API is to the "old" wallet2.h, the less work it will take. The question arises: It this important, and is this something that should guide us when designing the new API?

I propose "No" as answer. I think having a good API is much more important than trying to optimize towards migration work on the CLI wallet and the wallet RPC server being small. That migration happens once and is then over for all times, whereas the Wallet API will go indefinitely into the future and (hopefully) shape tons of Monero related software that together trump any migration work.

In detail that means we don't try to give each method of wallet2.h a more or less one-to-one counterpart in the parts we add to the Wallet API. We mostly design the API independently, following the other rules that I propose here, and then migrate.

@rbrunner7
Copy link
Contributor

Keep the style consistent

I write this down more for the sake of completeness, like the very first rule Make it complete:

There is a clash in naming style between the Wallet API and most of the rest of the Monero codebase: The first uses casing to form names like TransactionInfo and getUnlockedBalance while the rest uses "snake casing", like so: device_derivation_path_option, unconfirmed_transfer_details.

For the things we add we basically have two choices: We keep the naming style consistent with the things that are already there in the Wallet API, or we use the snake casing that is much more widely used in the codebase. Both choices are well possible, and both have advantages as well as disadvantages. (Renaming existing things as a third option and a way to achieve overal naming style consistency is probably not on the table.)

We dicussed this in a Seraphis wallet workgroup meeting, and it seemed that more people voted for keeping the style consistent. I myself was also in that camp: To continue with the "non-standard" naming style is IMHO the lesser evil than mixing styles within a single API.

@rbrunner7
Copy link
Contributor

Now my opinion about one the more important design questions:

wallet2.h has a number of separate methods to get various types of payments / transactions, e.g. get_payments (incoming confirmed payments), get_unconfirmed_payments (incoming unconfirmed payments), get_payments_out (outgoing confirmed transactions), and get_unconfirmed_payments_out (outgoing unconfirmed transactions).

As I explained here, neither the Wallet API, nor the RPC interface, nor Woodser's wallet API have such separate calls. They basically work with a single call that get payments / transactions of all types, using a struct that is "encompassing" i.e. is able to store all details of all these types. I hope I got that right, the Wallet API call for this seems to be history.

According to the rules Make it small and Limit the influence of existing clients I propose to continue with this one call and not create direct equivalents of these methods in the Wallet API.

Probably the type TransactionInfo needs some extensions to be able to give back every last bit of info that all those wallet2 methods provide, but I see this as a quite natural extension of the interface.

virtual bool isFrozen(const std::string multisig_sign_data) const = 0;
// QUESTION : Should we add the following function, which returns strings containing the key image for every frozen enote? I think it can be useful
// virtual std::vector<std::string> getAllFrozenEnotes() const = 0;
// TODO : Would this better fit in one of `Subaddress`/`SubaddressAccount` classes?
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you give some possible arguments for pushing this down to the account level (if I understand you correctly)? I don't see any right now, but you have probably give this much more thought than I :)

Copy link
Author

Choose a reason for hiding this comment

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

Quick update:
There is no need to move createOneOffSubaddress to the account level. Since this is an exotic function, maybe we should add a note/warning that this function should only be used in special cases?
(moneromoo: "It was intended to help people who generated a subaddress far into the random index values.")
e.g. something like:

* brief: createOneOffSubaddress - Create a new account or subaddress for given index, without adding it to known accounts and subaddresses.
*                                 Use `SubaddressAccount::addRow()` or `Subaddress::addRow()` instead, to make sure the wallet adds accounts/subaddresses consecutively and keeps track of accounts and subaddresses.

WalletImpl::addSubaddress & WalletImpl::addSubaddressAccount also do not refresh the known accounts/subaddresses, if that's not for a good reason (that I don't see) I would either add refresh at the end of those functions, or at least give the same warning as proposed above.
Seems the GUI only uses the addRow functions (see here) not the addSubaddress* ones (see here).

* brief: getTransfers - get all transfers
* outparam: transfers -
*/
// virtual void getTransfers(wallet2::transfer_container& transfers) const = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

Here we probably enter a whole region of things that we won't need in this form if my proposal to make a single encompassing "get payments / transactions" methods really flies.

* brief: getMinRingSize - get the minimum allowed ring size
* return: min ring size
*/
virtual std::uint64_t getMinRingSize() const = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can go. We don't have the possibility to choose ringsize anymore for years, and well, soon we won't have any rings at all. Maximum that I see here is an info "ring size" in the new WalletState.

Copy link
Contributor

Choose a reason for hiding this comment

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

Would require something like this: tobtoht@894adf0. Can PR.

* messages and prompts. When it finally calls this method here "to catch up" so to say we can't use
* incremental update anymore, because with that we might miss some txs altogether.
*/
virtual void updatePoolState(std::vector<std::tuple<cryptonote::transaction, std::string, bool>> &process_txs, bool refreshed = false, bool try_incremental = false) = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure yet what possibilities of "pool info update" the Wallet API has to offer, or should offer, but I am pretty sure that this method in this form is on too low a level of abstraction, like the following processPoolState as well.

Copy link
Author

Choose a reason for hiding this comment

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

This is too complex for me right now to fully dive into, maybe someone with more experience in this area can comment?

Copy link
Contributor

Choose a reason for hiding this comment

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

I try to follow the general principle of YAGNI, especially when making APIs. Don't add it unless someone asks for it. A use case for processing abstractions of the pool state might pop up in the future at some point, but it also might not. Until then, these functions are internal business logic and shouldn't be exposed to the API.

Copy link
Author

Choose a reason for hiding this comment

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

If we don't add them to the API, how would the wallet-rpc & wallet-cli call this function, if the goal is to get rid of all the wallet2 function calls? Sorry if I'm missing something.

Copy link
Contributor

Choose a reason for hiding this comment

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

I checked, and it's used indeed, so it's not a case of YAGNI like @jeffro256 suspected, and after I saw that @moneromooo-monero fixed an information leak a few years back when using remote daemons where somehow this method and the next for processing the pool state were involved, I think it's best to just add those to the interface and keep them "public".

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that the function update_pool_state, with process_pool_state are both leaky abstractions as written. Both times they are used outside of wallet2, they are called like so:

std::vector<std::tuple<cryptonote::transaction, crypto::hash, bool>> process_txs;
m_wallet->update_pool_state(process_txs);
if (!process_txs.empty())
  m_wallet->process_pool_state(process_txs);

This sequence of statements basically means "rescan the pool" and IMO should be abstracted into a method as such. Calling update_pool_state without an immediate corresponding call to process_pool_state will almost always be a mistake, as this will update the fetching marker without actually scanning the transactions and getting up-to-date information about your balance. So I'd add a method to the Wallet interface called something like refreshPoolOnly which calls the above statements, and drop both updatePoolState and processPoolState from the interface.

* return: true if succeeded
* note: sets status error on fail
*/
virtual bool saveMultisigTx(const PendingTransaction &multisig_ptx, const std::string &filename) const = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder whether it would make sense to group all the multisig related methods together in the source, and not e.g. put non-multisig and multisig versions of methods that do similar things together. Thus, "write normal tx to file" and "write multisig transaction to file" would not be next to each other, but the first in the group of "normal" methods and the second in the multisig method group.

My argument: Many wallet apps will probably continue to not offer multisig, and their devs may welcome that all those multisig methods do not "clutter" the API for them.

There also have been thoughts that ideally, "multisig wallet" would be a completely separate wallet type, with its own API, and maybe even with the code in a separate repository. That multisig stuff is so highly special, so different, and so complicated ...

I also ask myself wether we can't make some of such methods "smart" anyway so they decide themselves what there is to do, based on internal knowledge whether a transaction is multisig or not.

Copy link
Author

Choose a reason for hiding this comment

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

In regards to grouping multisig stuff together, that's already done in another PR (can be seen e.g. here or here). If I remember correctly for this PR I was asked to add everything new in one place, if the "organize functions" get merged this can get based onto that (actually there is still another one for in between in the pipeline I didn't PR yet, the "add comments" found here).

A separate multisig wallet really sounds ideal, not sure how many clients currently use the multisig functionality from the API, the GUI afaik doesn't. But that's out of scope for now I'd say!?

I'll look into if I can come up with such "smart" methods.

* brief: getAccountTags - get the list of registered account tags
* return: first.Key=(tag's name), first.Value=(tag's label), second[i]=(i-th account's tag)
*/
virtual const std::pair<std::map<std::string, std::string>, std::vector<std::string>>& getAccountTags() = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems to me, if we "go with the pattern", we have a method somewhere that gives back all existing accounts with info, using an account_info struct. The tag would be part of that struct, and thus this call here not needed because not "essential".

Or maybe a call giving back all subaddresses, with info about them, and info how they group into accounts - and any tags of course.

Methods like the following setAccountTag stay of course even if we use this pattern whenever we can, because changing things is something fundamentally different and altogether impossible if there is no method, after all.

Copy link
Author

Choose a reason for hiding this comment

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

I think that tags would fit into SubaddressAccountRow, but returning the "i-th account's tag" is just one part of this function, it also returns all tag descriptions (used by wallet-cli and wallet-rpc). So we probably need at least a getAccountTagDescriptions() method anyways, or alternatively we could store the tag description in SubaddressAccountRow, too.

Copy link
Contributor

@jeffro256 jeffro256 left a comment

Choose a reason for hiding this comment

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

This is my updated opinion about the update_pool_state/process_pool_state issue

src/wallet/api/wallet2_api.h Outdated Show resolved Hide resolved
* messages and prompts. When it finally calls this method here "to catch up" so to say we can't use
* incremental update anymore, because with that we might miss some txs altogether.
*/
virtual void updatePoolState(std::vector<std::tuple<cryptonote::transaction, std::string, bool>> &process_txs, bool refreshed = false, bool try_incremental = false) = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that the function update_pool_state, with process_pool_state are both leaky abstractions as written. Both times they are used outside of wallet2, they are called like so:

std::vector<std::tuple<cryptonote::transaction, crypto::hash, bool>> process_txs;
m_wallet->update_pool_state(process_txs);
if (!process_txs.empty())
  m_wallet->process_pool_state(process_txs);

This sequence of statements basically means "rescan the pool" and IMO should be abstracted into a method as such. Calling update_pool_state without an immediate corresponding call to process_pool_state will almost always be a mistake, as this will update the fetching marker without actually scanning the transactions and getting up-to-date information about your balance. So I'd add a method to the Wallet interface called something like refreshPoolOnly which calls the above statements, and drop both updatePoolState and processPoolState from the interface.

- add `refreshPoolOnly()` which handles `upadte_pool_state()` and
  `process_pool_state`
- remove `cryptonote_basic.h` from interface
- change `shared_ptr` to `unique_ptr`
@SNeedlewoods
Copy link
Author

SNeedlewoods commented Nov 18, 2024

Update

Tests

Started working on tests on a new branch which helped finding the following minor issues with the Wallet API so far:

  • bool ssl is unused in WalletImpl::doInit() src and same for bool use_ssl and bool lightWallet in WalletImpl::init() src
  • wallet2::allow_mismatched_daemon_version() is not available in the API, it's not used by simplewallet or wallet-rpc, but imo it makes sense to add to the API. (Done)
  • WalletApiAccessorTest was only used for allow_mismatched_daemon_version(), not sure if it can get removed (from here and here).

TODO

Here are the last couple things which need attention and have been on my list for a while, but I haven't made any progress on recently, so feedback would be appreciated:

  • Multisig members from wallet2::transfer_details into EnoteDetailsImpl
    // QUESTION : Any input on these multisig members? I'd ignore them for now.
  • Missing i_wallet2_callback methods in Wallet2CallbackImpl
    virtual void on_reorg(uint64_t height, uint64_t blocks_detached, size_t transfers_detached) { /* TODO */ }
    virtual boost::optional<epee::wipeable_string> on_get_password(const char *reason) { return boost::none; }
    virtual void on_pool_tx_removed(const crypto::hash &txid) { /* TODO */ }
  • wallet2::get_multisig_wallet_state()
    // TODO : mms::multisig_wallet_state - from a quick search for get_multisig_wallet_state in simplewallet.cpp this will be complicated to replace
  • There are quite a few different ways to set a daemon, when comparing wallet2 to the GUI, CLI, RPC and API. I think it would make sense to add WalletImpl::setDaemon() with the same behavior as wallet2::set_daemon(), to get rid of all this complexity, but I'm unsure this can be done easily without breaking anything else, due to the current complexity:
    • wallet2: wallet2::set_daemon() (src), wallet2::init() (src) which calls wallet2::set_daemon()
    • GUI: WalletManager::setDaemonAddressAsync() (src) uses API WalletManager::setDaemonAddress()
    • CLI: simplewallet::set_daemon() uses wallet2::init() (src)
    • RPC: wallet_rpc_server::on_set_daemon() uses wallet2::set_daemon() (src)
    • API: WalletManager::setDaemonAddress() calls epee::net_utils::http::abstract_http_client::set_server() (src)

@rbrunner7
Copy link
Contributor

I had a look at the TODOs today. I am not sure how much my comments will help, but let me write them down nevertheless:

  • Multisig members from wallet2::transfer_details into EnoteDetailsImpl

I agree with your decision to leave them out, at least for the time being. Maybe we will be forced to add them if one day we really swap out wallet2 underneath and replace it with a fully new implementation, because that info has to "live" somewhere after all in such a scenario, but that's not a problem for today, seems to me.

  • Missing i_wallet2_callback methods in Wallet2CallbackImpl

As far as I can see you solved that in the meantime. I had a look at your latest, corresponding commit, and I think I could understand the flow of control more or less, and things look ok to me.

  • wallet2::get_multisig_wallet_state()

This is indeed only relevant in connection with the MMS, and the fate of that is pretty unclear. @tobtoht is currently building something that will replace / succeed it, and I don't have any knowledge how they will propose to modify and/or enlarge the interface of wallet2 to make it fly. So I think this method does not need a counterpart in the Wallet API for now.

  • Setting a daemon

I think a port of wallet2::set_daemon to the Wallet API with equivalent functionality makes sense, and right now I don't see what could go wrong with that available, or which requirements could be left unsatisfied by it. But it may well be that I didn't look closely enough.

SNeedlewoods and others added 2 commits November 27, 2024 13:44
@SNeedlewoods
Copy link
Author

Two more things:

  1. We have walletExists() in WalletManager src, but that only returns true if keys_file_exists, which is not sufficient for this case in simplewallet, where we differentiate between different combinations of keys_file_exists and wallet_file_exists (we also don't have access to WalletManager instance). So I added this static method to Wallet.
    I think we can get rid of the one in WalletManager now and replace the calls in GUI wallet to instead call the static Wallet method. Any opinions?

  2. I can't decide if we rather have two public methods for each freeze(), thaw() and is_frozen(), one by "index of enote in local enote storage" the other one by "key image" (like it's currently done src, context from "wallet2" freeze(idx) / freeze(key_image)), or should we make getEnoteIndex(key_image) (src) public, remove the freeze/thaw/is_frozen(key_image) and make the Wallet API clients have to make a call like freeze(getEnoteIndex(key_image))?

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

Successfully merging this pull request may close these issues.

5 participants