About Wallets
Date | Responsible | Changes |
---|---|---|
December 15, 2022 | @Željko Rumenjak | Initial version |
January 19, 2023 | @Tomislav Herman | Update wallet naming and addressing |
January 20, 2023 | @Tomislav Herman | Removed Nested Wallet concept since it is already explained in Addressing part. |
October 24, 2023 | @Luis Fidelis | • Added Domains resolution section • Added Anchors resolution section |
October 23, 2024 | @Tomislav Bradaric | Added list of all supported routes and descriptions to the first section of routes in wallet docs. Updated wallet routes and added input/output routes concept. |
All ledger records have a very similar structure, but each one has a specific purpose. It is important to understand when to use each of those core concepts to avoid abusing them in certain situations or modelling a system that can be difficult to extend in the future.
Wallets are the only ledger records that can hold balances. This is a very important distinction from other core concepts. Because of that, it should be relatively easy to decide what should be modelled as a wallet. In general, anything that holds a balance should be a wallet. The most common examples include: bank or any other type of financial accounts, credit cards, loyalty cards, etc.
Some other, less common uses could also include concepts like invoices, loans, bills and similar. It may be unintuitive to think that we could model those in the same way as, let’s say, bank accounts, but it is important to understand that wallets are a lightweight concept in ledger. This means that they can be created very quickly and this makes them a good option for single-use situations as well. As a quick example, modelling an invoice as a wallet can be beneficial in some situations because that would allow us to leave it open until balance is settled. This opens a lot of interesting possibilities in the future, things like partial payments, payments through instalments, increasing the outstanding amount by adding interests, etc.
Although wallets can hold balances, it isn’t required that they always must hold a balance. Some cases for wallets that can’t hold balances include representing virtual ledger accounts or remote accounts from external systems. It makes sense to represent those concepts as wallets as well, since they are accounts, but we can configure ledger in a way to prevent assigning balances to such wallets. Balances are usually assigned to a settlement account in such cases, this can be configured as well. Representing such concepts as wallets also leaves room for future improvements, for example, we could decide to convert those wallets to native ledger wallets that can hold balances at some point. This is very simple to do if those accounts are already represented as wallets.
Naming Wallets
The identifier of each wallet is its handle. The most basic wallet handle is a simple name, containing only letters and numbers. This can be enough in some cases, but in more complex systems we need more structure around wallet names. Wallet handles have a syntax that can be used in order to provide a consistent naming structure. The main parts of wallet handle are schema
, handle
and parent
:
- schema (optional) - defines a type of wallet, schema can be used to categorise wallets depending on their purpose, and to attach specific policies to those categories. Examples of schemas:
tel
,account
,loan
, etc. - handle (required) - represents the wallet identifier within an (optional) schema. This is usually the primary wallet identifier, it can be an organization name, phone number, account number or similar. For example
hpb
,+1356545666
orHR1210010051863000160
. - parent (optional) - represents the parent wallet which is authoritative wallet for the
handle
on the left side. If parent wallet is specified in addressing then it usually means that the handle part is not used by ledger when assigning balances, although the handle can be used to configure ledger routing rules more easily. Data in handle is usually interpreted by external ledgers in order to identify resources within their system. For example, in case aparent
identifier is a bank,handle
could be an account within that bank.
Address resolution
Although the wallet handle and address both have the same structure they don’t represent the same concept.
- wallet handle is identifier of existing wallet in the ledger. Existing wallet can receive balance or be the source of balance movement.
- address is identifier which is used as source and target in balance movements. Sometimes an address can be used which is not the handle of existing wallet in the ledger. In this case address resolution is used to resolve the authoritative wallet for this address. Resolved wallet is then used as source or target of balance movement and also as the point of contact for external ledger through Bridges. See more here: About Bridges.
Address resolution is performed by going up the addressing hierarchy until a wallet with the matching handle is found. Addressing hierarchy for our generic address format is listed below. Note that each step in hierarchy will be considered only if all components for this step are present in address:
Examples:
Addressing examples
Let’s look at two examples of addressing in real world scenarios. First, let’s assume we are modelling external bank accounts and using ledger only to track settlement amounts between banks. An address naming structure for this case could look like account:1050000029@hpb
. There are no wallets with such handles, but instead there is an authoritative wallet with handle hpb
representing the bank.
account
is our schema, it tells us that this is a bank account1070000029
is a handle representing an account number in the banking core. This is something that bank uses when processing payments to know which account to debit or credit.hpb
is the name of a bank and this means that all balances targetingaccount:*@hpb
are going to be credited to authoritativehpb
wallet in our ledger. Ledger is not taking thehandle
into account here.
The second example we can look at is a case when we want to model wallets that represent phone numbers in the system. This is an example of how to model an phone alias directory service. We can use addresses with structure tel:15261234578
to send balance to phone numbers. In this example there exists an authoritative wallet tel
which is in charge of handling all balance movements to phone numbers.
tel
is our schema, it identifies all wallets that represent phone alias directory records. Since there is authoritative wallettel
, it will be used to pass all intents withtel:*
target to a service responsible for our alias directory by using Bridge.15261234578
is a phone number handle that we want to target, this handle can be used by our phone alias directory service to onboard users by sending them SMS messages, etc.
💡 Phone alias directory service can be used to onboard the user by creating the wallet with handle tel:15261234578
and making sure that this wallet has a route which forwards balance to a bank account address. After that, this phone number wallet is more specific and will supersede previous authoritative tel
wallet and will become in charge for it’s own address.
Wallet Types
There are several types of wallets that can be created in ledger, those include:
- native wallets - default type of ledger wallets, balances of those wallets are fully managed by ledger, there are no external systems involved in processing of balance movements related to these wallets
- bridge wallets - these are wallets that are linked to external systems. Every balance movement related to these wallets needs to be processed in a remote system before it is confirmed in the ledger.
Default wallet type for all new wallets is a native ledger wallet. To create a native wallet is enough to provide only a handle, for example:
If we want to create a bridge wallet with the same handle, we can do it by providing a name of a bridge along with the handle like this:
Note: A bridge with handle hpb
must be registered in ledger in order for this to work.
To learn more about bridges, see About Bridges.
Ledger is going to handle operations on bridge wallets by performing two-phase commits for all balance movements related to those wallets. If a bridge wallet is a source of a claim, ledger is going to require a confirmation that a debit was performed in a remote system before confirming the operation. Similarly, if a bridge wallet is used as a target, ledger is going to require a confirmation that a credit was performed before the operation is confirmed. In case any of those operations isn’t performed successfully, ledger is going to abort the whole intent or chain of related intents. This ensures that all balance movements are performed cleanly end-to-end, regardless of the number of remote systems involved in the process.
Wallet Routes
Routes are a ledger concept that allows setting up automatic rules for funds management on wallets. Routes need to be registered on specific wallets by users with sufficient permissions to perform the resulting operation. By registering a route, user is delegating authority to ledger to perform the described operation under defined permissions in the name of that user.
Ledger is going to perform those operations automatically during intent processing if the conditions described in routes are matched. This means that a resulting operation is going to extend the intent processing chain and any potential failures are also going to rollback the whole intent chain. This is an important concept to understand, it makes routes a very powerful tool for modelling more advanced transactional flows in ledgers.
Effects are a good alternative for cases when an operation is optional and it isn’t necessary to rollback all previous operations. For more details about effects, see About Extending Ledger.
Currently supported wallet routes are:
- debit
- Instead of the money being debited from the resolved source wallet of the claim, it will be debited from the
target
of this route. Bridge calls for debit will also be sent to the bridge assigned to the target wallet of this route, not of the resolved source wallet of the claim.
- Instead of the money being debited from the resolved source wallet of the claim, it will be debited from the
- credit
- Instead of the money being credited to the target wallet of the claim, it will be credited to the
target
of this route. Bridge calls for credit will also be sent to the bridge assigned to the target wallet of this route, and not the bridge of the resolved target wallet of the claim.
- Instead of the money being credited to the target wallet of the claim, it will be credited to the
- accept
- While other routes affect the flow of symbol balance, this one does not. If applied to a claim, defines whether or not an intent will be accepted. If the filter is not satisfied, the intent will be automatically rejected.
- forward
- If applied to target of a claim, will result in the amount of the claim being forwarded as a new intent in the same thread.
These routes are divided into two categories - input
routes and output
routes. input
routes may affect transfer
claims, while output
routes may affect transfer
and issue
claims.
Route filters
Each route has an optional filter
property. Routes are triggered during intent processing flow, and they are evaluated for each claim of the intent individually. That is why route targets the data from claims.
When registering a new route you can specify a filter
property that allows you to match any available fields from an incoming claim. If those conditions pass, a route action is going to be performed by ledger.
Here is an example of such a route which will be satisfied only if the symbol of the claim is eur
With a route like the above, only claims with symbol eur
are going to be accepted, all other claims are going to be rejected automatically. This route allows us to create a single-currency wallet.
input
routes
These routes affect in-routable
claims. Currently only transfer
claims are in-routable
.
Input routes are as follows:
debit
output
Routes
These routes affect out-routable
claims. Currently transfer
and issue
claims are out-routable
.
Output routes are as follows:
credit
forward
accept
An example of such a route is given here:
The route in the example above will forward intents to different wallets depending on the received currency. Forwarding will be done in a new intent within the same thread.
Catch-all Route
Routes are processed in order how they are declared within their route group (see in
and out
routes above). If some in
routes are defined, but none of their filters match, the claim is going to be rejected and the containing intent is going to fail as well. The same behaviour applies, separately, to out
routes.
To avoid this behaviour in situations when routes are considered optional it is necessary to define a catch-all route. This route should be placed last and can accept all claims by omitting a filter. For out
routes this could be a neutral route like accept
:
For in
routes, this can be a neutral route such as a debit
that debits from that same wallet (essentially it has no observable consequence). So if our wallet handle is my-wallet
, it would be:
If no routes are specified in the wallet it is considered that there are no conditions on incoming claims. As soon as you specify at least one route it is expected that routes cover all acceptable cases and any unmatched claim is going to be rejected.
Anchors resolution
-
The wallet API allows to fetch anchors via
GET /v2/wallets/:address/anchors
endpoint.Consider the address
tel:123
.Firstly, it queries in ledger's database the anchors which are registered for the wallet
tel:123
viawallet
property.Then, the ledger uses the address passed by parameter
tel:123
to resolve a wallet in the database by using the address resolution algorithm. If a wallet is found and has bridge defined, the ledger calls the bridge for fetching the anchors for the addresstel:123
passed via query:v2/anchors?wallet=tel:123
. If valid, the result is appended to the ledger response. -
it is also possible to resolve anchors via lookup
POST /v2/wallets/:address/anchors/!lookup
.Consider the address
tel:123
.The ledger uses the address passed by parameter
tel:123
to resolve a wallet in the database by using the address resolution algorithm. If a wallet is found and has bridge defined, the ledger calls the bridge's anchor lookup handler for resolving the anchors with request payload sent by the user:POST /v2/anchors/!lookup
.The body of lookup query should follows the same schema as an anchor record. Except by the fact all the properties in
data
are optional.See About Anchors for more details.
Bridge is called if it implements anchors
trait. Please read About Bridges for more details about bridge traits.
Domains resolution
The wallet exposes an endpoint for querying domains corresponding to an address via GET /v2/wallets/:address/domains
.
Consider the address tel:123
.
Firstly, it queries in ledger's database the wallets which their handle starts with tel:123@
in order to get the list of domains connected to tel:123
schema:identifier address.
Then, the ledger uses the address passed by parameter tel:123
to resolve a wallet in the database by using the address resolution algorithm. If a wallet is found and has bridge defined, the ledger calls the bridge for resolving domains for the address tel:123
passed via query: v2/domains?wallet=tel:123
. If valid, the result is appended to the ledger response.
Bridge is called if it implements domains
trait. Please read About Bridges for more details about bridge traits.