From 4dba0cf70fff6ea4ca7e57e165a0a54ddf43e9fb Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Wed, 19 Jan 2022 17:33:23 -0800 Subject: [PATCH] Build a Wallet: progress thru step 3: - 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 --- .../build-a-wallet/py/3_account.py | 9 +- .../build-a-wallet/py/4_tx_history.py | 10 +- .../build-a-wallet/py/5_send_xrp.py | 9 +- .../py/6_verification_and_polish.py | 9 +- .../build-a-desktop-wallet-in-python.md | 114 ++++++++++++++++-- img/python-wallet-2.gif | Bin 0 -> 11938 bytes img/python-wallet-3-enter.png | Bin 0 -> 9827 bytes img/python-wallet-3-main.png | Bin 0 -> 21195 bytes tool/filter_include_code.py | 28 ++++- 9 files changed, 147 insertions(+), 32 deletions(-) create mode 100644 img/python-wallet-2.gif create mode 100644 img/python-wallet-3-enter.png create mode 100644 img/python-wallet-3-main.png diff --git a/content/_code-samples/build-a-wallet/py/3_account.py b/content/_code-samples/build-a-wallet/py/3_account.py index f1e7889983..3a497784e4 100644 --- a/content/_code-samples/build-a-wallet/py/3_account.py +++ b/content/_code-samples/build-a-wallet/py/3_account.py @@ -253,9 +253,9 @@ class TWaXLFrame(wx.Frame): f"Ledger Index: {message['ledger_index']}\n" f"Ledger Hash: {message['ledger_hash']}\n" f"Close time: {close_time_iso}") - # Save reserve settings (in drops of XRP) so we can calc account reserve - self.reserve_base = Decimal(message["reserve_base"]) - self.reserve_inc = Decimal(message["reserve_inc"]) + # Save reserve settings so we can calculate account reserve + self.reserve_base = xrpl.utils.drops_to_xrp(str(message["reserve_base"])) + self.reserve_inc = xrpl.utils.drops_to_xrp(str(message["reserve_inc"])) def calculate_reserve_xrp(self, owner_count): """ @@ -265,8 +265,7 @@ class TWaXLFrame(wx.Frame): if self.reserve_base == None or self.reserve_inc == None: return None oc_decimal = Decimal(owner_count) - reserve_drops = self.reserve_base + (self.reserve_inc * oc_decimal) - reserve_xrp = xrpl.utils.drops_to_xrp(str(reserve_drops)) + reserve_xrp = self.reserve_base + (self.reserve_inc * oc_decimal) return reserve_xrp def update_account(self, acct): diff --git a/content/_code-samples/build-a-wallet/py/4_tx_history.py b/content/_code-samples/build-a-wallet/py/4_tx_history.py index bebeebc81f..9b531230a2 100644 --- a/content/_code-samples/build-a-wallet/py/4_tx_history.py +++ b/content/_code-samples/build-a-wallet/py/4_tx_history.py @@ -5,7 +5,6 @@ import wx import wx.dataview import wx.adv import asyncio -import re from threading import Thread from decimal import Decimal @@ -284,9 +283,9 @@ class TWaXLFrame(wx.Frame): f"Ledger Index: {message['ledger_index']}\n" f"Ledger Hash: {message['ledger_hash']}\n" f"Close time: {close_time_iso}") - # Save reserve settings (in drops of XRP) so we can calc account reserve - self.reserve_base = Decimal(message["reserve_base"]) - self.reserve_inc = Decimal(message["reserve_inc"]) + # Save reserve settings so we can calculate account reserve + self.reserve_base = xrpl.utils.drops_to_xrp(str(message["reserve_base"])) + self.reserve_inc = xrpl.utils.drops_to_xrp(str(message["reserve_inc"])) def calculate_reserve_xrp(self, owner_count): """ @@ -296,8 +295,7 @@ class TWaXLFrame(wx.Frame): if self.reserve_base == None or self.reserve_inc == None: return None oc_decimal = Decimal(owner_count) - reserve_drops = self.reserve_base + (self.reserve_inc * oc_decimal) - reserve_xrp = xrpl.utils.drops_to_xrp(str(reserve_drops)) + reserve_xrp = self.reserve_base + (self.reserve_inc * oc_decimal) return reserve_xrp def update_account(self, acct): diff --git a/content/_code-samples/build-a-wallet/py/5_send_xrp.py b/content/_code-samples/build-a-wallet/py/5_send_xrp.py index f0ac2548c4..ce0c1b5d76 100644 --- a/content/_code-samples/build-a-wallet/py/5_send_xrp.py +++ b/content/_code-samples/build-a-wallet/py/5_send_xrp.py @@ -398,9 +398,9 @@ class TWaXLFrame(wx.Frame): f"Ledger Index: {message['ledger_index']}\n" f"Ledger Hash: {message['ledger_hash']}\n" f"Close time: {close_time_iso}") - # Save reserve settings (in drops of XRP) so we can calc account reserve - self.reserve_base = Decimal(message["reserve_base"]) - self.reserve_inc = Decimal(message["reserve_inc"]) + # Save reserve settings so we can calculate account reserve + self.reserve_base = xrpl.utils.drops_to_xrp(str(message["reserve_base"])) + self.reserve_inc = xrpl.utils.drops_to_xrp(str(message["reserve_inc"])) def calculate_reserve_xrp(self, owner_count): """ @@ -410,8 +410,7 @@ class TWaXLFrame(wx.Frame): if self.reserve_base == None or self.reserve_inc == None: return None oc_decimal = Decimal(owner_count) - reserve_drops = self.reserve_base + (self.reserve_inc * oc_decimal) - reserve_xrp = xrpl.utils.drops_to_xrp(str(reserve_drops)) + reserve_xrp = self.reserve_base + (self.reserve_inc * oc_decimal) return reserve_xrp def update_account(self, acct): diff --git a/content/_code-samples/build-a-wallet/py/6_verification_and_polish.py b/content/_code-samples/build-a-wallet/py/6_verification_and_polish.py index 3151445ece..73b5968633 100644 --- a/content/_code-samples/build-a-wallet/py/6_verification_and_polish.py +++ b/content/_code-samples/build-a-wallet/py/6_verification_and_polish.py @@ -547,9 +547,9 @@ class TWaXLFrame(wx.Frame): f"Ledger Index: {message['ledger_index']}\n" f"Ledger Hash: {message['ledger_hash']}\n" f"Close time: {close_time_iso}") - # Save reserve settings (in drops of XRP) so we can calc account reserve - self.reserve_base = Decimal(message["reserve_base"]) - self.reserve_inc = Decimal(message["reserve_inc"]) + # Save reserve settings so we can calculate account reserve + self.reserve_base = xrpl.utils.drops_to_xrp(str(message["reserve_base"])) + self.reserve_inc = xrpl.utils.drops_to_xrp(str(message["reserve_inc"])) def calculate_reserve_xrp(self, owner_count): """ @@ -559,8 +559,7 @@ class TWaXLFrame(wx.Frame): if self.reserve_base == None or self.reserve_inc == None: return None oc_decimal = Decimal(owner_count) - reserve_drops = self.reserve_base + (self.reserve_inc * oc_decimal) - reserve_xrp = xrpl.utils.drops_to_xrp(str(reserve_drops)) + reserve_xrp = self.reserve_base + (self.reserve_inc * oc_decimal) return reserve_xrp def update_account(self, acct): diff --git a/content/tutorials/build-apps/build-a-desktop-wallet-in-python.md b/content/tutorials/build-apps/build-a-desktop-wallet-in-python.md index 5175066c9a..a09a9ee216 100644 --- a/content/tutorials/build-apps/build-a-desktop-wallet-in-python.md +++ b/content/tutorials/build-apps/build-a-desktop-wallet-in-python.md @@ -60,7 +60,7 @@ This installs and upgrades the following Python libraries: - [Requests](https://docs.python-requests.org/), a library for easily making HTTP requests. - [toml](https://github.com/uiri/toml), a library for parsing TOML-formatted files. -The `requests` and `toml` libraries are only needed for the [domain verification in step 6](TODO:link), but you can install them now while you're installing other dependencies. +The `requests` and `toml` libraries are only needed for the [domain verification in step 6](#6-domain-verification-and-polish), but you can install them now while you're installing other dependencies. ### 1. Hello World @@ -74,11 +74,15 @@ When you run this script, it displays a single window that (hopefully) shows the 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`](https://docs.wxpython.org/wx.Frame.html) subclass as the base of the user interface. This class makes a window the user can see, with a [`wx.StaticText`](https://docs.wxpython.org/wx.StaticText.html) widget to display text to the user, and a [`wx.Panel`](https://docs.wxpython.org/wx.Panel.html) to hold that widget. -### 2. Showing Updates +### 2. Show Ledger Updates -**Full code for this step:** [`2_threaded.py`](TODO:link). +**Full code for this step:** [`2_threaded.py`](({{target.github_forkurl}}//tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/py/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 app doesn't keep watching for updates. 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. +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: + +![Animation: Step 2, showing ledger updates](img/python-wallet-2.gif) + +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: @@ -122,28 +126,118 @@ Since the app uses a WebSocket client instead of the JSON-RPC client now, the co **Tip:** If you [run your own `rippled` server](the-rippled-server.html#reasons-to-run-your-own-server) you can connect to it using `ws://localhost:6006` as the URL. You can also use the WebSocket URLs of [public servers](public-servers.html) to connect to the Mainnet or other test networks. -### 3. Viewing an Account +### 3. Display an Account -**Full code for this step:** [`3_account.py`](TODO:link) +**Full code for this step:** [`3_account.py`](({{target.github_forkurl}}//tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/py/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](reserves.html). -When you do math on XRP amounts, you should import the `Decimal` class so that you don't get rounding errors. Add this to the top of the file: +The prompt is in a popup dialog like this: + +![Screenshot: step 3, account input prompt](img/python-wallet-3-enter.png) + +After the user inputs the prompt, the updated GUI looks like this: + +![Screenshot, step 3, showing account details](img/python-wallet-3-main.png) + +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") }} -Update the `watch_xrpl()` and `on_connected()` methods as follows: +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="class AutoGridBagSizer") }} +{{ 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 `watch_xrpl()` method has been renamed to `watch_xrpl_account()` because 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](subscribe.html#transaction-streams), because the `on_connected()` method now also subscribes to transactions for the provided account. 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][]. The `on_connected()` method now also calls [account_info][account_info method] on startup. In both cases, the worker passes the `account_data` object from the response back to the GUI using `wx.CallAfter()`. +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](subscribe.html#transaction-streams). 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`](https://docs.wxpython.org/wx.GridBagSizer.html) 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`](https://docs.wxpython.org/wx.StaticBox.html) 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](accountroot.html), 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: + +1. Create a new dialog, such as [`wx.TextEntryDialog`](https://docs.wxpython.org/wx.TextEntryDialog.html). +2. Use `showModal()` to display it to the user and get a return code based on which button the user clicked. +3. If the user clicked OK, get a value the user input, in this, whatever text the user entered in the box. +4. 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](https://docs.wxpython.org/wx.Event.html) 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](xrp-testnet-faucet.html) 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](tx-sender.html) 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`]({{target.github_forkurl}}//tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/py/4_tx_history.py) + + +***TODO*** + + +### 5. Send XRP + +**Full code for this step:** [`5_send_xrp.py`]({{target.github_forkurl}}//tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/py/5_send_xrp.py) + +***TODO*** + + +### 6. Domain Verification and Polish + +**Full code for this step:** [`6_verification_and_polish.py`]({{target.github_forkurl}}//tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/py/6_verification_and_polish.py) ***TODO*** diff --git a/img/python-wallet-2.gif b/img/python-wallet-2.gif new file mode 100644 index 0000000000000000000000000000000000000000..a799bc072efe0d4bd5f2d2dd18b08cac07f65575 GIT binary patch literal 11938 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 literal 0 HcmV?d00001 diff --git a/img/python-wallet-3-enter.png b/img/python-wallet-3-enter.png new file mode 100644 index 0000000000000000000000000000000000000000..50e7bf1c53f1d1738b3a13b12710887d836c0a23 GIT binary patch literal 9827 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 P7fA7~np~O8`>+28Qa6iZ literal 0 HcmV?d00001 diff --git a/img/python-wallet-3-main.png b/img/python-wallet-3-main.png new file mode 100644 index 0000000000000000000000000000000000000000..fba729aa6643b393bc00acf4f4d811b0cf37a163 GIT binary patch literal 21195 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 G+y4iBJ#hU1 literal 0 HcmV?d00001 diff --git a/tool/filter_include_code.py b/tool/filter_include_code.py index 1517b4ab19..b9124ad61f 100644 --- a/tool/filter_include_code.py +++ b/tool/filter_include_code.py @@ -18,6 +18,11 @@ def include_code(filename, lines="", mark_disjoint="", language="", start_i = s.find(start_with) if start_i == -1: raise ValueError("include_code: couldn't find start_with point '%s'"%start_with) + # Backtrack to start of the line + while start_i > 0: + if s[start_i] == "\n": + break + start_i -= 1 s = s[start_i:] # Truncate everything after the specified ending point (end_before) @@ -42,7 +47,28 @@ def include_code(filename, lines="", mark_disjoint="", language="", old_i = i s = s2 - return "```%s\n%s\n```" % (language, s.strip()) + if language == "py": + # Indentation in Python is meaningful so we can't remove all whitespace. + # Instead, remove only blank lines at the begining and end. + slines = s.split("\n") + s = "" + started = False + blank_tail = "" # Keep a running block of blank lines in case they're + # *between* meaningful lines. + for i, line in enumerate(slines): + if line.strip(): + started = True + s += blank_tail + blank_tail = "" + s += line + "\n" + elif started: + blank_tail += line + "\n" + + + else: + s = s.strip() + + return "```%s\n%s\n```" % (language, s) def parse_range(range_string): range_list = []