--- parent: build-apps.html targets: - en - ja # TODO: translate this page blurb: Build a graphical desktop wallet for the XRPL using JavaScript. --- # Build a Desktop Wallet in JavaScript This tutorial demonstrates how to build a desktop wallet for the XRP Ledger using the JavaScript programming language, the Electron Framework and various libraries. This application can be used as a starting point for building a more complex 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 requirements: - You have [Node.js](https://nodejs.org/) 14+ installed. - You are somewhat familiar with modern JavaScript programming and have completed the [Get Started Using JavaScript tutorial](get-started-using-javascript.html). - You have at least some rough understanding of what the XRP Ledger, it's capabilities and of cryptocurrency in general. Ideally you have completed the [Basic XRPL guide](https://learn.xrpl.org/). ### Source Code You can find the complete source code for all of this tutorial's examples in the [code samples section of this website's repository]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/). ## Rationale This tutorial will take you through the process of creating a XRP Wallet application from scratch. Starting with a simple, "Hello World" like example with minimal functionality, we will step-by-step add more complex features. We will use the well-established [Electron Framework](https://www.electronjs.org/) to let us use JavaScript to write this desktop application. ## Goals At the end of this tutorial, you will have built a JavaScript Wallet application that looks something like this: ![Desktop wallet screenshot](img/javascript-wallet-preview.png) The look and feel of the user interface should be roughly the same regardless of operating system, as the Electron Framework allows us to write cross-platform applications that are styled with HTML and CSS just like a web-based application. The application we are going to build here will be capable of the following: - Showing updates to the XRP Ledger in real-time. - Viewing any XRP Ledger account's activity "read-only" including showing how much XRP was delivered by each transaction. - Showing how much XRP is set aside for the account's [reserve requirement](reserves.html). - Sending [direct XRP payments](direct-xrp-payments.html), and providing feedback about the intended destination address, including: - Whether the intended destination already exists in the XRP Ledger, or the payment would have to fund its creation. - If the address doesn't want to receive XRP ([`DisallowXRP` flag](become-an-xrp-ledger-gateway.html#disallow-xrp) enabled). - If the address has a [verified domain name](https://xrpl.org/xrp-ledger-toml.html#account-verification) associated with it. The application in this tutorial _doesn't_ have the ability to send or trade [tokens](issued-currencies.html) or use other [payment types](payment-types.html) like [Escrow](https://xrpl.org/escrow.html) or [Payment Channels](https://xrpl.org/payment-channels.html). However, it provides a foundation that you can implement those and other features on top of. In addition to the above features, you'll also learn a bit about Events, IPC (inter-process-communication) and asynchronous (async) code in JavaScript. ## Steps ### 0. Project setup - Hello World 1. To initialize the project, create a package.json file with the following content: ```json { "name": "xrpl-javascript-desktop-wallet", "version": "1.0.0", "license": "MIT", "scripts": { "start": "electron ./index.js" }, "dependencies": { "async": "^3.2.4", "fernet": "^0.4.0", "node-fetch": "^2.6.9", "pbkdf2-hmac": "^1.1.0", "open": "^8.4.0", "toml": "^3.0.0", "xrpl": "^2.6.0" }, "devDependencies": { "electron": "22.3.2" } } ``` Here we define the libraries our application will use in the `dependencies` section as well as shortcuts for running our application in the `scripts` section. 2. After you create your package.json file, install those dependencies by running the following command: ```console npm install ``` This installs the Electron Framework, the xrpl.js client library and a couple of helpers we are going to need for our application to work. 3. In the root folder, create an `index.js` file with the following content: ```javascript const { app, BrowserWindow } = require('electron') const path = require('path') /** * This is our main function, it creates our application window, preloads the code we will need to communicate * between the renderer Process and the main Process, loads a layout and performs the main logic */ const createWindow = () => { // Creates the application window const appWindow = new BrowserWindow({ width: 1024, height: 768 }) // Loads a layout appWindow.loadFile(path.join(__dirname, 'view', 'template.html')) return appWindow } // Here we have to wait for the application to signal that it is ready // to execute our code. In this case we just create a main window. app.whenReady().then(() => { createWindow() }) ``` 4. Make a new folder named `view` in the root directory of the project. 5. Now, inside the `view` folder, add a `template.html` file with the following content: ```html XRPL Wallet Tutorial (JavaScript / Electron)

Build a XRPL Wallet

Hello world! ``` 6. Now, start the application with the following command: ```console npm run start ``` **Tip:** When you need to debug the application, you can open Developer Tools like in Chrome or Firefox by selecting "View / Toggle Developer Tools" from The application Menu or using a Shortcut ("Ctrl+Shift+I" on Windows or Linux, "Option+Command+I on Mac). You should see a window appear that displays the text "Build a XRPL Wallet" and "Hello world!" To run the reference application found in `content/_code-samples/build-a-wallet/desktop-js` for this step, run: ```console npm run hello ``` In the next steps we will continually expand on this very basic setup. To better keep track of all the changes that will be made, the files in the [reference section]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/) are numbered/prefixed with the respective step number: **Full code for this step:** [`0-hello/0_hello.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/0_hello.js), [`0-hello/view/0_hello.html`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/0_hello.html), ### 1. Ledger Index **Full code for this step:** [`1-ledger-index/index.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/1_ledger-index.js), [`1-ledger-index/view/preload.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/1_preload.js), [`1-ledger-index/view/template.html`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/1_ledger-index.html), [`1-ledger-index/view/renderer.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/1_renderer.js). Our first step was to have a running "Hello World" application. Now we want to expand on that so that the application can interact on a very basic level with the XRP Ledger and display some information about the current ledger state on the screen. After completing this step, the - for the time being unstyled - application should look like this: ![Screenshot: Step 1, hello world equivalent](img/javascript-wallet-1.png) 1. Update `index.js` by adding the following snippet in the import section at the top of the file below the `path` import: ```javascript const xrpl = require("xrpl") const TESTNET_URL = "wss://s.altnet.rippletest.net:51233" /** * This function creates a WebService client, which connects to the XRPL and fetches the latest ledger index. * * @returns {Promise} */ const getValidatedLedgerIndex = async () => { const client = new xrpl.Client(TESTNET_URL) await client.connect() // Reference: https://xrpl.org/ledger.html#ledger const ledgerRequest = { "command": "ledger", "ledger_index": "validated" } const ledgerResponse = await client.request(ledgerRequest) await client.disconnect() return ledgerResponse.result.ledger_index } ``` This helper function does the following: It establishes a WebSocket connection to the XRP Ledger, calls the XRP Ledger API's [ledger method](ledger.html) and returns the ledger index from the response. 2. In order to attach a preloader script, modify the `createWindow` method in `index.js` by adding the following code: ```javascript // Creates the application window const appWindow = new BrowserWindow({ width: 1024, height: 768, // Step 1 code additions - start webPreferences: { preload: path.join(__dirname, 'view', 'preload.js'), }, // Step 1 code additions - end }) ``` 3. Now in the `view` folder, create a file `preload.js`with the following content: ```javascript const { contextBridge, ipcRenderer } = require('electron'); // Expose functionality from main process (aka. "backend") to be used by the renderer process(aka. "backend") contextBridge.exposeInMainWorld('electronAPI', { // By calling "onUpdateLedgerIndex" in the frontend process we can now attach a callback function to // by making onUpdateLedgerIndex available at the window level. // The subscribed function gets triggered whenever the backend process triggers the event 'update-ledger-index' onUpdateLedgerIndex: (callback) => { ipcRenderer.on('update-ledger-index', callback) } }) ``` This preloader script is used to expose functions to the browsers window object which can be used to subscribe frontend logic to events broadcast from the main logic in `index.js`. In the browser, `window.electronAPI.onUpdateLedgerIndex(callback)` can now be used to pass a callback function via `ipcRenderer.on('eventName', callback)` that will be triggered by `appWindow.webContents.send('eventName', value)`. 4. Now, in `view/template.html`, replace the body in order to show a placeholder for the ledger index instead of "Hello world!" ```html

Build a XRPL Wallet

Latest validated ledger index: ``` 5. In `view/template.html` add the following line at the bottom of the file: ```html ``` 6. Now create the `renderer.js` file in the `view`folder with the following code: ```javascript const ledgerIndexEl = document.getElementById('ledger-index') // Here we define the callback function that performs the content update // whenever 'update-ledger-index' is called by the main process window.electronAPI.onUpdateLedgerIndex((_event, value) => { ledgerIndexEl.innerText = value }) ``` 7. To wire up our main application to send the ledger index to the frontend, modify `index.js` by adding the following snippet replacing the last section in the file: ```javascript // Here we have to wait for the application to signal that it is ready // to execute our code. In this case we create a main window, query // the ledger for its latest index and submit the result to the main // window where it will be displayed app.whenReady().then(() => { // Step 1 code additions - start const appWindow = createWindow() getValidatedLedgerIndex().then((value) => { appWindow.webContents.send('update-ledger-index', value) }) // Step 1 code additions - end }) ``` Here we first call our helper function `getValidatedLedgerIndex()` and then broadcast an event named `update-ledger-index`. This attaches a payload containing the latest ledger information which can be handled by the frontend. This example shows how to do Inter Process Communication (IPC) in Electron. Technically, JavaScript has no true parallel processes or threading because it follows a single-threaded event-driven paradigm. Nonetheless Electron provides us with two IPC modules called `ipcMain` and `ipcRenderer`. We can roughly equate `ipcMain` to a backend process and `ipcRenderer` to a frontend process when we think in terms of client-server applications. It works as follows: 1. We started by creating a function that enables the frontend to subscribe to backend events via the `ContextBridge` (`onUpdateLedgerIndex` in `view/preload.js`) 2. Then we make the function available by putting it in a preloader script to ensure it is loaded and can be used by the frontend. 3. On the frontend, we can then use that function to attach a callback that handles frontend updates when the event is dispatched. We could do this in the console, in a ` ```` Here we basically added the [Boostrap Framework]() and a little custom styling to our application. We'll leave it at that for this Step - to get the application running at this stage of development, run the following command: ```console npm run start ``` This time the application should display a dashboard like layout with the XRP logo and a navigation ono the left side, and a content area comprising most of the screen. You should be able to switch from the dashboard to the transactions list and back. To run the reference application found in `content/_code-samples/build-a-wallet/desktop-js` for this step, run: ```console npm run styling ``` ### 7. Send XRP **Full code for this step:** [`library/3_helper.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/library/3_helper.js), [`library/4_helper.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/library/4_helper.js), [`library/5_helper.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/library/5_helper.js), [`library/6_helper.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/library/6_helper.js), [`library/7_helper.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/library/7_helper.js), [`7-send-xrp/index.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/7_send-xrp.js), [`7-send-xrp/view/preload.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/7_preload.js), [`7-send-xrp/view/renderer.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/7_renderer.js), [`7-send-xrp/view/template.html`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/7_send-xrp.html). Up until now we have enabled our app to query and display data from the XRPL. Now it's time to actively participate in the ledger by enabling our application to send transactions. For now, we can stick to sending direct XRP payments because there are more complexities involved in sending issued tokens. After finishing this step the application should look like this: ![Screenshot: Step 7, send xrp dialog](img/javascript-wallet-7.png) 1. Create the file `library/7_helpers.js` and add the following contents: {{ include_code("_code-samples/build-a-wallet/desktop-js/library/7_helpers.js", language="js") }} (There was no `6-helpers.js`, so don't worry!) 2. Add the new function to the import section in `index.js`: ```javascript const { initialize, subscribe, saveSaltedSeed, loadSaltedSeed } = require('./library/5_helpers') const { sendXrp } = require('./library/7_helpers') ``` 3. Still in `index.js`, add an event listener handling the `send-xrp-event` from the frontend dialog: ```javascript await initialize(client, wallet, appWindow) // Step 7 code additions - start ipcMain.on('send-xrp-action', (event, paymentData) => { sendXrp(paymentData, client, wallet).then((result) => { appWindow.webContents.send('send-xrp-transaction-finish', result) }) }) // Step 7 code additions - start ``` 4. Modify `view/preload.js` by adding two new functions: ```javascript onClickSendXrp: (paymentData) => { ipcRenderer.send('send-xrp-action', paymentData) }, onSendXrpTransactionFinish: (callback) => { ipcRenderer.on('send-xrp-transaction-finish', callback) } ``` 5. In `view/template.html`, add a button to toggle the modal dialog housing the "Send XRP" logic: ```html

Build a XRPL Wallet - Part 7/8

``` 6. In the same file, at the end of the `
` section, add said modal dialog: ```html ``` 7. Add the following code to the bottom of `view/renderer.js`: ```javascript const modalButton = document.getElementById('send-xrp-modal-button') const modalDialog = new bootstrap.Modal(document.getElementById('send-xrp-modal')) modalButton.addEventListener('click', () => { modalDialog.show() }) const destinationAddressEl = document.getElementById('input-destination-address') const destinationTagEl = document.getElementById('input-destination-tag') const amountEl = document.getElementById('input-xrp-amount') const sendXrpButtonEl = document.getElementById('send-xrp-submit-button') sendXrpButtonEl.addEventListener('click', () => { modalDialog.hide() const destinationAddress = destinationAddressEl.value const destinationTag = destinationTagEl.value const amount = amountEl.value window.electronAPI.onClickSendXrp({destinationAddress, destinationTag, amount}) }) window.electronAPI.onSendXrpTransactionFinish((_event, result) => { alert('Result: ' + result.result.meta.TransactionResult) destinationAddressEl.value = '' destinationTagEl.value = '' amountEl.value = '' }) ``` Now, Run the following command: ```console npm run start ``` The application should now display a "Send XRP" button in the top right corner, which should open a Modal dialog on clicking. You can use this Dialog to send XRP tokens from this account to another, and the balance update as well as the transaction should be reflected in the app. If you need an account address to send the XRP to, [you can create an account on the testnet](https://learn.xrpl.org/course/code-with-the-xrpl/lesson/create-accounts-and-send-xrp/). To run the reference application found in `content/_code-samples/build-a-wallet/desktop-js` for this step, run: ```console npm run send-xrp ``` ### 8. Domain Verification and Polish **Full code for this step:** [`library/3_helper.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/library/3_helper.js), [`library/4_helper.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/library/4_helper.js), [`library/5_helper.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/library/5_helper.js), [`library/6_helper.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/library/6_helper.js), [`library/7_helper.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/library/7_helper.js), [`library/8_helper.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/library/8_helper.js), [`8-domain-verification/index.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/8_domain-verification.js), [`8-domain-verification/view/8_prelaod.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/8_preload.js), [`8-domain-verification/view/8_domain-verification.html`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/8_domain-verification.html), [`8-domain-verification/view/8_renderer.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/8_renderer.js). 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 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 one 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 XRP. One type of check we could make is to verify the domain name associated with an XRP Ledger address; this is called [account domain verification](xrp-ledger-toml.html#account-verification). When an account's domain is verified, we can could show it like this: ![Screenshot: Step 8, use domain verification](img/javascript-wallet-8.png) 1. In the `library` folder, add a new file `4_helpers.js`. Then add the following contents to that file: {{ include_code("_code-samples/build-a-wallet/desktop-js/library/8_helpers.js", language="js") }} The code in `8_helpers.js` looks up the account on the ledger by sending an [`account_info`](account_info.html) request. If the account does exist, the code checks for the [`lsfDisallowXRP` flag](accountroot.html#accountroot-flags). 2. Import the new helper function in`index.js`: ```javascript const { initialize, subscribe, saveSaltedSeed, loadSaltedSeed } = require('./library/5_helpers') const { sendXrp } = require('./library/7_helpers') // Step 8 code additions - start const { verify } = require('./library/8_helpers') // Step 8 code additions - end ``` 3. After the callback function `ipcMain.on('send-xrp-action', callback)` add the following event handler: ```javascript ipcMain.on('send-xrp-action', (event, paymentData) => { sendXrp(paymentData, client, wallet).then((result) => { appWindow.webContents.send('send-xrp-transaction-finish', result) }) }) // Step 8 code additions - start ipcMain.on('destination-account-change', (event, destinationAccount) => { verify(destinationAccount, client).then((result) => { appWindow.webContents.send('update-domain-verification-data', result) }) }) // Step 8 code additions - end ``` 3. Modify `view/preload.js` and add the following two functions to `'electronAPI'`: ```javascript onDestinationAccountChange: (callback) => { ipcRenderer.send('destination-account-change', callback) }, onUpdateDomainVerificationData: (callback) => { ipcRenderer.on('update-domain-verification-data', callback) } ``` Finally, the code decodes the account's `Domain` field, if present, and performs domain verification using the method imported above. 4. Update the view logic - in `view/template.html` add the following lines just before the `` element with `id="input-destination-address`: ```html
Verification status:
To (Address)
``` 5. Lastly, modify the renderer as described below: ```javascript modalButton.addEventListener('click', () => { modalDialog.show() }) // Step 8 code additions - start const accountVerificationEl = document.querySelector('.accountVerificationIndicator span') // Step 8 code additions - end const destinationAddressEl = document.getElementById('input-destination-address') const destinationTagEl = document.getElementById('input-destination-tag') const amountEl = document.getElementById('input-xrp-amount') const sendXrpButtonEl = document.getElementById('send-xrp-submit-button') // Step 8 code additions - start destinationAddressEl.addEventListener('input', (event) => { window.electronAPI.onDestinationAccountChange(destinationAddressEl.value) }) window.electronAPI.onUpdateDomainVerificationData((_event, result) => { accountVerificationEl.textContent = `Domain: ${result.domain || 'n/a'} Verified: ${result.verified}` }) // Step 8 code additions - end sendXrpButtonEl.addEventListener('click', () => { modalDialog.hide() const destinationAddress = destinationAddressEl.value const destinationTag = destinationTagEl.value const amount = amountEl.value window.electronAPI.onClickSendXrp({destinationAddress, destinationTag, amount}) }) ``` Run the following command: ```console npm run start ``` Test your wallet app the same way you did in the previous steps. It should display a hint about the receiving account when opening up the "Send XRP" dialog and entering the address. 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 run the reference application, run: ```console npm run domain-verification ``` ## Summary Congratulations, you now have created your own wallet application! In completing this tutorial, you have learned how to interact with the XRP Ledger - from sending simple requests up to doing actual transactions, all in an electron application.