- Tutorial docs through step 3 (inputting an account) - New screenshots - Minor code cleanup - Fix the include_code filter to better handle indentation in Python code
20 KiB
parent, filters, targets, blurb
| parent | filters | targets | blurb | |||
|---|---|---|---|---|---|---|
| build-apps.html |
|
|
Build a graphical desktop wallet for the XRPL using Python. |
Build a Desktop Wallet in Python
This tutorial demonstrates how to build a desktop wallet for the XRP Ledger using the Python programming language and various libraries. This application can be used as a starting point for building a more complete and powerful application, as a reference point for building comparable apps, or as a learning experience to better understand how to integrate XRP Ledger functionality into a larger project.
Prerequisites
To complete this tutorial, you should meet the following guidelines:
- You have Python 3.7 or higher installed.
- You are somewhat familiar with object-oriented programming in Python and have completed the Get Started Using Python tutorial.
- You have some understanding of what the XRP Ledger can do and of cryptocurrency in general. You don't need to be an expert.
Source Code
You can find the complete source code for all of this tutorial's examples in the code samples section of this website's repository.
Goals
At the end of this tutorial, you should have a Python application that looks something like this:
The exact look and feel of the user interface depend on your computer's operating system. This application is capable of the following:
- Shows updates to the XRP Ledger in real-time.
- Can view any XRP Ledger account's activity "read-only" including showing how much XRP was delivered by each transaction.
- Shows how much XRP is set aside for the account's reserve requirement.
- Can send direct XRP payments, and provides feedback about the intended destination address, including:
- Whether the intended destination already exists in the XRP Ledger, or the payment would have to fund its creation.
- If the address doesn't want to receive XRP (DisallowXRP flag enabled).
- If the address a verified domain name associated with it.
The application in this tutorial doesn't have the ability to send or trade tokens or use other payment types like Escrow or Payment Channels. However, it provides a foundation that you can implement those and other features on top of.
In addition to the above features, you'll also learn a little bit about graphical user interface (GUI) programming, threading, and asynchronous (async) code in Python.
Steps
Install Dependencies
This tutorial depends on various programming libraries. Before you get started coding, you should install all of them as follows:
pip3 install --upgrade xrpl-py wxPython requests toml
This installs and upgrades the following Python libraries:
- xrpl-py, a client library for the XRP Ledger. This tutorial requires version 1.3.0 or higher.
- wxPython, a cross-platform graphical toolkit.
- Requests, a library for easily making HTTP requests.
- toml, a library for parsing TOML-formatted files.
The requests and toml libraries are only needed for the domain verification in step 6, but you can install them now while you're installing other dependencies.
1. Hello World
The first step is to build an app that combines the "hello world" equivalents for the XRP Ledger and wxPython programming. The code is as follows:
{{ include_code("_code-samples/build-a-wallet/py/1_hello.py", language="py") }}
When you run this script, it displays a single window that (hopefully) shows the latest validated ledger index on the XRP Ledger Testnet. It looks like this:
Under the hood, the code makes a JSON-RPC client, connects to a public Testnet server, and uses the [ledger method][] to get this information. Meanwhile, it creates a wx.Frame subclass as the base of the user interface. This class makes a window the user can see, with a wx.StaticText widget to display text to the user, and a wx.Panel to hold that widget.
2. Show Ledger Updates
Full code for this step: 2_threaded.py.
You may have noticed that the app in step 1 only shows the latest validated ledger at the time you opened it: the text displayed never changes unless you close the app and reopen it. The actual XRP Ledger is constantly making forward progress, so a more useful app would show it, something like this:
If you want to continually watch the ledger for updates (for example, waiting to see when new transactions have been confirmed), then you need to change the architecture of your app slightly. For reasons specific to Python, it's best to use two threads: a "GUI" thread to handle user input and display, and a "worker" thread for XRP Ledger network connectivity. The operating system can switch quickly between the two threads at any time, so user interface can remain responsive while the background thread waits on information from the network that may take a while to arrive.
The main challenge with threads is that you have to be careful not to access data from one thread that another thread may be in the middle of changing. A straightforward way to do this is to design your program so that you each thread has variables it "owns" and doesn't write to the other thread's variables. In this program, the class attributes (anything starting with self.) are When the threads need to communicate, they use specific, "threadsafe" methods of communication, namely:
- For GUI to worker thread, use
asyncio.run_coroutine_threadsafe(). - For worker to GUI communications, use
wx.CallAfter().
To make full use of the XRP Ledger's ability to push messages to the client, use xrpl-py's AsyncWebsocketClient instead of JsonRpcClient. This lets you "subscribe" to updates using asynchronous code, while also performing other request/response actions in response to various events such as user input.
Note: While you can, technically, use the synchronous (that is, non-async) WebSocket client, it gets significantly more complicated to manage these things while also handling input from the GUI. Even if writing async code is unfamiliar to you, it can be worth it to reduce the overall complexity of the code you have to write later.
Add these imports to the top of the file:
{{ include_code("_code-samples/build-a-wallet/py/2_threaded.py", language="py", start_with="import async", end_before="class XRPLMonitorThread") }}
Then, the code for the monitor thread is as follows (put this in the same file as the rest of the app):
{{ include_code("_code-samples/build-a-wallet/py/2_threaded.py", language="py", start_with="class XRPLMonitorThread", end_before="class TWaXLFrame") }}
This code defines a Thread subclass for the worker. When the thread is created, it starts an event loop, which means it's waiting for async tasks and functions to be created. The watch_xrpl() function is an example of a such a task (which the GUI thread starts when it's ready): connects to the XRP Ledger, then calls the [subscribe method][] to be notified whenever a new ledger is validated. It uses the immediate response and all later subscription stream messages to trigger updates of the GUI.
**Tip: Define worker jobs like this using async def instead of def so that you can use the await keyword in them; you need to use await to get the response to the AsyncWebsocketClient.request() method.
Update the code for the main thread and GUI frame to look like this:
{{ include_code("_code-samples/build-a-wallet/py/2_threaded.py", language="py", start_with="class TWaXLFrame", end_before="if name") }}
The part that builds the GUI has been moved to a separate method, build_ui(self). This helps to divide the code into chunks that are easier to understand, because the __init__() constructor has other work to do now, too: it starts the worker thread, and gives it its first job. The GUI setup also now uses a sizer to control placement of the text within the frame.
Tip: In this tutorial, all the GUI code is written by hand, but you may find it easier to create powerful GUIs using a "builder" tool such as wxGlade. Separating the GUI code from the constructor may make it easier to switch to this type of approach later.
There's a new helper method, run_bg_job(), which runs an asynchronous function (defined with async def) in the worker thread. Use this method any time you want the worker thread to interact with the XRP Ledger network.
Instead of a get_validated_ledger() method, the GUI class now has an update_ledger() method, which takes an object in the format of a ledger stream message and displays some of that information to the user. The worker thread calls this method using wx.CallAfter() whenever it gets a ledgerClosed event from the ledger.
Finally, change the code to start the app (at the end of the file) slightly:
{{ include_code("_code-samples/build-a-wallet/py/2_threaded.py", language="py", start_with="if name") }}
Since the app uses a WebSocket client instead of the JSON-RPC client now, the code has to be use WebSocket URL to connect.
Tip: If you run your own rippled server you can connect to it using ws://localhost:6006 as the URL. You can also use the WebSocket URLs of public servers to connect to the Mainnet or other test networks.
3. Display an Account
Full code for this step: 3_account.py
A "wallet" application is one that lets you manage your account. Now that we have a working, ongoing connection to the XRP Ledger, it's time to start adding details for a specific account. For this step, you should prompt the user to input their address or master seed, then use that to display information about their account including how much XRP is set aside for the reserve requirement.
The prompt is in a popup dialog like this:
After the user inputs the prompt, the updated GUI looks like this:
When you do math on XRP amounts, you should use the Decimal class so that you don't get rounding errors. Add this to the top of the file, with the other imports:
{{ include_code("_code-samples/build-a-wallet/py/3_account.py", language="py", start_with="from decimal", end_before="class XRPLMonitorThread") }}
In the XRPLMonitorThread class, rename and update the watch_xrpl() method as follows:
{{ include_code("_code-samples/build-a-wallet/py/3_account.py", language="py", start_with="async def watch_xrpl", end_before="async def on_connected") }}
The newly renamed watch_xrpl_account() method now it takes an address and optional wallet and saves them for later. (The GUI thread provides these based on user input.) This method also adds a new case for transaction stream messages. When it sees a new transaction, the worker does not yet do anything with the transaction itself, but it uses that as a trigger to get the account's latest XRP balance and other info using the [account_info method][]. When that response arrives, the worker passes the account data to the GUI for display.
Still in the XRPLMonitorThread class, update the on_connected() method as follows:
{{ include_code("_code-samples/build-a-wallet/py/3_account.py", language="py", start_with="async def on_connected", end_before="class AutoGridBagSizer") }}
The on_connected() method now subscribes to transactions for the provided account (in addition to the ledger stream). Furthermore, it now calls [account_info][account_info method] on startup, and passes the response to the GUI for display.
The new GUI has a lot more fields that need to be laid out in two dimensions. The following subclass of wx.GridBagSizer provides a quick way to do so, setting the appropriate padding and sizing values for a two-dimensional list of widgets. Add this code to the same file:
{{ include_code("_code-samples/build-a-wallet/py/3_account.py", language="py", start_with="class AutoGridBagSizer", end_before="class TWaXLFrame") }}
Update the TWaXLFrame's constructor as follows:
{{ include_code("_code-samples/build-a-wallet/py/3_account.py", language="py", start_with="def init(self, url, test_network=True):", end_before="def build_ui(self):") }}
Now the constructor takes a boolean to indicate whether it's connecting to a test network. (If you provide a Mainnet URL, you should also pass False.) It uses this to encode and decode X-addresses and warn if they're intended for a different network. It also calls a new method, prompt_for_account() to get an address and wallet, and passes those to the renamed watch_xrpl_account() background job.
Update the build_ui() method definition as follows:
{{ include_code("_code-samples/build-a-wallet/py/3_account.py", language="py", start_with="def build_ui(self):", end_before="def run_bg_job(self, job):") }}
This adds a wx.StaticBox with several new widgets, then uses the AutoGridBagSizer (defined above) to lay them out in 2×4 grid within the box. These new widgets are all static text to display details of the account, though some of them start with placeholder text. (Since they require data from the ledger, you have to wait for the worker thread to send that data back.)
Caution: You may notice that even though the constructor for this class sees the wallet variable, it does not save it as a property of the object. This is because the wallet mostly needs to be managed by the worker thread, not the GUI thread, and updating it in both places might not be threadsafe.
Add these two new methods to the TWaXLFrame class:
{{ include_code("_code-samples/build-a-wallet/py/3_account.py", language="py", start_with="def prompt_for_account", end_before="def update_ledger") }}
The prompt_for_account() method is the important one: the constructor calls this method to prompt the user for their address or master seed, then processes the user input to decode whatever value the user put in, and use it accordingly. With wxPython, you usually follow this pattern with dialog boxes:
- Create a new dialog, such as
wx.TextEntryDialog. - Use
showModal()to display it to the user and get a return code based on which button the user clicked. - If the user clicked OK, get a value the user input, in this, whatever text the user entered in the box.
- Destroy the dialog.
From there, the code branches based on whether the input is a classic address, X-address, seed, or not a valid value at all. Assuming the value decodes successfully, it updates the wx.StaticText widgets with both the classic and X-address equivalents of the address and returns them. (As noted above, the constructor passes these values to the worker thread.)
Tip: This code exits if the user inputs an invalid value, but you could rewrite it to prompt again or display a different message to the user.
This code also does something unusual: it binds an event handler, which is a method that is called whenever a certain type of thing happens in the GUI, usually based on the user's actions. In this case, the trigger is wx.EVT_TEXT on the dialog, which triggers immediately when the user types or pastes anything into the dialog's text box.
Add the following method to TWaXLFrame class to define the handler:
{{ include_code("_code-samples/build-a-wallet/py/3_account.py", language="py", start_with="def toggle_dialog_style", end_before="def prompt_for_account") }}
Event handlers generally take one positional argument, a wx.Event object which is provided by the GUI toolkit and describes the exact event that occurred. In this case, the handler uses this object to find out what value the user input. If the input looks like a master seed (it starts with the letter "s"), the handler switches the dialog to a "password" style that masks the user input, so people viewing the user's screen won't see the secret. And, if the user erases it and switches back to inputting an address, it toggles the style back.
Add the following lines at the end of the update_ledger() method:
{{ include_code("_code-samples/build-a-wallet/py/3_account.py", language="py", start_with="# Save reserve settings", end_before="def calculate_reserve_xrp") }}
This saves the ledger's current reserves settings, so that you can use them to calculate the account's total amount of XRP reserved. Add the following method to the TWaXLFrame class, to do exactly that:
{{ include_code("_code-samples/build-a-wallet/py/3_account.py", language="py", start_with="def calculate_reserve_xrp", end_before="def update_account") }}
Add an update_account() method:
{{ include_code("_code-samples/build-a-wallet/py/3_account.py", language="py", start_with="def update_account", end_before="if name") }}
The worker thread calls this method to pass account details to the GUI for display.
Lastly, towards the end of the file, pass the new test_net parameter when instantiating the TWaXLFrame class:
{{ include_code("_code-samples/build-a-wallet/py/3_account.py", language="py", start_with="frame = TWaXLFrame", end_before="frame.Show()") }}
(If you change the code to connect to a Mainnet server URL, also change this value to False.)
To test your wallet app with your own test account, first go to the Testnet Faucet and Get Testnet credentials. Save the address and secret key somewhere, and try your wallet app with either one. Then, to see balance changes, go to the Transaction Sender and paste your address into the Destination Address field. Click Initialize and try out some of the transaction types there, and see if the balance displayed by your wallet app updates as you expect.
4. Show Account's Transactions
Full code for this step: 4_tx_history.py
TODO
5. Send XRP
Full code for this step: 5_send_xrp.py
TODO
6. Domain Verification and Polish
Full code for this step: 6_verification_and_polish.py
TODO
{% include '_snippets/rippled-api-links.md' %} {% include '_snippets/tx-type-links.md' %} {% include '_snippets/rippled_versions.md' %}




