-
Notifications
You must be signed in to change notification settings - Fork 0
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
'wallet2' interface documentation: Query info about enotes, "payments" and transactions #49
Comments
I started looking at this issue this week and using the method 'divide to conquer' I think we could first separate the incoming and outcoming txs into separate discussions. I want to talk a bit about the outgoing txs. I agree that in wallet3 we could unify the confirmed_transfer_details and unconfirmed_transfer_details but I am not sure if it would be necessary as I dont know if we reached consensus that all wallet3 transaction outputs HAVE to be enotes (Seraphis standard). In this case, a module for tracking outputs would only actually track enotes. There are three ways to populate (in memory or in a wallet file) the 'transaction_history_out' (the analogous of the 'confirmed_transfer_details'):
It is not possible to recover the full information of the outgoing transactions (for example to whom you sent to) by only scanning the blockchain (in a scenario where you recover your keys in a new computer for example). But anyway, this component should contain the most updated and complete information about the outgoing txs. I see it being organized in a struct similar to that:
To get an idea, I prototyped a function to add a tx to the tx manager and a function to show the content of it. ... (one day later)... Now that I am thinking about it again, I am not really sure if we really need a component like that since in Seraphis the class SpEnoteStoreMockV1 (used to track the enotes) is capable of providing all the information contained in that struct. Maybe better things to think about are:
I will be looking again on the whole issue in the next days. |
I can't comment about your design thoughts yet, but for now just want to throw a thought of mine into the ring that might get overlooked: When designing the wallet components that hold info like enotes and transactions we should not merely care about the wallet itself, but also think about all the possible clients that want to get info from the wallet about what happened.
In my TechWallet I want people to show as clearly as possible what happens on the enote level. For that I need the info for a spent enote which transaction spent it. It seems that I had to resort to quite extreme measures to get the desired info, just because |
One comment nevertheless, about this here in your mock definitions:
This is less than elegant right now already, and doesn't this look like it will develop into some big mess over time, quite in principle? What happens if Seraphis evolves and comes up with EnoteRecordV2, V3, V4, whatever? Maybe for enotes the same question is appropriate like I ask it for transactions, proposed to discuss tomorrow: Maybe some "generalized", "over-arching" class able to hold any enote would be a very good idea? |
Agree. But before going into the details I will think more about the members and shape of this struct. The class SpEnoteStoreMockV1 contains really a lot of information and it could be almost itself the 'transaction_manager'. |
The enote store should not expand its capabilities beyond its current state. Its purpose is storing enotes loaded from balance-recovery processes. The internal complexity is already quite high to achieve that, adding more would overburden the class. |
If necessary you can use a variant of records. |
Agree with that too. So the trick will be to get the info we request in the most optimized way apparently. |
Yes, the strategy I decided on is to build secondary representations of the enote store contents using whatever caching strategy makes the most sense for each use-case. When an enote store is updated, it will emit a list of 'EnoteStoreEvents' that can be used to build the secondary representations. If no special caching methods are needed, you can just query the enote store directly. |
|
I dont think that the input rings record are needed as they can be easily retrieved if needed for something. |
Enote records contain more information than the tx record needs, so it's better to just store an identifier like the key image that can be used to find the enote later.
My idea is to store a |
Yeah, seems the best approach since the |
I don't follow. Transaction history should be an isolated component. |
Ok. Let me try to better shape it and confirm that we are on the same page.
There may be more or different ones but that's the direction that this component should go. What do you think? |
These don't need to be methods on the component. I'd encourage implementing as lean a component as possible.
How to resolve conflicts is something you'll have to figure out as the engineer. There is potential for conflict between all of these: records loaded from file, information found in an enote store on startup, records added while the program is running, enote store updates while the program is running.
Just call this
Would be much more useful to record the entire spent context. Plus you need the full context if you want to use e.g.
This should be impossible. If you encounter multiple conflicting tx records, you need to have a solution ready to merge them.
Is this a map of timestamps? It's not documented.
I think boost has a utility for storing a range of references. It may be worth looking into, so you don't need to make copies of all txs in a case like this.
These shouldn't need to be methods of the manager. The manager can expose 'lookup' functions for specific txs or ranges of txs, then you can use free functions to do needed operations on acquired txs (like getting full balance). The reason to minimize the interface of the manager is to reduce direct accesses to the manager's member variables, since over-abundant member access is a source of technical debt/spaghetti. It also makes it relatively harder to reason about the methods involved since the internal state of an object is controlled by intricate and dispersed invariants whereas its interface's invariants are direct and specific.
What information? Outlays? Outlays cannot be recovered from the blockchain.
Access to the daemon is not needed. Enote stores will emit an enote store event list when they are updated. Use the store events plus a const ref to the store to identify transaction records that need an updated spent context. Look up key images from the store events in the manager (e.g.
All of these should work as free functions. The manager just needs to be a cache that focuses on managing its internal state and providing access to that state in a clean/efficient manner.
Like I said, outlays cannot be recovered from the blockchain so it's not appropriate to store them in spent contexts. Outlays can only be recorded when you are creating a transaction (only records of txs that get submitted should be recorded).
I think you're on the right track, as long as you don't over-burden the manager. One piece of advice: don't force yourself to implement all of those functions if they have complicated dependencies (e.g. needing to request information from the daemon). First implement the easy ones that can be completed with just the available tools, then we can review what's left over and see if there is a 'next task/component' that can support missing functionality. |
I agree. I will try to make the simplest function (show some records) as soon as we agree with the minimal structure.
Yeah I know, just making sure we understand that.
What about the incoming transactions? Should this module keep track of the txs with unspent enotes? I didnt think much about it but seems to be a good idea. Anyway I will think about it later.
Okay, I was not sure about the correct approach here. You could indeed create a field in the EnoteContext to update the EnoteStore when a transaction is made but then I guess it would generate more conflicts if you would try to reload in different places... You are right here, it is better to be simple and coherent and let the EnoteStore recover only information available from the blockchain. The wallet should keep the records of the outlays.
Oh I'm stupid sorry. I thought one thing and wrote another. So, the idea is to go as optimized as possible from: blockheight -> tx_ids -> TransactionRecord. One blockheight can contain multiple tx_ids but every tx_id refers to only one TransactionRecord of course. I thought about storing a multimap<blockheight,tx_ids> and an unordered_map<tx_ids,TransactionRecord> so if I want to select a range (or the last txs) then I would first query the variable
Cool. I will make use of the EnoteStore a lot and don't really care about its management for now.
Ok, thanks for the answers. I will make the simplest implementations and then we can discuss more about it next week. |
There is no such thing as an incoming transaction. I feel a little crazy reading this paragraph.
If you want to go
To do 'I would get the TransactionRecords' you would normally need to allocate a vector or list of elements to return all the data. I am saying you could instead just return a container of references to your elements, which may be more efficient. |
Yeah, all the information you get from an incoming transaction can be found in the EnoteStore. So better call it incoming enotes instead of transactions. So yeah, nothing to do here.
Okay, I will try to find the optimum way and then we can discuss it. Thanks. |
After thinking a couple of days, I believe I'm close to the most optimized and minimalist structure. Statement of problem:
Let me begin comparing wallet2 and seraphis_wallet with an example where we retrieve the outgoing unconfirmed_payment (outlays and all the info related). Also works for confirmed txs. So, if we want to show the (unconfirmed) txs chronologically then wallet2 would fill and recover this information in the Solution for seraphis_wallet:
To achieve this solution I propose the following class/structs:
So, I would start implementing a basic show_transfers (to make use of all the structures and features) and in this case, if the user enters a tx id then I would go look at So before starting, I want to make sure that we are on the same wavelength with the following questions:
Let me know your thoughts and if we agree that I can proceed using the proposed scheme. If so, I think it is pretty clear to me the next tasks. I might have written some imprecise information but hopefully the line of thinking is clear. |
Right now I am thinking to use polling on async queue/channel where enote store update reports will be inserted. You don't have to worry about how exactly that will work just yet, since the method to update a transaction store doesn't need to care where the inputs come from. Your design looks like it's going in the right direction. |
The following text documents a small but very important subset of the public
wallet2
interface: all methods to query info about enotes, "payments" and transactions, incoming and outgoing, confirmed and unconfirmed, together with the structs used to return the info. (The complete header file is here.)The idea is supporting work on the top-level public interface of the new Seraphis wallet, sometimes called
wallet3
, its API so to say. By showing as clearly as possible what exists now it hopefully makes it easier to define the corresponding methods of the new wallet, and to define them in a better way, especially regarding terminology. It may help to decide how to store the info wallet-internally as well.I also compared this most basic API with 4 other places where you can query the info:
Ideally there would be much more simularity and regularity than today between the lowest-level Seraphis wallet API, the RPC wallet interface, and "higher" APIs that make it easy to program wallet apps like the wallet2 API or the Monero C++ library.
Confirmed enotes
This gets the list of all confirmed enotes.
transfer_details
must describe a single enote e.g. because there is only a single amount and a single destinationm_subaddr_index
. The term transfer is used for several different things, but inwallet2
in combination with "incoming" it is pretty unambiguous. This call is the basis for the CLI wallet commandincoming_transfers
and RPC walletCOMMAND_RPC_INCOMING_TRANSFERS
.There can be several enotes with the same incoming transaction id
txid
because several enotes for us can arrive in a single transaction. If the enote is spent it seems there is no call to query which transaction spent it.The wallet does not store incoming transactions with at least 1 enote as as separate dedicated list, only in the form of
transfer_details.m_tx
where we can have duplicates. Once full transactions were part of this struct until it was recognized that this is not needed and only a waste of space; since then not the full transaction, but only its prefix is here.The member variable to hold this list is
m_transfers
:The implementation of
get_transfers
is therefore an absolutely trivial single line:Incoming confirmed payments
This gives back all payments received with incoming confirmed transactions. A payment in the sense of this call is the sum of all enotes of such a transaction that went to a particular subaddress with incoming confirmed transaction
tx_hash
. The name of this method would be clearer if it contained the term incoming somewhere.With a typical transaction it's pretty simple: It contains a single enote to one subaddress, and in this case that enote is already the payment, and in a certain sense also the transaction because the enote with the change does not interest at all. Transactions that contain enotes going to several of our subaddresses are somewhat special, and several enotes going to the same subaddress even more so, but of course possible because a transaction can contain any combination of up to 16 enotes.
It's interesting that only
wallet2
has a dedicated method for merely getting these payments in particular, and also that its use of the term payment stands out:In the CLI wallet you get them if you use the command
show_transfers
with typein
included. RPC walletCOMMAND_RPC_GET_TRANSFERS
gives them back if you set thein
boolean in the request and works with atransfer_entry
struct that covers all possible types of payments, not only incoming confirmed ones. wallet2 API includes them in aTransactionHistory
object, marked asDirection_In
, also working with a struct calledTransactionInfo
that covers all possible types of payments. The Monero C++ library has aget_transfers
method that depending on the query can only give back incoming confirmed payments as a list ofmonero_transfer
structs.The struct member
m_amounts
detailing the amounts in the case of several enotes to the same subaddress is a relatively new addition.The first element of the pair is the payment id. Not including this as a member of
payment_details
and not giving back the payments as a simple list looks strange but is probably a consequence of the waywallet2
stores these payments internally, to support lookup by payment id:Incoming confirmed payments for payment id
This returns all incoming confirmed payments that have the given payment id. It's the basis for the CLI wallet command
payments
and RPC walletCOMMAND_RPC_GET_PAYMENTS
. It's a "convenience method" with low importance as you could easily use the firstget_payments
method above and pick payments by id yourself.Incoming unconfirmed payments
This returns all incoming unconfirmed payments. It introduces yet another struct
pool_payment_details
for just one member more thanpayment_details
that theget_payments
method for confirmed payments uses.Again the other systems give back these payments as part of some unified system as
pool
- for the general approach check comments further up.The container to store these:
Outgoing confirmed transactions
This returns all outgoing confirmed transactions. Thus a bit confusingly here payment is indeed a full transaction and not merely info about a collection of enotes like with the
get_payments
methods to get incoming confirmed payments. Maybe things would also be a bit clearer ifconfirmed_transfer_details
contained "out" in its name somehow.Again the other systems give back these transactions as part of some unified system under
out
- for the general approach check comments further up. The RPC interface has atransfer_entry.destinations
list member to store the possibly multiple destinations of an outgoing transaction that only gets used for such. wallet2 API hasTransactionInfo.transfers
for this, also working with a single struct for all types, and the Monero C++ library hasmonero_outgoing_transfer.m_destinations
working with inheritance, asmonero_outgoing_transfer
is an extension ofmonero_transfer
.The container to store these:
It's really a bit painful to see how the method name uses the term payment, the struct name the term transfer and the variable name the term transaction.
Outgoing unconfirmed transactions
This returns all outgoing unconfirmed transactions. The few and small differences between the two structs
confirmed_transfer_details
andunconfirmed_transfer_details
begs the question why have two different structs in the first place.Again the other systems give back these transactions as part of some unified system as
pending
andfailed
- for the general approach check comments further up.The container to store these:
The text was updated successfully, but these errors were encountered: