mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2025-12-06 17:27:57 +00:00
WIP revised tutorials IA
This commit is contained in:
@@ -1,555 +0,0 @@
|
||||
---
|
||||
html: build-a-desktop-wallet-in-python.html
|
||||
parent: python.html
|
||||
seo:
|
||||
description: Build a graphical desktop wallet for the XRPL in Python.
|
||||
---
|
||||
# Build a Desktop Wallet in Python
|
||||
<!-- STYLE_OVERRIDE: wallet -->
|
||||
|
||||
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](./get-started.md).
|
||||
- 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 {% repo-link path="_code-samples/build-a-desktop-wallet/py/" %}code samples section of this website's repository{% /repo-link %}.
|
||||
|
||||
## 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](../../../concepts/accounts/reserves.md).
|
||||
- Can send [direct XRP payments](../../../concepts/payment-types/direct-xrp-payments.md), 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 (**Disallow XRP** flag enabled).
|
||||
- If the address has a [verified domain name](../../../references/xrp-ledger-toml.md#account-verification) associated with it.
|
||||
|
||||
The application in this tutorial _doesn't_ have the ability to send or trade [tokens](../../../concepts/tokens/index.md) or use other [payment types](../../../concepts/payment-types/index.md) like Escrow or Payment Channels. However, it provides a foundation that you can implement those and other features on top of.
|
||||
|
||||
Other topics mentioned in this tutorial include 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:
|
||||
|
||||
```sh
|
||||
pip3 install --upgrade xrpl-py wxPython requests toml
|
||||
```
|
||||
|
||||
(On some systems, the command may be `pip` or you may need to use `sudo pip3` instead.)
|
||||
|
||||
This installs and upgrades the following Python libraries:
|
||||
|
||||
- [xrpl-py](https://xrpl-py.readthedocs.io/), a client library for the XRP Ledger. This tutorial requires **version 1.3.0 or higher**.
|
||||
- [wxPython](https://wxpython.org/), a cross-platform graphical toolkit.
|
||||
- [Requests](https://requests.readthedocs.io/), a library for 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](#6-domain-verification-and-polish), but you can install them now while you're installing other dependencies.
|
||||
|
||||
#### Notes for Windows Users
|
||||
|
||||
On Windows, you can build apps using either Windows natively or by using the Windows Subsystem for Linux (WSL). <!-- SPELLING_IGNORE: wsl -->
|
||||
|
||||
On native Windows, the GUI uses native Windows controls and should run without any dependencies beyond those mentioned above.
|
||||
|
||||
On WSL, you may need to install `libnotify-dev` as follows:
|
||||
|
||||
```sh
|
||||
apt-get install libnotify-dev
|
||||
```
|
||||
|
||||
If you have trouble installing wxPython on WSL, you can also try installing it this way:
|
||||
|
||||
```sh
|
||||
python -m pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-18.04 wxPython
|
||||
```
|
||||
|
||||
|
||||
### 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:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-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`](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. Show Ledger Updates
|
||||
|
||||
**Full code for this step:** {% repo-link path="_code-samples/build-a-desktop-wallet/py/2_threaded.py" %}`2_threaded.py`{% /repo-link %}.
|
||||
|
||||
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 the 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 each thread has variables it "owns" and doesn't write to the other thread's variables. In this program, each thread is its own class, so each thread should only write to its own class attributes (anything starting with `self.`). When the threads need to communicate, they use specific, "thread-safe" methods of communication, namely:
|
||||
|
||||
- For GUI to worker thread, use [`asyncio.run_coroutine_threadsafe()`](https://docs.python.org/3/library/asyncio-task.html#asyncio.run_coroutine_threadsafe).
|
||||
- For worker to GUI communications, use [`wx.CallAfter()`](https://docs.wxpython.org/wx.functions.html#wx.CallAfter).
|
||||
|
||||
To make full use of the XRP Ledger's ability to push messages to the client, use [xrpl-py's `AsyncWebsocketClient`](https://xrpl-py.readthedocs.io/en/stable/source/xrpl.asyncio.clients.html#xrpl.asyncio.clients.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.
|
||||
|
||||
{% admonition type="info" name="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.{% /admonition %}
|
||||
|
||||
Add these imports to the top of the file:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/2_threaded.py" from="import async" before="class XRPLMonitorThread" language="py" /%}
|
||||
|
||||
Then, the code for the monitor thread is as follows (put this in the same file as the rest of the app):
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/2_threaded.py" from="class XRPLMonitorThread" before="class TWaXLFrame" language="py" /%}
|
||||
|
||||
This code defines a `Thread` subclass for the worker. When the thread starts, it sets up an event loop, which waits for async tasks to be created and run. The code uses [`asyncio`'s Debug Mode](https://docs.python.org/3/library/asyncio-dev.html#asyncio-debug-mode) so that the console shows any errors that occur in async tasks.
|
||||
|
||||
The `watch_xrpl()` function is an example of a such a task (which the GUI thread starts when it's ready): it 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.
|
||||
|
||||
{% admonition type="success" name="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](https://xrpl-py.readthedocs.io/en/stable/source/xrpl.asyncio.clients.html#xrpl.asyncio.clients.AsyncWebsocketClient.request). Normally, you would also need to use `await` or something similar to get the response from any function you define with `async def`; but, in this app, the `run_bg_job()` helper takes care of that in a different way.{% /admonition %}
|
||||
|
||||
Update the code for the main thread and GUI frame to look like this:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/2_threaded.py" from="class TWaXLFrame" before="if __name__" language="py" /%}
|
||||
|
||||
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](https://docs.wxpython.org/sizers_overview.html) to control placement of the text within the frame.
|
||||
|
||||
{% admonition type="success" name="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](http://wxglade.sourceforge.net/). Separating the GUI code from the constructor may make it easier to switch to this type of approach later. <!-- SPELLING_IGNORE: wxGlade -->{% /admonition %}
|
||||
|
||||
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](../../../references/http-websocket-apis/public-api-methods/subscription-methods/subscribe.md#ledger-stream) 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:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/2_threaded.py" from="if __name__" language="py" /%}
|
||||
|
||||
Since the app uses a WebSocket client instead of the JSON-RPC client now, the code has to use a WebSocket URL to connect.
|
||||
|
||||
{% admonition type="success" name="Tip" %}If you [run your own `rippled` server](../../../concepts/networks-and-servers/index.md#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.md) to connect to the Mainnet or other test networks.{% /admonition %}
|
||||
|
||||
#### Troubleshooting SSL Certificate Errors
|
||||
|
||||
If you get an error like this, you may need to make sure your operating system's certificate authority store is updated:
|
||||
|
||||
```text
|
||||
[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate
|
||||
```
|
||||
|
||||
On macOS, run the [`Install Certificates.command`](https://stackoverflow.com/questions/52805115/certificate-verify-failed-unable-to-get-local-issuer-certificate) for your Python version.
|
||||
|
||||
On Windows, open Edge or Chrome and browse to <https://s1.ripple.com>, then close the page. This should be enough to update your system's certificates. (It doesn't work if you use Firefox or Safari, because those browser's don't use Windows' certificate validation APIs.) <!-- SPELLING_IGNORE: s1 -->
|
||||
|
||||
|
||||
### 3. Display an Account
|
||||
|
||||
**Full code for this step:** {% repo-link path="_code-samples/build-a-desktop-wallet/py/3_account.py" %}`3_account.py`{% /repo-link %}
|
||||
|
||||
Now that you have a working, ongoing connection to the XRP Ledger, it's time to start adding some "wallet" functionality that lets you manage an individual 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](../../../concepts/accounts/reserves.md).
|
||||
|
||||
The prompt is in a pop-up 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:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/3_account.py" from="from decimal" before="class XRPLMonitorThread" language="py" /%}
|
||||
|
||||
In the `XRPLMonitorThread` class, rename and update the `watch_xrpl()` method as follows:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/3_account.py" from="async def watch_xrpl" before="async def on_connected" language="py" /%}
|
||||
|
||||
The newly renamed `watch_xrpl_account()` method now 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](../../../references/http-websocket-apis/public-api-methods/subscription-methods/subscribe.md#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:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/3_account.py" from="async def on_connected" before="class AutoGridBagSizer" language="py" /%}
|
||||
|
||||
The `on_connected()` method now subscribes to transactions for the provided account (and the ledger stream too). 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:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/3_account.py" from="class AutoGridBagSizer" before="class TWaXLFrame" language="py" /%}
|
||||
|
||||
Update the `TWaXLFrame`'s constructor as follows:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/3_account.py" from="def __init__(self, url, test_network=True):" before="def build_ui(self):" language="py" /%}
|
||||
|
||||
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:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/3_account.py" from="def build_ui(self):" before="def run_bg_job(self, job):" language="py" /%}
|
||||
|
||||
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](../../../references/protocol/ledger-data/ledger-entry-types/accountroot.md), 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.)
|
||||
|
||||
{% admonition type="warning" name="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 thread-safe.{% /admonition %}
|
||||
|
||||
Add a new `prompt_for_account()` method to the `TWaXLFrame` class:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/3_account.py" from="def prompt_for_account" before="def update_ledger" language="py" /%}
|
||||
|
||||
The constructor calls this method to prompt the user for their [address](../../../concepts/accounts/addresses.md) or [master seed](../../../concepts/accounts/cryptographic-keys.md#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 instance of a dialog class, such as a [`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. This example gets the text the user entered in the box.
|
||||
4. Destroy the dialog instance. If you forget to do this, the application can leak memory whenever the user opens a new dialog.
|
||||
|
||||
From there, the `prompt_for_account()` 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.)
|
||||
|
||||
{% admonition type="success" name="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.{% /admonition %}
|
||||
|
||||
This code also binds an _event handler_, which is a method that is called whenever a certain type of thing happens to a particular part of 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:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/3_account.py" from="def toggle_dialog_style" before="def prompt_for_account" language="py" /%}
|
||||
|
||||
Event handlers generally take one positional argument, a [`wx.Event` object](https://docs.wxpython.org/wx.Event.html) which 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, the handler toggles the style back.
|
||||
|
||||
Add the following lines **at the end of** the `update_ledger()` method:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/3_account.py" from="# Save reserve settings" before="def calculate_reserve_xrp" language="py" /%}
|
||||
|
||||
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:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/3_account.py" from="def calculate_reserve_xrp" before="def update_account" language="py" /%}
|
||||
|
||||
Add an `update_account()` method to the `TWaXLFrame` class:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/3_account.py" from="def update_account" before="if __name__" language="py" /%}
|
||||
|
||||
The worker thread calls this method to pass account details to the GUI for display.
|
||||
|
||||
Lastly, towards the end of the file, in the `if __name__ == "__main__":` block, update the line that instantiates the `TWaXLFrame` class to pass the new `test_net` parameter:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/3_account.py" from="frame = TWaXLFrame" before="frame.Show()" language="py" /%}
|
||||
|
||||
(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](/resources/dev-tools/xrp-faucets) 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](/resources/dev-tools/tx-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:** {% repo-link path="_code-samples/build-a-desktop-wallet/py/4_tx_history.py" %}`4_tx_history.py`{% /repo-link %}
|
||||
|
||||
At this point, your wallet shows the account's balance getting updated, but doesn't show you anything about the actual transactions that caused the updates. So, the next step is to display the account's transaction history (and keep it updated).
|
||||
|
||||
The new transaction history displays in a new tab, like this:
|
||||
|
||||

|
||||
|
||||
Additionally, the app can produce desktop notifications (sometimes called "toasts"), which might look like this depending on your operating system:
|
||||
|
||||

|
||||
|
||||
First, add the following imports to get GUI classes for the table view and notifications:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/4_tx_history.py" from="import wx.dataview" before="import asyncio" language="py" /%}
|
||||
|
||||
Next, update the `watch_xrpl_account()` method of the worker class to pass transaction details to the GUI when you receive a transaction subscription message. This requires only one line:
|
||||
|
||||
```py
|
||||
wx.CallAfter(self.gui.add_tx_from_sub, message)
|
||||
```
|
||||
|
||||
The complete method should look like this:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/4_tx_history.py" from="async def watch_xrpl_account" before="async def on_connected" language="py" /%}
|
||||
|
||||
Have the worker use the [account_tx method][] to look up the account's transaction history and pass it to the GUI. This method gets a list of transactions that affected an account, including transactions from, to, or passing through the account in question, starting with the most recent by default. Add new code **to the end of** the `XRPLMonitorThread`'s `on_connected()` method, as follows:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/4_tx_history.py" from="# Get the first page of the account's transaction history" before="class AutoGridBagSizer" language="py" /%}
|
||||
|
||||
{% admonition type="info" name="Note" %}You may have to [paginate](../../../references/http-websocket-apis/api-conventions/markers-and-pagination.md) across multiple [account_tx][account_tx method] requests and responses if you want the _complete_ list of transactions that affected an account since its creation. This example does not show pagination, so the app only displays the most recent transactions to affect the account.{% /admonition %}
|
||||
|
||||
Now, edit the `build_ui()` method of the `TWaXLFrame` class. **Update the beginning of the method** to add a new [`wx.Notebook`](https://docs.wxpython.org/wx.Notebook.html), which makes a "tabs" interface, and make the `main_panel` into the first tab, as follows:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/4_tx_history.py" from="def build_ui" before="self.acct_info_area" language="py" /%}
|
||||
|
||||
Additionally, add a new tab for the transaction history to the **end of the** `build_ui()` method, as follows:
|
||||
|
||||
{{ include_code("_code-samples/build-a-desktop-wallet/py/4_tx_history.py", language="py", start_with="Tab 2: \"Transaction History\"", end_before="def run_bg_job") }}
|
||||
|
||||
This adds a second tab containing a [`wx.dataview.DataViewListCtrl`](https://docs.wxpython.org/wx.dataview.DataViewListCtrl.html), which is capable of displaying a bunch of info as a table. It sets up the table columns to show some relevant details of the account's transactions.
|
||||
|
||||
Add the following helper method to the `TWaXLFrame` class:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/4_tx_history.py" from="def displayable_amount" before="def add_tx_row" language="py" /%}
|
||||
|
||||
This method takes a [currency amount](../../../references/protocol/data-types/basic-data-types.md#specifying-currency-amounts) and converts it into a string for display to a human. Since it's used with the [`delivered_amount` field](../../../references/protocol/transactions/metadata.md#delivered_amount) in particular, it also handles the special case for pre-2014 partial payments where the delivered amount is unavailable.
|
||||
|
||||
After that, add another helper method to the `TWaXLFrame` class:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/4_tx_history.py" from="def add_tx_row" before="def update_account_tx" language="py" /%}
|
||||
|
||||
This method takes a transaction object, parses some of its fields into formats more suitable for displaying to users, and then adds it to the `DataViewListCtrl` in the transaction history tab.
|
||||
|
||||
Add a method to the `TWaXLFrame` class to update the transaction history based on the [account_tx response][account_tx method] from the worker thread, as follows:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/4_tx_history.py" from="def update_account_tx" before="def add_tx_from_sub" language="py" /%}
|
||||
|
||||
Lastly, add a similar method to the `TWaXLFrame` to add a single transaction to the transaction history table whenever the worker thread passes a transaction subscription message:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/4_tx_history.py" from="def add_tx_from_sub" before="if __name__" language="py" /%}
|
||||
|
||||
As before, you can test your wallet app with your own test account if you use the [Testnet Faucet](/resources/dev-tools/xrp-faucets) and the [Transaction Sender](/resources/dev-tools/tx-sender). On the Faucet page, select **Get Testnet credentials** (or use the same credentials from before). Input either the address or secret when you open your wallet app. On the Transaction Sender page, paste your address into the **Destination Address** field, click **Initialize**, then click various transaction buttons to see how your wallet displays the results.
|
||||
|
||||
|
||||
### 5. Send XRP
|
||||
|
||||
**Full code for this step:** {% repo-link path="_code-samples/build-a-desktop-wallet/py/5_send_xrp.py" %}`5_send_xrp.py`{% /repo-link %}
|
||||
|
||||
Until now, you've made the app able to view data from the ledger, and it's capable of showing the transactions an account has received. Now it's finally time to make the app capable of _sending_ transactions. For now, you can stick to sending [direct XRP payments](../../../concepts/payment-types/direct-xrp-payments.md) because there are more complexities involved in sending [issued tokens](../../../concepts/tokens/index.md).
|
||||
|
||||
The main window gets a new "Send XRP" button:
|
||||
|
||||

|
||||
|
||||
Clicking this button opens a dialog where the user can enter the details of the payment:
|
||||
|
||||

|
||||
|
||||
First, add the [regular expressions](https://docs.python.org/3/howto/regex.html) library to the list of imports at the top of the file:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/5_send_xrp.py" from="import re" before="from threading" language="py" /%}
|
||||
|
||||
In the `XRPLMonitorThread` class, add the following lines to the `on_connected()` method, anywhere **after getting a successful [account_info][account_info method] response**:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/5_send_xrp.py" from="if self.wallet:" before="# Get the first page" language="py" /%}
|
||||
|
||||
Add a new method to the `XRPLMonitorThread` class to send an XRP payment based on data the user provides, and alert the GUI when it has been sent:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/5_send_xrp.py" from="def send_xrp" before="class AutoGridBagSizer" language="py" /%}
|
||||
|
||||
In this flow, the app sends the transaction without waiting for it to be confirmed by the consensus process. You should be careful to mark any results from the initial submission as "pending" or "tentative" since the actual result of the transaction [isn't final until it's confirmed](../../../concepts/transactions/finality-of-results/index.md). Since the app is also subscribed to the account's transactions, it automatically gets notified when the transaction is confirmed.
|
||||
|
||||
Now, create a custom dialog for the user to input the necessary details for the payment:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/5_send_xrp.py" from="class SendXRPDialog" before="def on_to_edit" language="py" /%}
|
||||
|
||||
This subclass of [`wx.Dialog`](https://docs.wxpython.org/wx.Dialog.html) has several custom widgets, which are laid out using the `GridBagSizer` defined earlier. Notably, it has text boxes for the "To" address, the amount of XRP, and the [destination tag](../../../concepts/transactions/source-and-destination-tags.md) to use, if any. (A destination tag is kind of like a phone extension for an XRP Ledger address: for addresses owned by individuals, you don't need it, but if the destination address has many users then you need to specify it so that the destination knows which recipient you intended. It's common to need a destination tag to deposit at a cryptocurrency exchange.) The dialog also has **OK** and **Cancel** buttons, which automatically function to cancel or complete the dialog, although the "OK" button is labeled "Send" instead to make it clearer what the app does when the user clicks it.
|
||||
|
||||
The `SendXRPDialog` constructor also binds two event handlers for when the user inputs text in the "to" and "destination tag" fields, so you need the definitions for those handlers to the same class. First, add `on_to_edit()`:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/5_send_xrp.py" from="def on_to_edit" before="def on_dest_tag_edit" language="py" /%}
|
||||
|
||||
This checks the "To" address to ensure that it matches two conditions:
|
||||
|
||||
1. It's a validly formatted classic address or X-address.
|
||||
2. It's not the user's own address—you can't send XRP to yourself.
|
||||
|
||||
If either condition is not met, the handler disables the "Send" button for this dialog. If both conditions are met, it enables the "Send" button.
|
||||
|
||||
Next, add the `on_dest_tag_edit()` handler, also as a method of the `SendXRPDialog` class:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/5_send_xrp.py" from="def on_dest_tag_edit" before="class TWaXLFrame" language="py" /%}
|
||||
|
||||
In other GUI toolkits, you might be able to use a dedicated number entry control for the Destination Tag field, but with wxPython there is only a generic text entry field, so the `on_dest_tag_edit()` handler makes it behave more like a number-only control by instantly deleting any non-numeric characters the user tries to enter in the field.
|
||||
|
||||
From here, you need to edit the `TWaXLFrame` class. First, in the `build_ui()` method, you need to add a new "Send XRP" button, and bind it to a new event handler. Add the following lines **before the code to add things to the sizer**:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/5_send_xrp.py" from="# Send XRP button." before="self.ledger_info =" language="py" /%}
|
||||
|
||||
Still in the `build_ui()` method, add the new button to the `main_sizer` so it fits nicely in between the account info area and the ledger info area. The sizer code **at the end of the "Tab 1" section** should look like the following, including one new line and the previous (unchanged) lines:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/5_send_xrp.py" from="main_sizer = wx.BoxSizer" before="# Tab 2:" language="py" /%}
|
||||
|
||||
Also in the `build_ui()` method, initialize a dictionary to hold rows with pending transaction details, so that you can replace them with the confirmed results when those are available. Add this line **anywhere near the "Tab 2" section** that sets up `self.tx_list` code:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/5_send_xrp.py" from="self.pending_tx_rows = {}" before="objs_panel" language="py" /%}
|
||||
|
||||
The "Send XRP" button starts out disabled, so you need to add a new method to the `TWaXLFrame` class to enable it when the right conditions are met:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/5_send_xrp.py" from="def enable_readwrite" before="def displayable_amount" language="py" /%}
|
||||
|
||||
The changes you made to `on_connected()` earlier in this step call this method after successfully receiving account data, but only if the worker class has a `Wallet` instance—meaning the user input the secret key to an account that really exists. If the user input an address, this method never gets called.
|
||||
|
||||
Add the handler for when the user clicks the "Send XRP" button as a method of the `TWaXLFrame` class:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/5_send_xrp.py" from="def click_send_xrp" before="if __name__" language="py" /%}
|
||||
|
||||
This dialog opens a new "Send XRP" dialog using the custom `SendXRPDialog` class defined earlier in this step. If the user clicks the "Send" button, it passes the details to the worker thread to send the payment, and displays a notification that indicates the transaction is sending. (Note, the transaction can still fail after this point, so the notification does not say what the transaction did.)
|
||||
|
||||
Also add a new method to the `TWaXLFrame` class to display the pending transaction in the Transaction History pane when the worker thread sends it, as follows:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/5_send_xrp.py" from="def add_pending_tx" before="def click_send_xrp" language="py" /%}
|
||||
|
||||
This method is similar to the `add_tx_row()` method in that it processes a transaction for display and adds it to the Transaction History table. The differences are that it takes one of [xrpl-py's Transaction models](https://xrpl-py.readthedocs.io/en/stable/source/xrpl.models.transactions.html) rather than a JSON-like API response; and it handles certain columns differently because the transaction has not yet been confirmed. Importantly, it saves a reference to table row containing this transaction to the `pending_tx_rows` dictionary, so that later on when the transaction is confirmed, you can remove the table row for the pending version and replace it with the final version of the transaction.
|
||||
|
||||
Lastly, update the `add_tx_from_sub()` method so that it finds and updates pending transactions with their final results when those transactions are confirmed. Add the following lines **right before the call to** `self.add_tx_row()`:
|
||||
|
||||
{{ include_code("_code-samples/build-a-desktop-wallet/py/5_send_xrp.py", language="py", start_with="if t[\"tx\"][\"hash\"] in", end_before="self.add_tx_row(t, prepend=True)") }}
|
||||
|
||||
You can now use your wallet to send XRP! You can even fund an entirely new account. To do that:
|
||||
|
||||
1. Open the Python interpreter.
|
||||
|
||||
```
|
||||
python3
|
||||
```
|
||||
|
||||
{% admonition type="warning" name="Caution" %}Depending on your OS, the command may be `python` or `python3`. You want to open Python 3, not a Python 2.x version.{% /admonition %}
|
||||
|
||||
|
||||
2. Run the following commands in the Python interpreter:
|
||||
|
||||
```
|
||||
import xrpl
|
||||
w = xrpl.wallet.Wallet.create()
|
||||
print(w.address)
|
||||
print(w.seed)
|
||||
exit()
|
||||
```
|
||||
|
||||
Save the classic address and seed somewhere.
|
||||
|
||||
3. Open your wallet app and provide a **Secret** (seed) value from an already-funded address, such as one you got from the [Testnet Faucet](/resources/dev-tools/xrp-faucets).
|
||||
|
||||
4. Send at least the [base reserve](../../../concepts/accounts/reserves.md) (currently {% $env.PUBLIC_BASE_RESERVE %}) to the brand-new classic address you generated in the Python interpreter.
|
||||
|
||||
5. Wait for the transaction to be confirmed, then close your wallet app.
|
||||
|
||||
6. Open your wallet app and provide the seed value you generated in the Python interpreter.
|
||||
|
||||
7. You should see the balance and transaction history of your newly-funded account, matching the address you generated in the interpreter.
|
||||
|
||||
|
||||
### 6. Domain Verification and Polish
|
||||
|
||||
**Full code for this step:** {% repo-link path="_code-samples/build-a-desktop-wallet/py/6_verification_and_polish.py" %}`6_verification_and_polish.py`{% /repo-link %}
|
||||
|
||||
One of the biggest shortcomings of the wallet app from the previous step is that it doesn't provide a lot of protections or feedback for users to save them from human error and [DeFi scams](https://learn.xrpl.org/safeguarding-your-crypto-wallet-your-essential-checklist-against-defi-scams/). These sorts of protections are extra important when dealing with the cryptocurrency space, because decentralized systems like the XRP Ledger don't have an admin or support team you can ask to cancel or refund a payment if you made a mistake such as sending it to the wrong address. This step shows how to add some checks on destination addresses to warn the user before sending.
|
||||
|
||||
One type of check you can make is to verify the domain name associated with an XRP Ledger address; this is called [account domain verification](../../../references/xrp-ledger-toml.md#account-verification). When an account's domain is verified, you could show it like this:
|
||||
|
||||

|
||||
|
||||
When there are other errors, you can expose them to the user with an icon and a tooltip, which looks like this:
|
||||
|
||||

|
||||
|
||||
The following code implements account domain verification; **save it as a new file** named `verify_domain.py` in the same folder as your app's main file:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/verify_domain.py" language="py" /%}
|
||||
|
||||
**In your app's main file,** import the `verify_account_domain` function:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/6_verification_and_polish.py" from="from verify_domain" before="class XRPLMonitorThread" language="py" /%}
|
||||
|
||||
In the `XRPLMonitorThread` class, add a new `check_destination()` method to check the destination address, as follows:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/6_verification_and_polish.py" from="async def check_destination" before="async def send_xrp" language="py" /%}
|
||||
|
||||
This code uses [`xrpl.asyncio.account.get_account_info()`](https://xrpl-py.readthedocs.io/en/stable/source/xrpl.asyncio.account.html#xrpl.asyncio.account.get_account_info) to look up the account in the ledger; unlike using the client's `request()` method, `get_account_info()` raises an exception if the account is not found.
|
||||
|
||||
If the account _does_ exist, the code checks for the [`lsfDisallowXRP` flag](../../../references/protocol/ledger-data/ledger-entry-types/accountroot.md#accountroot-flags). Note that this is an `lsf` (ledger state flag) value because this is an object from the ledger state data; these are different than the flag values the [AccountSet transaction][] uses to configure the same settings.
|
||||
|
||||
Finally, the code decodes the account's `Domain` field, if present, and performs domain verification using the method imported above.
|
||||
|
||||
{% admonition type="warning" name="Caution" %}The background check takes the Send XRP dialog (`dlg`) as a parameter, since each dialog is a separate instance, but does not modify the dialog directly since that might not be thread-safe. (It _only_ uses `wx.CallAfter` to pass the results of the check back to the dialog.){% /admonition %}
|
||||
|
||||
After this, it's time to update the `SendXRPDialog` class to make it capable of displaying these errors. You can also set a more specific upper bound for how much XRP the account can actually send. Change the constructor to take a new parameter:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/6_verification_and_polish.py" from="def __init__(self, parent, max_send=100000000.0)" before="wx.Dialog.__init__" language="py" /%}
|
||||
|
||||
Add some icon widgets to the UI, also in the `SendXRPDialog` constructor:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/6_verification_and_polish.py" from="# Icons to indicate" before="lbl_to =" language="py" /%}
|
||||
|
||||
Still in the `SendXRPDialog` constructor, add a maximum value to the line that creates the `self.txt_amt` widget:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/6_verification_and_polish.py" from="self.txt_amt =" before="self.txt_amt.SetDigits(6)" language="py" /%}
|
||||
|
||||
Don't forget to add all the new widgets to the `SendXRPDialog`'s sizer so they fit in the right places. Update the `BulkAdd` call in the constructor as follows:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/6_verification_and_polish.py" from="sizer.BulkAdd(((lbl_to," before="sizer.Fit(self)" language="py" /%}
|
||||
|
||||
Next, refactor the `on_to_edit()` handler in the `SendXRPDialog` class to perform more checks, including the new background check on the destination address. The updated handler should be as follows:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/6_verification_and_polish.py" from="def on_to_edit" before="def on_dest_tag_edit" language="py" /%}
|
||||
|
||||
In addition to starting the background check, this handler does some checks immediately. Any check that doesn't require getting data from the network is probably fast enough to run directly in the handler; if the check requires network access, you have to run it in the worker thread instead.
|
||||
|
||||
One of the new checks is to decode X-addresses to pull out the additional data they encode:
|
||||
|
||||
- If the X-address includes a destination tag, show it in the destination tag field.
|
||||
- If the X-address is not intended for a test network and the app is connected to a test network (or the other way around), show an error.
|
||||
|
||||
One tricky bit of writing handlers like this in GUI code is that you have to be ready for the handler to be called many times as the user inputs and erases data. For example, if you disable a field when some input is invalid, you also have to enable it if the user changes their input to be valid.
|
||||
|
||||
The code shows the error icons when it finds errors (and hides them when it doesn't), and adds tooltips with the error message. You could, of course, display errors to the user in another way as well, such as additional pop-up dialogs or a status bar.
|
||||
|
||||
Moving on, you also need a new method in the `SendXRPDialog` class to process the results from the background check. Add the following code:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/6_verification_and_polish.py" from="def update_dest_info" before="class TWaXLFrame" language="py" /%}
|
||||
|
||||
This code takes the dictionary passed by the `check_destination()` and uses it to update various widgets in the Send XRP dialog's GUI.
|
||||
|
||||
You need to make a few small updates to configure the maximum send amount in the Send XRP dialog. Start by adding these lines to the `TWaXLFrame` class's constructor:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/6_verification_and_polish.py" from="# This account's total XRP reserve" before="self.build_ui()" language="py" /%}
|
||||
|
||||
Then modify the `update_account()` method of the `TWaXLFrame` to save the latest calculated reserve. Modify the last few lines to look like this:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/6_verification_and_polish.py" from="# Display account reserve and" before="def enable_readwrite" language="py" /%}
|
||||
|
||||
Finally, calculate the maximum amount the user can send and provide it to the Send XRP dialog. Modify **the beginning of the `click_send_xrp()` handler** as follows:
|
||||
|
||||
{% code-snippet file="/_code-samples/build-a-desktop-wallet/py/6_verification_and_polish.py" from="xrp_bal = Decimal" before="dlg.CenterOnScreen()" language="py" /%}
|
||||
|
||||
The formula this code uses to calculate the maximum amount the user can send is the account's XRP balance, minus its [reserve](../../../concepts/accounts/reserves.md) and minus the [transaction cost](../../../concepts/transactions/transaction-cost.md). The calculation uses the `Decimal` class to avoid rounding errors, but ultimately it has to be converted down to a `float` because that's what wxPython's [`wx.SpinCtrlDouble`](https://docs.wxpython.org/wx.SpinCtrlDouble.html) accepts for minimum and maximum values. Still there is less opportunity for floating-point rounding errors to occur if the conversion happens _after_ the other calculations.
|
||||
|
||||
Test your wallet app the same way you did in the previous steps. To test domain verification, try entering the following addresses in the "To" box of the Send XRP dialog:
|
||||
|
||||
| Address | Domain | Verified? |
|
||||
|:-------------------------------------|:-------------|:----------|
|
||||
| `rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW` | `mduo13.com` | ✅ Yes |
|
||||
| `rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn` | `xrpl.org` | ❌ No |
|
||||
| `rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe` | (Not set) | ❌ No |
|
||||
|
||||
To test X-addresses, try the following addresses:
|
||||
|
||||
| Address | Destination Tag | Test Net? |
|
||||
|:--------------------------------------------------|:----------------|:----------|
|
||||
| `T7YChPFWifjCAXLEtg5N74c7fSAYsvPKxzQAET8tbZ8q3SC` | 0 | Yes |
|
||||
| `T7YChPFWifjCAXLEtg5N74c7fSAYsvJVm6xKZ14AmjegwRM` | None | Yes |
|
||||
| `X7d3eHCXzwBeWrZec1yT24iZerQjYLjJrFT7A8ZMzzYWCCj` | 0 | No |
|
||||
| `X7d3eHCXzwBeWrZec1yT24iZerQjYLeTFXz1GU9RBnWr7gZ` | None | No |
|
||||
|
||||
|
||||
## Next Steps
|
||||
|
||||
Now that you have a functional wallet, you can take it in several new directions. The following are a few ideas:
|
||||
|
||||
- You could support more of the XRP Ledger's [transaction types](../../../references/protocol/transactions/types/index.md) including [tokens](../../../concepts/tokens/index.md) and [cross-currency payments](../../../concepts/payment-types/cross-currency-payments.md)
|
||||
- Example code for displaying token balances and other objects: {% repo-link path="_code-samples/build-a-desktop-wallet/py/7_owned_objects.py" %}`7_owned_objects.py`{% /repo-link %}
|
||||
- Allow the user to trade in the [decentralized exchange](../../../concepts/tokens/decentralized-exchange/index.md)
|
||||
- Add a way to request payments, such as with QR codes or URIs that open in your wallet.
|
||||
- Support better account security including [regular key pairs](../../../concepts/accounts/cryptographic-keys.md#regular-key-pair) or [multi-signing](../../../concepts/accounts/multi-signing.md).
|
||||
|
||||
{% raw-partial file="/docs/_snippets/common-links.md" /%}
|
||||
@@ -1,357 +0,0 @@
|
||||
---
|
||||
seo:
|
||||
description: Build a credential issuing microservice in Python.
|
||||
---
|
||||
# Build a Credential Issuing Service
|
||||
_(Requires the Credentials amendment. {% not-enabled /%})_
|
||||
|
||||
This tutorial demonstrates how to build and use a microservice that issues [Credentials](../../../concepts/decentralized-storage/credentials.md) on the XRP Ledger, in the form of a RESTlike API, using the [Flask](https://flask.palletsprojects.com/) framework for Python.
|
||||
|
||||
## Setup
|
||||
|
||||
First, download the complete sample code for this tutorial from GitHub:
|
||||
|
||||
- {% repo-link path="_code-samples/issue-credentials/py/" %}Credential Issuing Service sample code{% /repo-link %}
|
||||
|
||||
Then, in the appropriate directory, set up a virtual environment and install dependencies:
|
||||
|
||||
```sh
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
This should install appropriate versions of Flask and xrpl-py.
|
||||
|
||||
To use the API that this microservice provides, you also need an HTTP client such as [Postman](https://www.postman.com/downloads/), [RESTED](https://github.com/RESTEDClient/RESTED), or [cURL](https://curl.se/).
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
The Credential Issuer microservice, mostly implemented in `issuer_service.py`, provides a RESTlike API with the following methods:
|
||||
|
||||
| Method | Description |
|
||||
|---|----|
|
||||
| `POST /credential` | Request that the issuer issue a specific credential to a specific account. |
|
||||
| `GET /admin/credential` | List all credentials issued by the issuer's address, optionally filtering only for credentials that have or have not been accepted by their subject. |
|
||||
| `DELETE /admin/credential` | Delete a specific credential from the XRP Ledger, which revokes it. |
|
||||
|
||||
{% admonition type="info" name="Note" %}Some of the methods have `/admin` in the path because they are intended to be used by the microservice's administrator. However, the sample code does not implement any authentication.{% /admonition %}
|
||||
|
||||
The sample code also contains a simple commmandline interface for a user account to accept a credential issued to it, as `accept_credential.py`.
|
||||
|
||||
The other files contain helper code that is used by one or both tools.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Get Accounts
|
||||
|
||||
To use the credential issuing service, you need two accounts on the Devnet, where the Credentials amendment is already enabled. Go to the [XRP Faucets page](../../../../resources/dev-tools/xrp-faucets.page.tsx) and select **Devnet**. Then, click the button to Generate credentials, saving the key pair (address and secret), twice. You will use one of these accounts as a **credential issuer** and the other account as the **credential subject** (holder), so make a note of which is which.
|
||||
|
||||
### 2. Start Issuer Service
|
||||
|
||||
To start the issuer microservice in dev mode, run the following command from the directory with the sample code:
|
||||
|
||||
```sh
|
||||
flask --app issuer_service run
|
||||
```
|
||||
|
||||
On macOS, you may need to specify a different port number because the Flask default port, `5000`, is used by the OS's **AirPlay Receiver** service. For example:
|
||||
|
||||
```sh
|
||||
flask --app issuer_service run --port 5008
|
||||
```
|
||||
|
||||
It should prompt you for your **issuer account** seed. Input the secret key you saved previously and press Enter.
|
||||
|
||||
The output should look like the following:
|
||||
|
||||
```txt
|
||||
Issuer account seed:
|
||||
Starting credential issuer with XRPL address rJ6XzCCSapCaWZxExArkcBWLgJvT6bXCbV
|
||||
* Serving Flask app 'issuer_service'
|
||||
* Debug mode: off
|
||||
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
|
||||
* Running on http://127.0.0.1:5000
|
||||
Press CTRL+C to quit
|
||||
```
|
||||
|
||||
Double-check that the XRPL address displayed matches the address of the credential issuer keys you saved earlier.
|
||||
|
||||
### 3. Request Credential
|
||||
|
||||
To request a credential, make a request such as the following:
|
||||
|
||||
{% tabs %}
|
||||
|
||||
{% tab label="Summary" %}
|
||||
* HTTP method: `POST`
|
||||
* URL: `http://localhost:5000/credential`
|
||||
{% admonition type="info" name="Note for macOS Users" %}If you specified a different port when starting the service, change `:5000` in this and other examples to match the port you specified.{% /admonition %}
|
||||
* Headers:
|
||||
* `Content-Type: application/json`
|
||||
* Request Body:
|
||||
```json
|
||||
{
|
||||
"subject": "rGtnKx7veDhV9CgYenkiCV5HMLpgU2BfcQ",
|
||||
"credential": "TestCredential",
|
||||
"documents": {
|
||||
"reason": "please"
|
||||
}
|
||||
}
|
||||
```
|
||||
{% /tab %}
|
||||
|
||||
{% tab label="cURL" %}
|
||||
```sh
|
||||
curl -H "Content-Type: application/json" -X POST -d '{"subject": "rGtnKx7veDhV9CgYenkiCV5HMLpgU2BfcQ", "credential": "TestCredential", "documents": {"reason": "please"}}' http://localhost:5000/credential
|
||||
```
|
||||
{% /tab %}
|
||||
|
||||
{% /tabs %}
|
||||
|
||||
The parameters of the JSON request body should be as follows:
|
||||
|
||||
| Field | Type | Required? | Description |
|
||||
|---|---|---|---|
|
||||
| `subject` | String - Address | Yes | The XRPL classic address of the subject of the credential. Set this to the address that you generated at the start of this tutorial for the credential holder account. |
|
||||
| `credential` | String | Yes | The type of credential to issue. The example microservice accepts any string consisting of alphanumeric characters as well as the special characters underscore (`_`), dash (`-`), and period (`.`), with a minimum length of 1 and a maximum length of 64 characters. |
|
||||
| `documents` | Object | Yes | As a credential issuer, you typically need to verify some confidential information about someone before you issue them a credential. As a placeholder, the sample code checks for a nested field named `reason` that contains the string `please`. |
|
||||
| `expiration` | String - ISO8601 Datetime | No | The time after which the credential expires, such as `2025-12-31T00:00:00Z`. |
|
||||
| `uri` | String | No | Optional URI data to store with the credential. This data will become public on the XRP Ledger. If provided, this must be a string with minimum length 1 and max length 256, consisting of only characters that are valid in URIs, which are numbers, letters, and the following special characters: `-._~:/?#[]@!$&'()*+,;=%`. Conventionally, it should link to a Verifiable Credential document as defined by the W3C. |
|
||||
|
||||
This microservice immediately issues any credential that the user requests. A successful response from the API uses the HTTP status code `201 Created` and has a response body with the result of submitting the transaction to the XRP Ledger. You can use the `hash` or `ctid` value from the response to look up the transaction using an explorer such as [https://devnet.xrpl.org/](https://devnet.xrpl.org/).
|
||||
|
||||
{% admonition type="warning" name="Differences from Production" %}For a real credential issuer, you would probably check the credential type and only issue specific types of credentials, or maybe even just one type. <br><br> If checking the user's documents requires human intervention or takes longer than the amount of time an API request should wait to respond, you would need to store credential requests to some kind of storage, like an SQL database. You might also want to add a separate method for admins (or automated processes) to reject or issue the credential after checking the documents.{% /admonition %}
|
||||
|
||||
### 4. List Credentials
|
||||
|
||||
To show a list of credentials issued by the issuing account, make the following request:
|
||||
|
||||
{% tabs %}
|
||||
|
||||
{% tab label="Summary" %}
|
||||
* HTTP method: `GET`
|
||||
* URL: `http://localhost:5000/admin/credential`
|
||||
* Query parameters (optional): Use `?accepted=yes` to filter results to only credentials that the subject has accepted, or `?accepted=no` for credentials the user has not accepted.
|
||||
{% /tab %}
|
||||
|
||||
{% tab label="cURL" %}
|
||||
```sh
|
||||
curl http://localhost:5000/admin/credential
|
||||
```
|
||||
{% /tab %}
|
||||
|
||||
{% /tabs %}
|
||||
|
||||
A response could look like the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"credentials": [
|
||||
{
|
||||
"accepted": false,
|
||||
"credential": "TestCredential",
|
||||
"subject": "rGtnKx7veDhV9CgYenkiCV5HMLpgU2BfcQ"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
In the response, each entry in the `credentials` array represents a Credential issued by the issuer account and stored in the blockchain. The details should match the request from the previous step, except that the `documents` are omitted because they are not saved on the blockchain.
|
||||
|
||||
### 5. Accept Credential
|
||||
|
||||
For a credential to be valid, the subject of the credential has to accept it. You can use `accept_credential.py` to do this:
|
||||
|
||||
```sh
|
||||
python accept_credential.py
|
||||
```
|
||||
|
||||
It should prompt you for your **subject account** seed. Input the secret key you saved previously and press Enter.
|
||||
|
||||
The script displays a list of Credentials that have been issued to your account and have not been accepted yet. Input the number that corresponds to the credential you want to accept, then press Enter. For example:
|
||||
|
||||
```txt
|
||||
Accept a credential?
|
||||
0) No, quit.
|
||||
1) 'TestCredential' issued by rJ6XzCCSapCaWZxExArkcBWLgJvT6bXCbV
|
||||
2) 'AnotherTestCredential' issued by rJ6XzCCSapCaWZxExArkcBWLgJvT6bXCbV
|
||||
Select an option (0-2): 1
|
||||
```
|
||||
|
||||
The script signs and submits a transaction to accept the specified credential, and prints the output to the console. You can use the `hash` or `ctid` value to look up the transaction using an explorer.
|
||||
|
||||
### 6. Revoke Credential
|
||||
|
||||
To revoke an issued credential, make a request such as the following:
|
||||
|
||||
{% tabs %}
|
||||
|
||||
{% tab label="Summary" %}
|
||||
* HTTP method: `DELETE`
|
||||
* URL: `http://localhost:5000/admin/credential`
|
||||
* Headers:
|
||||
* `Content-Type: application/json`
|
||||
* Request Body:
|
||||
```json
|
||||
{
|
||||
"subject": "rGtnKx7veDhV9CgYenkiCV5HMLpgU2BfcQ",
|
||||
"credential": "TestCredential"
|
||||
}
|
||||
```
|
||||
{% /tab %}
|
||||
|
||||
{% tab label="cURL" %}
|
||||
```sh
|
||||
curl -H "Content-Type: application/json" -X DELETE -d '{"subject": "rGtnKx7veDhV9CgYenkiCV5HMLpgU2BfcQ", "credential": "TestCredential"}' http://localhost:5000/admin/credential
|
||||
```
|
||||
{% /tab %}
|
||||
|
||||
{% /tabs %}
|
||||
|
||||
The parameters of the JSON request body should be as follows:
|
||||
|
||||
| Field | Type | Required? | Description |
|
||||
|---|---|---|---|
|
||||
| `subject` | String - Address | Yes | The XRPL classic address of the subject of the credential to revoke. |
|
||||
| `credential` | String | Yes | The type of credential to revoke. This must match a credential type previously issued. |
|
||||
|
||||
A successful response from the API uses the HTTP status code `200 OK` and has a response body with the result of submitting the transaction to the XRP Ledger. You can use the `hash` or `ctid` value from the response to look up the transaction using an explorer.
|
||||
|
||||
## Code Walkthrough
|
||||
|
||||
The code for this tutorial is divided among the following files:
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `accept_credential.py` | Commandline interface for a credential subject to look up and accept Credentials. |
|
||||
| `credential_mode.py` | A model class for Credentials that validates user input, and maps between the microservice's simplified Credential format and the full XRPL representation of Credentials. |
|
||||
| `decode_hex.py` | A helper function for decoding hexadecimal into human-readable strings, used by both the credential issuer and holder. |
|
||||
| `issuer_service.py` | Defines the microservice as a Flask app, including API methods and error handling. |
|
||||
| `look_up_credentials.py` | A helper function for looking up Credentials tied to an account, including pagination and filtering, used by both the credential issuer and holder. |
|
||||
|
||||
### accept_credential.py
|
||||
|
||||
This file is meant to be run as a commandline tool so it starts with a [shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)), followed by dependencies grouped by type: standard lib, then PyPI packages, and local files last.
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/accept_credential.py" language="py" before="XRPL_SERVER =" /%}
|
||||
|
||||
It then defines the XRPL client and sets up a `Wallet` instance with the subject account's key pair, using a seed either passed as an environment variable or input as a password:
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/accept_credential.py" language="py" from="XRPL_SERVER =" before="pending_credentials = " /%}
|
||||
|
||||
It looks up pending credentials using the `look_up_credentials(...)` function imported from `look_up_credentials.py`:
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/accept_credential.py" language="py" from="pending_credentials = " before="prompt = " /%}
|
||||
|
||||
Next is a text menu that displays each of the unaccepted credentials returned by the lookup, as well as the option to quit:
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/accept_credential.py" language="py" from="prompt = " before="chosen_cred = " /%}
|
||||
|
||||
Finally, if the user picked a credential, the code constructs a [CredentialAccept transaction][], signs and submits it, and waits for it to be validated by consensus before displaying the result.
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/accept_credential.py" language="py" from="chosen_cred = " /%}
|
||||
|
||||
### issuer_service.py
|
||||
|
||||
This file defines the Flask app of the issuer microservice. It opens by importing dependencies, grouped into standard lib, PyPI dependencies, and lastly local files:
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/issuer_service.py" language="py" before="# Set up" /%}
|
||||
|
||||
It then defines the XRPL client and sets up a `Wallet` instance with the issuer account's key pair, using a seed either passed as an environment variable or input as a password:
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/issuer_service.py" language="py" from="# Set up" before="# Define Flask app" /%}
|
||||
|
||||
Next, it creates the Flask app:
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/issuer_service.py" language="py" from="# Define Flask app" before="# Method for users" /%}
|
||||
|
||||
After that come the definitions for the three API methods, starting with `POST /credential`. Users call this method to request a credential from the service. This method parses the request body as JSON and instantiates a `CredentialRequest` object—one of the data models defined in `credential_model.py`. If this succeeds, it uses the data to fill out a CredentialCreate transaction. Finally, it checks the transaction's [result code](../../../references/protocol/transactions/transaction-results/index.md) to decide which HTTP response code to use:
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/issuer_service.py" language="py" from="# Method for users to request a credential from the service" before="# Method for admins to look up all credentials issued" /%}
|
||||
|
||||
The next API method is `GET /admin/credential`, which looks up credentials issued by the service. It uses the `look_up_credentials(...)` method defined in `look_up_credentials.py` to get a list of credentials. It uses the `Credential` data model, imported from `credential_model.py`, to transform each ledger entry from the XRP Ledger format to the simplified representation the microservice uses.
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/issuer_service.py" language="py" from="# Method for admins to look up all credentials issued" before="# Method for admins to revoke an issued credential" /%}
|
||||
|
||||
The final API method, `DELETE /admin/credential`, deletes a Credential from the ledger, revoking it. This again uses the `Credential` data model to validate user inputs and translate them into XRPL format where necessary. After that, it attempts to look up the Credential in the ledger and returns an error if it doesn't exist. This way, the issuer doesn't have to pay the cost of sending a transaction that would fail. Finally, the method checks the transaction result code and sets the HTTP response code accordingly.
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/issuer_service.py" language="py" from="# Method for admins to revoke an issued credential" before="# Error handling" /%}
|
||||
|
||||
Finally, the file ends by adding error handlers for a variety of errors that can be raised by the API methods, including in the data models or by xrpl-py's API methods:
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/issuer_service.py" language="py" from="# Error handling" /%}
|
||||
|
||||
### look_up_credentials.py
|
||||
|
||||
This file implements lookup of Credentials. Both the issuer code and the subject code use this function to look up their own credentials.
|
||||
|
||||
This code performs [pagination using markers](../../../references/http-websocket-apis/api-conventions/markers-and-pagination.md) to get all the results from the ledger. It also filters results based on the issuer/subject account, so that lookup by issuer, for example, doesn't include credentials that someone else issued _to_ the issuer account. Finally, it can optionally check the accepted status of the Credentials and only include ones that are or aren't accepted.
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/look_up_credentials.py" language="py" /%}
|
||||
|
||||
### decode_hex.py
|
||||
|
||||
This file implements conversion of hex strings to human-readable text using ASCII, where possible. If the hex can't be decoded, it returns the original text prefaced with `(BIN) ` as a graceful fallback instead of throwing an error. This is important when reading data from the XRP Ledger because other users and tools can create Credentials with arbitrary binary data which might not decode to actual text at all. Even though the microservice from this tutorial only creates Credentials that use a restricted subset of ASCII characters, it might need to read ledger data that was created with different tools and different rules. You might even want to put more restrictions or checks in place depending on how you use the data; for example, if you output the results to a webpage you should make sure to escape or strip HTML tags to avoid visual glitches or cross-site-scripting attacks.
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/decode_hex.py" language="py" /%}
|
||||
|
||||
### credential_model.py
|
||||
|
||||
This file implements the simplified "Credential" data model that the issuer microservice uses to represent credentials. It performs validation of user input and conversion between formats.
|
||||
|
||||
The file starts with importing dependencies grouped by type:
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/credential_model.py" language="py" before="def is_allowed_credential_type" /%}
|
||||
|
||||
It then has a function to validate the credential type, using a regular expression that checks the length and characters used:
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/credential_model.py" language="py" from="def is_allowed_credential_type" before="def is_allowed_uri" /%}
|
||||
|
||||
It uses a similar function to validate user-provided URI values:
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/credential_model.py" language="py" from="def is_allowed_uri" before="class Credential" /%}
|
||||
|
||||
The main export of this file is the `Credential` class. Most of the methods use this class, or a class derived from it, to read user input from the API.
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/credential_model.py" language="py" from="class Credential" before="def __init__(self, d: dict):" /%}
|
||||
|
||||
The default constructor for the Credential class checks that user input meets various requirements. It uses the `dict.get(key)` method, which returns `None` instead of raising an error when the key doesn't exist, to set optional fields to `None`. It also parses the user-provided timestamp from a string to a native Python `datetime` object if necessary.
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/credential_model.py" language="py" from=" def __init__(self, d: dict):" before="@classmethod" /%}
|
||||
|
||||
The `from_xrpl(...)` class method is an alternate constructor for the Credential class. It takes a dictionary in the XRP Ledger's native format and decodes it to the native Python formats the Credential class expects (for example, converting the `credential` field from hexadecimal to a native string). The API methods that read data from the XRP Ledger use this constructor so that their output is formatted the same way as user input in the other API methods.
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/credential_model.py" language="py" from=" @classmethod" before="def to_dict(self):" /%}
|
||||
|
||||
The `to_dict(self)` method builds a dictionary representation for the Credential object, which can then be returned by the API as JSON. It converts from a `datetime` back to an ISO 8601 string and omits optional fields instead of including them with a `None` or `null` value.
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/credential_model.py" language="py" from=" def to_dict(self):" before="def to_xrpl(self):" /%}
|
||||
|
||||
The `to_xrpl(self)` method returns a different class of object, `XrplCredential`, which is formatted for submitting to the XRP Ledger:
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/credential_model.py" language="py" from=" def to_xrpl(self):" before="class XrplCredential:" /%}
|
||||
|
||||
The implementation of `XrplCredential` performs the necessary conversions in its constructor:
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/credential_model.py" language="py" from="class XrplCredential:" before="class CredentialRequest(Credential):" /%}
|
||||
|
||||
Finally, the `CredentialRequest` class inherits from the `Credential` class but checks for an additional field, `documents`. For a realistic credential issuer, you might require the user to provide specific documents in the request body, like a photo of their government-issued ID or a cryptographically signed message from another business, which your code would check. For this tutorial, the check is only a placeholder:
|
||||
|
||||
{% code-snippet file="/_code-samples/issue-credentials/py/credential_model.py" language="py" from="class CredentialRequest(Credential):" /%}
|
||||
|
||||
## Next Steps
|
||||
|
||||
Using this service as a base, you can extend the service with more features, such as:
|
||||
|
||||
- Security/authentication to protect API methods from unauthorized use.
|
||||
- Actually checking user documents to decide if you should issue a credential.
|
||||
|
||||
Alternatively, you can use credentials to for various purposes, such as:
|
||||
|
||||
- Define a [Permissioned Domain](/docs/concepts/tokens/decentralized-exchange/permissioned-domains) that uses your credentials to grant access to features on the XRP Ledger.
|
||||
- [Verify credentials](../compliance/verify-credential.md) manually to grant access to services that exist off-ledger.
|
||||
|
||||
{% raw-partial file="/docs/_snippets/common-links.md" /%}
|
||||
@@ -1,210 +0,0 @@
|
||||
---
|
||||
seo:
|
||||
description: Build a Python app that interacts with the XRP Ledger.
|
||||
labels:
|
||||
- Development
|
||||
---
|
||||
# Get Started Using Python Library
|
||||
|
||||
This tutorial walks you through the basics of building an XRP Ledger-connected application using [`xrpl-py`](https://github.com/XRPLF/xrpl-py), a pure [Python](https://www.python.org) library built to interact with the XRP Ledger using native Python models and methods.
|
||||
|
||||
This tutorial is intended for beginners and should take no longer than 30 minutes to complete.
|
||||
|
||||
## Learning Goals
|
||||
|
||||
In this tutorial, you'll learn:
|
||||
|
||||
* The basic building blocks of XRP Ledger-based applications.
|
||||
* How to connect to the XRP Ledger using `xrpl-py`.
|
||||
* How to get an account on the [Testnet](/resources/dev-tools/xrp-faucets) using `xrpl-py`.
|
||||
* How to use the `xrpl-py` library to look up information about an account on the XRP Ledger.
|
||||
* How to put these steps together to create a Python app.
|
||||
|
||||
## Requirements
|
||||
|
||||
The `xrpl-py` library supports [Python 3.7](https://www.python.org/downloads/) and later.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
The [`xrpl-py` library](https://github.com/XRPLF/xrpl-py) is available on [PyPI](https://pypi.org/project/xrpl-py/). Install with `pip`: <!-- SPELLING_IGNORE: pypi -->
|
||||
|
||||
|
||||
```py
|
||||
pip3 install xrpl-py
|
||||
```
|
||||
|
||||
## Start Building
|
||||
|
||||
When you're working with the XRP Ledger, there are a few things you'll need to manage, whether you're adding XRP to your [account](../../../concepts/accounts/index.md), integrating with the [decentralized exchange](../../../concepts/tokens/decentralized-exchange/index.md), or [issuing tokens](../../../concepts/tokens/index.md). This tutorial walks you through basic patterns common to getting started with all of these use cases and provides sample code for implementing them.
|
||||
|
||||
Here are the basic steps you'll need to cover for almost any XRP Ledger project:
|
||||
|
||||
1. [Connect to the XRP Ledger.](#1-connect-to-the-xrp-ledger)
|
||||
1. [Get an account.](#2-get-account)
|
||||
1. [Query the XRP Ledger.](#3-query-the-xrp-ledger)
|
||||
|
||||
|
||||
### 1. Connect to the XRP Ledger
|
||||
|
||||
To make queries and submit transactions, you need to connect to the XRP Ledger. To do this with `xrpl-py`, use the [`xrp.clients` module](https://xrpl-py.readthedocs.io/en/latest/source/xrpl.clients.html):
|
||||
|
||||
{% code-snippet file="/_code-samples/get-started/py/get-acct-info.py" from="# Define the network client" before="# Create a wallet using the testnet faucet:" language="py" /%}
|
||||
|
||||
#### Connect to the production XRP Ledger
|
||||
|
||||
The sample code in the previous section shows you how to connect to the Testnet, which is a [parallel network](../../../concepts/networks-and-servers/parallel-networks.md) for testing where the money has no real value. When you're ready to integrate with the production XRP Ledger, you'll need to connect to the Mainnet. You can do that in two ways:
|
||||
|
||||
* By [installing the core server](../../../infrastructure/installation/index.md) (`rippled`) and running a node yourself. The core server connects to the Mainnet by default, but you can [change the configuration to use Testnet or Devnet](../../../infrastructure/configuration/connect-your-rippled-to-the-xrp-test-net.md). [There are good reasons to run your own core server](../../../concepts/networks-and-servers/index.md#reasons-to-run-your-own-server). If you run your own server, you can connect to it like so:
|
||||
|
||||
```
|
||||
from xrpl.clients import JsonRpcClient
|
||||
JSON_RPC_URL = "http://localhost:5005/"
|
||||
client = JsonRpcClient(JSON_RPC_URL)
|
||||
```
|
||||
|
||||
See the example [core server config file](https://github.com/XRPLF/rippled/blob/c0a0b79d2d483b318ce1d82e526bd53df83a4a2c/cfg/rippled-example.cfg#L1562) for more information about default values.
|
||||
|
||||
* By using one of the available [public servers][]:
|
||||
|
||||
```
|
||||
from xrpl.clients import JsonRpcClient
|
||||
JSON_RPC_URL = "https://s2.ripple.com:51234/"
|
||||
client = JsonRpcClient(JSON_RPC_URL)
|
||||
```
|
||||
|
||||
|
||||
### 2. Get account
|
||||
|
||||
To store value and execute transactions on the XRP Ledger, you need an account: a [set of keys](../../../concepts/accounts/cryptographic-keys.md#key-components) and an [address](../../../concepts/accounts/addresses.md) that's been [funded with enough XRP](../../../concepts/accounts/index.md#creating-accounts) to meet the [account reserve](../../../concepts/accounts/reserves.md). The address is the identifier of your account and you use the [private key](../../../concepts/accounts/cryptographic-keys.md#private-key) to sign transactions that you submit to the XRP Ledger.
|
||||
|
||||
For testing and development purposes, you can use the [XRP Faucets](/resources/dev-tools/xrp-faucets) to generate keys and fund the account on the Testnet or Devnet. For production purposes, you should take care to store your keys and set up a [secure signing method](../../../concepts/transactions/secure-signing.md). Another difference in production is that XRP has real worth, so you can't get it for free from a faucet.
|
||||
|
||||
To create and fund an account on the Testnet, `xrpl-py` provides the [`generate_faucet_wallet`](https://xrpl-py.readthedocs.io/en/latest/source/xrpl.wallet.html#xrpl.wallet.generate_faucet_wallet) method:
|
||||
|
||||
{% code-snippet file="/_code-samples/get-started/py/get-acct-info.py" from="# Create a wallet using the testnet faucet:" before="# Create an account str from the wallet" language="py" /%}
|
||||
|
||||
|
||||
This method returns a [`Wallet` instance](https://xrpl-py.readthedocs.io/en/latest/source/xrpl.wallet.html#xrpl.wallet.Wallet):
|
||||
|
||||
|
||||
```py
|
||||
print(test_wallet)
|
||||
|
||||
# print output
|
||||
|
||||
public_key:: 022FA613294CD13FFEA759D0185007DBE763331910509EF8F1635B4F84FA08AEE3
|
||||
private_key:: -HIDDEN-
|
||||
classic_address: raaFKKmgf6CRZttTVABeTcsqzRQ51bNR6Q
|
||||
```
|
||||
|
||||
#### Using the account
|
||||
|
||||
In this tutorial we only query details about the generated account from the XRP Ledger, but you can use the values in the `Wallet` instance to prepare, sign, and submit transactions with `xrpl-py`.
|
||||
|
||||
##### Prepare
|
||||
|
||||
To prepare the transaction:
|
||||
|
||||
{% code-snippet file="/_code-samples/get-started/py/prepare-payment.py" from="# Prepare payment" before="# print prepared payment" language="py" /%}
|
||||
|
||||
|
||||
|
||||
##### Sign and submit
|
||||
|
||||
To sign and submit the transaction:
|
||||
|
||||
{% code-snippet file="/_code-samples/get-started/py/prepare-payment.py" from="# Sign and submit the transaction" before="# Print tx response" language="py" /%}
|
||||
|
||||
|
||||
##### Derive an X-address
|
||||
|
||||
You can use `xrpl-py`'s [`xrpl.core.addresscodec`](https://xrpl-py.readthedocs.io/en/latest/source/xrpl.core.addresscodec.html) module to derive an [X-address](https://xrpaddress.info/) from the `Wallet.address` field:
|
||||
|
||||
{% code-snippet file="/_code-samples/get-started/py/get-acct-info.py" from="# Derive an x-address from the classic address:" before="# Look up info about your account" language="py" /%}
|
||||
|
||||
The X-address format [packs the address and destination tag](https://github.com/XRPLF/XRPL-Standards/issues/6) into a more user-friendly value.
|
||||
|
||||
|
||||
### 3. Query the XRP Ledger
|
||||
|
||||
You can query the XRP Ledger to get information about [a specific account](../../../references/http-websocket-apis/public-api-methods/account-methods/index.md), [a specific transaction](../../../references/http-websocket-apis/public-api-methods/transaction-methods/tx.md), the state of a [current or a historical ledger](../../../references/http-websocket-apis/public-api-methods/ledger-methods/index.md), and [the XRP Ledger's decentralized exchange](../../../references/http-websocket-apis/public-api-methods/path-and-order-book-methods/index.md). You need to make these queries, among other reasons, to look up account info to follow best practices for [reliable transaction submission](../../../concepts/transactions/reliable-transaction-submission.md).
|
||||
|
||||
Here, we use `xrpl-py`'s [`xrpl.account`](https://xrpl-py.readthedocs.io/en/latest/source/xrpl.account.html) module to look up information about the [account we got](#2-get-account) in the previous step.
|
||||
|
||||
|
||||
{% code-snippet file="/_code-samples/get-started/py/get-acct-info.py" from="# Look up info about your account" language="py" /%}
|
||||
|
||||
|
||||
|
||||
### 4. Putting it all together
|
||||
|
||||
Using these building blocks, we can create a Python app that:
|
||||
|
||||
1. Gets an account on the Testnet.
|
||||
2. Connects to the XRP Ledger.
|
||||
3. Looks up and prints information about the account you created.
|
||||
|
||||
|
||||
{% code-snippet file="/_code-samples/get-started/py/get-acct-info.py" language="python" /%}
|
||||
|
||||
To run the app, you can copy and paste the code into an editor or IDE and run it from there. Or you could download the file from the [XRP Ledger Dev Portal repo](https://github.com/XRPLF/xrpl-dev-portal/tree/master/_code-samples/get-started/py) and run it locally:
|
||||
|
||||
|
||||
```sh
|
||||
git clone git@github.com:XRPLF/xrpl-dev-portal.git
|
||||
cd xrpl-dev-portal/_code-samples/get-started/py/get-acct-info.py
|
||||
python3 get-acct-info.py
|
||||
```
|
||||
|
||||
You should see output similar to this example:
|
||||
|
||||
```sh
|
||||
Classic address:
|
||||
|
||||
rnQLnSEA1YFMABnCMrkMWFKxnqW6sQ8EWk
|
||||
X-address:
|
||||
|
||||
T7dRN2ktZGYSTyEPWa9SyDevrwS5yDca4m7xfXTGM3bqff8
|
||||
response.status: ResponseStatus.SUCCESS
|
||||
{
|
||||
"account_data": {
|
||||
"Account": "rnQLnSEA1YFMABnCMrkMWFKxnqW6sQ8EWk",
|
||||
"Balance": "1000000000",
|
||||
"Flags": 0,
|
||||
"LedgerEntryType": "AccountRoot",
|
||||
"OwnerCount": 0,
|
||||
"PreviousTxnID": "5A5203AFF41503539D11ADC41BE4185761C5B78B7ED382E6D001ADE83A59B8DC",
|
||||
"PreviousTxnLgrSeq": 16126889,
|
||||
"Sequence": 16126889,
|
||||
"index": "CAD0F7EF3AB91DA7A952E09D4AF62C943FC1EEE41BE926D632DDB34CAA2E0E8F"
|
||||
},
|
||||
"ledger_current_index": 16126890,
|
||||
"queue_data": {
|
||||
"txn_count": 0
|
||||
},
|
||||
"validated": false
|
||||
}
|
||||
```
|
||||
|
||||
#### Interpreting the response
|
||||
|
||||
The response fields that you want to inspect in most cases are:
|
||||
|
||||
* `account_data.Sequence` — This is the sequence number of the next valid transaction for the account. You need to specify the sequence number when you prepare transactions. With `xrpl-py`, you can use the [`get_next_valid_seq_number`](https://xrpl-py.readthedocs.io/en/latest/source/xrpl.account.html#xrpl.account.get_next_valid_seq_number) to get this automatically from the XRP Ledger. See an example of this usage in the project [README](https://github.com/XRPLF/xrpl-py#serialize-and-sign-transactions).
|
||||
|
||||
* `account_data.Balance` — This is the account's balance of [XRP, in drops][]. You can use this to confirm that you have enough XRP to send (if you're making a payment) and to meet the [current transaction cost](../../../concepts/transactions/transaction-cost.md#current-transaction-cost) for a given transaction.
|
||||
|
||||
* `validated` — Indicates whether the returned data is from a [validated ledger](../../../concepts/ledgers/open-closed-validated-ledgers.md). When inspecting transactions, it's important to confirm that [the results are final](../../../concepts/transactions/finality-of-results/index.md) before further processing the transaction. If `validated` is `true` then you know for sure the results won't change. For more information about best practices for transaction processing, see [Reliable Transaction Submission](../../../concepts/transactions/reliable-transaction-submission.md).
|
||||
|
||||
For a detailed description of every response field, see [account_info](../../../references/http-websocket-apis/public-api-methods/account-methods/account_info.md#response-format).
|
||||
|
||||
|
||||
## Keep on building
|
||||
|
||||
Now that you know how to use `xrpl-py` to connect to the XRP Ledger, get an account, and look up information about it, you can also use `xrpl-py` to:
|
||||
|
||||
* [Send XRP](../../how-tos/send-xrp.md).
|
||||
* [Set up secure signing](../../../concepts/transactions/secure-signing.md) for your account.
|
||||
|
||||
{% raw-partial file="/docs/_snippets/common-links.md" /%}
|
||||
@@ -1,221 +0,0 @@
|
||||
---
|
||||
seo:
|
||||
description: Verify that an account holds a valid credential on the XRP Ledger.
|
||||
labels:
|
||||
- Credentials
|
||||
---
|
||||
# Verify Credentials in Python
|
||||
|
||||
This tutorial describes how to verify that an account holds a valid [credential](/docs/concepts/decentralized-storage/credentials) on the XRP Ledger, which has different use cases depending on the type of credential and the meaning behind it. A few possible reasons to verify a credential include:
|
||||
|
||||
- Confirming that a recipient has passed a background check before sending a payment.
|
||||
- Checking a person's professional certifications, after verifying their identity with a [DID](/docs/concepts/decentralized-storage/decentralized-identifiers).
|
||||
- Displaying a player's achievements in a blockchain-connected game.
|
||||
|
||||
This tutorial uses sample code in Python using the [xrpl-py library](../index.md).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- You must have Python installed and know how to run Python code from the command line. Python 3.8 or later is required for xrpl-py.
|
||||
- You should have a basic understanding of the XRP Ledger.
|
||||
- The credential you want to verify should exist in the ledger already, and you should know the addresses of both the issuer and the holder, as well as the official credential type you want to check.
|
||||
- For sample code showing how to create credentials, see [Build a Credential Issuing Service](../build-apps/credential-issuing-service.md).
|
||||
|
||||
## Setup
|
||||
|
||||
First, download the complete sample code for this tutorial from GitHub:
|
||||
|
||||
- {% repo-link path="_code-samples/verify-credential/py/" %}Verify Credential sample code{% /repo-link %}
|
||||
|
||||
Then, in the appropriate directory, set up a virtual environment and install dependencies:
|
||||
|
||||
```sh
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
This installs the appropriate version of the `xrpl-py` library. There are no other dependencies for this tutorial outside of the Python standard library.
|
||||
|
||||
## Overview
|
||||
|
||||
The Verify Credential sample code consists of one file, `verify_credential.py`, and contains two main parts:
|
||||
|
||||
1. A function, `verify_credential(...)` which can be called with appropriate arguments to verify that a credential exists and is valid. This function can be imported into other code to be used as part of a larger application.
|
||||
2. A commandline utility that can be used to verify any credential.
|
||||
|
||||
## Usage
|
||||
|
||||
To test that you have the code installed and working properly, you can run the commandline utility with no arguments to check the existence of a default credential on Devnet, such as:
|
||||
|
||||
```sh
|
||||
python verify_credential.py
|
||||
```
|
||||
|
||||
If all goes well, you should see output such as the following:
|
||||
|
||||
```text
|
||||
Encoded credential_type as hex: 6D795F63726564656E7469616C
|
||||
Looking up credential...
|
||||
{'ledger_index': 'validated', 'method': 'ledger_entry', 'api_version': 2, 'credential': {'subject': 'rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA', 'issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'credential_type': '6D795F63726564656E7469616C'}, 'binary': False}
|
||||
Found credential:
|
||||
{'CredentialType': '6D795F63726564656E7469616C', 'Flags': 65536, 'Issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'IssuerNode': '0', 'LedgerEntryType': 'Credential', 'PreviousTxnID': '7D1257779E2D298C07C7E0C73CD446534B143FBD1F13DB268A878E40FD153B9A', 'PreviousTxnLgrSeq': 803275, 'Subject': 'rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA', 'SubjectNode': '0', 'index': '9603F0E204A8B1C61823625682EB0ECE98A4ECF22FF46CD4845FA9BFA3606B24'}
|
||||
Credential is valid.
|
||||
```
|
||||
|
||||
If the code reports that the credential was not found when called with no arguments, it's possible that the example credential has been deleted or the Devnet has been reset. If you have another credential you can verify, you can provide the details as commandline arguments. For example:
|
||||
|
||||
```sh
|
||||
python verify_credential.py rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA my_credential
|
||||
```
|
||||
|
||||
A full usage statement is available with the `-h` flag.
|
||||
|
||||
### Interactive Shell
|
||||
|
||||
You can open an interactive python shell and import the `verify_credential` function, as in the following example:
|
||||
|
||||
```py
|
||||
>>> from verify_credential import verify_credential
|
||||
>>> from xrpl.clients import JsonRpcClient
|
||||
>>> client = JsonRpcClient("https://s.devnet.rippletest.net:51234/")
|
||||
>>> verify_credential(client, issuer="rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS", subject="rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA", credential_type="my_credential")
|
||||
True
|
||||
```
|
||||
|
||||
You can import the `verify_credential(...)` function into other scripts and use it the same way.
|
||||
|
||||
### Other Examples
|
||||
|
||||
The following examples show other possible scenarios. The data for these examples may or may not still be present in Devnet. For example, anyone can delete an expired credential.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Valid with Expiration" %}
|
||||
```text
|
||||
$ ./verify_credential.py rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S long_lasting_credential
|
||||
|
||||
Encoded credential_type as hex: 6C6F6E675F6C617374696E675F63726564656E7469616C
|
||||
Looking up credential...
|
||||
{'ledger_index': 'validated', 'method': 'ledger_entry', 'api_version': 2, 'credential': {'subject': 'rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S', 'issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'credential_type': '6C6F6E675F6C617374696E675F63726564656E7469616C'}, 'binary': False}
|
||||
Found credential:
|
||||
{'CredentialType': '6C6F6E675F6C617374696E675F63726564656E7469616C', 'Expiration': 1167724800, 'Flags': 65536, 'Issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'IssuerNode': '0', 'LedgerEntryType': 'Credential', 'PreviousTxnID': 'C65794B7C322F028DB0D2DD72C9FF69D53A676B1608B77ADEF22311AFB22BFF7', 'PreviousTxnLgrSeq': 996934, 'Subject': 'rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S', 'SubjectNode': '0', 'index': 'FC4BB495DAE7C9F4615174188B3C5F2E337680017BA90E1F126DE08CAD15FD66'}
|
||||
Credential has expiration: 2037-01-01T08:00:00+00:00
|
||||
Looking up validated ledger to check for expiration.
|
||||
Most recent validated ledger is: 2025-03-11T20:01:51+00:00
|
||||
Credential is valid.
|
||||
```
|
||||
{% /tab %}
|
||||
|
||||
{% tab label="Expired" %}
|
||||
```text
|
||||
$ ./verify_credential.py rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S expiring_credential
|
||||
|
||||
Encoded credential_type as hex: 6578706972696E675F63726564656E7469616C
|
||||
Looking up credential...
|
||||
{'ledger_index': 'validated', 'method': 'ledger_entry', 'api_version': 2, 'credential': {'subject': 'rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S', 'issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'credential_type': '6578706972696E675F63726564656E7469616C'}, 'binary': False}
|
||||
Found credential:
|
||||
{'CredentialType': '6578706972696E675F63726564656E7469616C', 'Expiration': 795038400, 'Flags': 65536, 'Issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'IssuerNode': '0', 'LedgerEntryType': 'Credential', 'PreviousTxnID': 'E497F1EFE2E198EDED0D94ADDEE4CEFACDDC3B1674133A0123C765F8061B9600', 'PreviousTxnLgrSeq': 997087, 'Subject': 'rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S', 'SubjectNode': '0', 'index': 'F3A9475871E7BA994E257732D0C7CB0B91CACBBB9F840BDFA6ABABD6F71454CD'}
|
||||
Credential has expiration: 2025-03-11T20:00:00+00:00
|
||||
Looking up validated ledger to check for expiration.
|
||||
Most recent validated ledger is: 2025-03-11T20:02:03+00:00
|
||||
Credential is expired.
|
||||
```
|
||||
{% /tab %}
|
||||
|
||||
{% tab label="Unaccepted" %}
|
||||
```text
|
||||
$ ./verify_credential.py rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S unaccepted_credential
|
||||
|
||||
Encoded credential_type as hex: 756E61636365707465645F63726564656E7469616C
|
||||
Looking up credential...
|
||||
{'ledger_index': 'validated', 'method': 'ledger_entry', 'api_version': 2, 'credential': {'subject': 'rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S', 'issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'credential_type': '756E61636365707465645F63726564656E7469616C'}, 'binary': False}
|
||||
Found credential:
|
||||
{'CredentialType': '756E61636365707465645F63726564656E7469616C', 'Flags': 0, 'Issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'IssuerNode': '0', 'LedgerEntryType': 'Credential', 'PreviousTxnID': '59DB4B17E5552AB1CA1E2A89F5C03E51C2ACD0D293955FA701AE4A1801E94C96', 'PreviousTxnLgrSeq': 997107, 'Subject': 'rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S', 'SubjectNode': '0', 'index': '8E5AD9444D566BE5C6F87C94D696139CEEE43ACB9A96137A59C003B48DF565C6'}
|
||||
Credential is not accepted.
|
||||
```
|
||||
{% /tab %}
|
||||
{% tab label="Hexadecimal Credential Type" %}
|
||||
```text
|
||||
$ ./verify_credential.py rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA 6D795F63726564656E7469616C -b
|
||||
|
||||
Looking up credential...
|
||||
{'ledger_index': 'validated', 'method': 'ledger_entry', 'api_version': 2, 'credential': {'subject': 'rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA', 'issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'credential_type': '6D795F63726564656E7469616C'}, 'binary': False}
|
||||
Found credential:
|
||||
{'CredentialType': '6D795F63726564656E7469616C', 'Flags': 65536, 'Issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'IssuerNode': '0', 'LedgerEntryType': 'Credential', 'PreviousTxnID': '7D1257779E2D298C07C7E0C73CD446534B143FBD1F13DB268A878E40FD153B9A', 'PreviousTxnLgrSeq': 803275, 'Subject': 'rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA', 'SubjectNode': '0', 'index': '9603F0E204A8B1C61823625682EB0ECE98A4ECF22FF46CD4845FA9BFA3606B24'}
|
||||
Credential is valid.
|
||||
```
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
|
||||
## Code Walkthrough
|
||||
|
||||
### 1. Initial setup
|
||||
|
||||
The `verify_credential.py` file implements the code for this tutorial.
|
||||
This file can be run as a commandline tool, so it starts with a [shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)). Then it imports dependencies, with standard lib first and then specific parts of the `xrpl-py` library:
|
||||
|
||||
{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" before="# Set up logging" /%}
|
||||
|
||||
The next section of the code sets the default log level for messages that might be written to the console through the utility:
|
||||
|
||||
{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Set up logging" before="# Define an error to throw" /%}
|
||||
|
||||
Then it defines a type of exception to throw if something goes wrong when connecting to the XRP Ledger:
|
||||
|
||||
{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Define an error to throw" before="# Main function" /%}
|
||||
|
||||
### 2. Main function
|
||||
|
||||
The `verify_credential(...)` function performs the main work for this tutorial. The function definition and docstring define the parameters:
|
||||
|
||||
{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Main function" before="# Handle function inputs" /%}
|
||||
|
||||
The first thing the function does is verify that the user provided a credential type in either the `credential_type` or `credential_type_hex` parameter. The XRP Ledger APIs require the credential type to be hexadecimal, so it converts the user input if necessary:
|
||||
|
||||
{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Handle function inputs" before="# Perform XRPL lookup" /%}
|
||||
|
||||
Next, it calls the [ledger_entry method](/docs/references/http-websocket-apis/public-api-methods/ledger-methods/ledger_entry#get-credential-entry) to look up the requested Credential ledger entry:
|
||||
|
||||
{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Perform XRPL lookup" before="# Confirm that the credential has been accepted" /%}
|
||||
|
||||
If it succeeds in finding the credential, the function continues by checking that the credential has been accepted by its holder. Since anyone can issue a credential to anyone else, a credential is only considered valid if its subject has accepted it.
|
||||
|
||||
{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Confirm that the credential has been accepted" before="# Confirm that the credential is not expired" /%}
|
||||
|
||||
Then, if the credential has an expiration time, the function checks that the credential is not expired. If the credential has no expiration, this step can be skipped. A credential is officially considered expired if its expiration time is before the [official close time](/docs/concepts/ledgers/ledger-close-times) of the most recently validated ledger. This is more universal than comparing the expiration to your own local clock. Thus, the code uses the [ledger method][] to look up the most recently validated ledger:
|
||||
|
||||
{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Confirm that the credential is not expired" before="# Credential has passed all checks" /%}
|
||||
|
||||
If none of the checks up to this point have returned a `False` value, then the credential must be valid. This concludes the `verify_credential(...)` main function:
|
||||
|
||||
{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Credential has passed all checks" before="# Commandline usage" /%}
|
||||
|
||||
### 3. Commandline interface
|
||||
|
||||
This file also implements a commandline utility which runs when the file is executed directly as a Python script. Some variables, such as the set of available networks, are only needed for this mode:
|
||||
|
||||
{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Commandline usage" before="# Parse arguments" /%}
|
||||
|
||||
Then it uses the [argparse library](https://docs.python.org/3/library/argparse.html) to define and parse the arguments that the user can pass from the commandline:
|
||||
|
||||
{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Parse arguments" before="# Call verify_credential" /%}
|
||||
|
||||
After parsing the commandline args, it sets the appropriate values and passes them to `verify_credential(...)` to perform the credential verification:
|
||||
|
||||
{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Call verify_credential" before="# Return a nonzero exit code" /%}
|
||||
|
||||
Finally, it returns a nonzero exit code if this credential was not verified. This can be useful for shell scripts:
|
||||
|
||||
{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Call verify_credential" before="# Return a nonzero exit code" /%}
|
||||
|
||||
Otherwise, the code exits as normal, which returns a successful exit code of `0` to the shell.
|
||||
|
||||
## Next Steps
|
||||
|
||||
Now that you know how to use `xrpl-py` to verify credentials, you can try building this or related steps together into a bigger project. For example:
|
||||
|
||||
- Incorporate credential verification into a [wallet application](../build-apps/build-a-desktop-wallet-in-python.md).
|
||||
- Issue your own credentials with a [credential issuing service](../build-apps/credential-issuing-service.md).
|
||||
|
||||
{% raw-partial file="/docs/_snippets/common-links.md" /%}
|
||||
Reference in New Issue
Block a user