Re-added code to clean branch

This commit is contained in:
AlexanderBuzz
2023-06-20 13:57:07 +02:00
parent 5b841573f8
commit b96d3a31e5
54 changed files with 3261 additions and 11 deletions

View File

@@ -0,0 +1,132 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
Wallet/

View File

@@ -0,0 +1,63 @@
const { app, BrowserWindow } = require('electron')
const path = require('path')
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<number>}
*/
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 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,
webPreferences: {
preload: path.join(__dirname, 'view', '1_preload.js'),
},
})
// Loads a layout
appWindow.loadFile(path.join(__dirname, 'view', '1_hello.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 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(() => {
const appWindow = createWindow()
getValidatedLedgerIndex().then((value) => {
appWindow.webContents.send('update-ledger-index', value)
})
})

View File

@@ -0,0 +1,75 @@
const async = require('async')
const { app, BrowserWindow } = require('electron')
const path = require('path')
const xrpl = require("xrpl")
const TESTNET_URL = "wss://s.altnet.rippletest.net:51233"
/**
* This function sends a ledger-request and returns the response
*
* @param client
* @returns {Promise<*>}
*/
const getValidatedLedgerData = async (client) => {
// Reference: https://xrpl.org/ledger.html#ledger
const ledgerRequest = {
"command": "ledger",
"ledger_index": "validated"
}
const ledgerResponse = await client.request(ledgerRequest)
return ledgerResponse.result
}
/**
* This function creates our application window
*
* @returns {Electron.CrossProcessExports.BrowserWindow}
*/
const createWindow = () => {
const appWindow = new BrowserWindow({
width: 1024,
height: 768,
webPreferences: {
preload: path.join(__dirname, 'view', '2_preload.js'),
},
})
appWindow.loadFile(path.join(__dirname, 'view', '2_async.html'))
return appWindow
}
/**
* This function creates a XRPL client, continuously polls the XRPL with a ledger-request and broadcasts
* the result by dispatching the 'update-ledger-data' event which will be picked up by the frontend
*
* @returns {Promise<void>}
*/
const main = async () => {
const appWindow = createWindow()
const client = new xrpl.Client(TESTNET_URL)
await client.connect()
async.forever(
function(next) {
getValidatedLedgerData(client).then((value) => {
appWindow.webContents.send('update-ledger-data', value)
})
setTimeout(function() {
next()
}, 2500)
},
function(err) {
// if next is called with a value in its first parameter, it will appear
// in here as 'err', and execution will stop.
}
)
}
app.whenReady().then(main)

View File

@@ -0,0 +1,53 @@
const { app, BrowserWindow } = require('electron')
const path = require('path')
const xrpl = require("xrpl")
const TESTNET_URL = "wss://s.altnet.rippletest.net:51233"
/**
* This function creates our application window
*
* @returns {Electron.CrossProcessExports.BrowserWindow}
*/
const createWindow = () => {
const appWindow = new BrowserWindow({
width: 1024,
height: 768,
webPreferences: {
preload: path.join(__dirname, 'view', '2_preload.js'),
},
})
appWindow.loadFile(path.join(__dirname, 'view', '2_async.html'))
return appWindow
}
/**
* This function creates a XRPL client, subscribes to 'ledger' events from the XRPL and broadcasts those by
* dispatching the 'update-ledger-data' event which will be picked up by the frontend
*
* @returns {Promise<void>}
*/
const main = async () => {
const appWindow = createWindow()
const client = new xrpl.Client(TESTNET_URL)
await client.connect()
// Subscribe client to 'ledger' events
// Reference: https://xrpl.org/subscribe.html
await client.request({
"command": "subscribe",
"streams": ["ledger"]
})
// Dispatch 'update-ledger-data' event
client.on("ledgerClosed", async (ledger) => {
appWindow.webContents.send('update-ledger-data', ledger)
})
}
app.whenReady().then(main)

View File

@@ -0,0 +1,75 @@
const { app, BrowserWindow, ipcMain} = require('electron')
const path = require('path')
const xrpl = require("xrpl")
const { prepareReserve, prepareAccountData, prepareLedgerData} = require('./library/3_helpers')
const TESTNET_URL = "wss://s.altnet.rippletest.net:51233"
let reserveBaseXrp = null, reserveIncrementXrp = null
const createWindow = () => {
const appWindow = new BrowserWindow({
width: 1024,
height: 768,
webPreferences: {
preload: path.join(__dirname, 'view', '3_preload.js'),
},
})
appWindow.loadFile(path.join(__dirname, 'view', '3_account.html'))
return appWindow
}
const main = async () => {
const appWindow = createWindow()
ipcMain.on('address-entered', async (event, address) => {
let reserve = null
const client = new xrpl.Client(TESTNET_URL)
await client.connect()
// Reference: https://xrpl.org/subscribe.html
await client.request({
"command": "subscribe",
"streams": ["ledger"],
"accounts": [address]
})
// Reference: https://xrpl.org/subscribe.html#ledger-stream
client.on("ledgerClosed", async (rawLedgerData) => {
reserve = prepareReserve(rawLedgerData)
const ledger = prepareLedgerData(rawLedgerData)
appWindow.webContents.send('update-ledger-data', ledger)
})
// Reference: https://xrpl.org/subscribe.html#transaction-streams
client.on("transaction", async (transaction) => {
// Reference: https://xrpl.org/account_info.html
const accountInfoRequest = {
"command": "account_info",
"account": address,
"ledger_index": transaction.ledger_index
}
const accountInfoResponse = await client.request(accountInfoRequest)
const accountData = prepareAccountData(accountInfoResponse.result.account_data, reserve)
appWindow.webContents.send('update-account-data', accountData)
})
// Initial Account Request -> Get account details on startup
// Reference: https://xrpl.org/account_info.html
const accountInfoResponse = await client.request({
"command": "account_info",
"account": address,
"ledger_index": "current"
})
const accountData = prepareAccountData(accountInfoResponse.result.account_data)
appWindow.webContents.send('update-account-data', accountData)
})
}
app.whenReady().then(main)

View File

@@ -0,0 +1,88 @@
const {app, BrowserWindow, ipcMain} = require('electron')
const path = require('path')
const xrpl = require("xrpl")
const { prepareReserve, prepareAccountData, prepareLedgerData} = require('./library/3_helpers')
const { prepareTxData } = require('./library/4_helpers')
const TESTNET_URL = "wss://s.altnet.rippletest.net:51233"
const createWindow = () => {
const appWindow = new BrowserWindow({
width: 1024,
height: 768,
webPreferences: {
preload: path.join(__dirname, 'view', '4_preload.js'),
},
})
appWindow.loadFile(path.join(__dirname, 'view', '4_tx-history.html'))
return appWindow
}
const main = async () => {
const appWindow = createWindow()
ipcMain.on('address-entered', async (event, address) => {
let reserve = null
const client = new xrpl.Client(TESTNET_URL)
await client.connect()
// Reference: https://xrpl.org/subscribe.html
await client.request({
"command": "subscribe",
"streams": ["ledger"],
"accounts": [address]
})
// Reference: https://xrpl.org/subscribe.html#ledger-stream
client.on("ledgerClosed", async (rawLedgerData) => {
reserve = prepareReserve(rawLedgerData)
const ledger = prepareLedgerData(rawLedgerData)
appWindow.webContents.send('update-ledger-data', ledger)
})
// Wait for transaction on subscribed account and re-request account data
client.on("transaction", async (transaction) => {
// Reference: https://xrpl.org/account_info.html
const accountInfoRequest = {
"command": "account_info",
"account": address,
"ledger_index": transaction.ledger_index
}
const accountInfoResponse = await client.request(accountInfoRequest)
const accountData = prepareAccountData(accountInfoResponse.result.account_data, reserve)
appWindow.webContents.send('update-account-data', accountData)
const transactions = prepareTxData([{tx: transaction.transaction}])
appWindow.webContents.send('update-transaction-data', transactions)
})
// Initial Account Request -> Get account details on startup
// Reference: https://xrpl.org/account_info.html
const accountInfoResponse = await client.request({
"command": "account_info",
"account": address,
"ledger_index": "current"
})
const accountData = prepareAccountData(accountInfoResponse.result.account_data)
appWindow.webContents.send('update-account-data', accountData)
// Initial Transaction Request -> List account transactions on startup
// Reference: https://xrpl.org/account_tx.html
const txResponse = await client.request({
"command": "account_tx",
"account": address
})
const transactions = prepareTxData(txResponse.result.transactions)
appWindow.webContents.send('update-transaction-data', transactions)
})
}
app.whenReady().then(main)

View File

@@ -0,0 +1,73 @@
const {app, BrowserWindow, ipcMain} = require('electron')
const fs = require('fs')
const path = require('path')
const xrpl = require("xrpl")
const { initialize, subscribe, saveSaltedSeed, loadSaltedSeed } = require('./library/5_helpers')
const TESTNET_URL = "wss://s.altnet.rippletest.net:51233"
const WALLET_DIR = 'Wallet'
const createWindow = () => {
const appWindow = new BrowserWindow({
width: 1024,
height: 768,
webPreferences: {
preload: path.join(__dirname, 'view', '5_preload.js'),
},
})
appWindow.loadFile(path.join(__dirname, 'view', '5_password.html'))
return appWindow
}
const main = async () => {
const appWindow = createWindow()
// Create Wallet directory in case it does not exist yet
if (!fs.existsSync(WALLET_DIR)) {
fs.mkdirSync(path.join(__dirname, WALLET_DIR));
}
let seed = null;
ipcMain.on('seed-entered', async (event, providedSeed) => {
seed = providedSeed
appWindow.webContents.send('open-password-dialog')
})
ipcMain.on('password-entered', async (event, password) => {
if (!fs.existsSync(path.join(__dirname, WALLET_DIR , 'seed.txt'))) {
saveSaltedSeed('../' + WALLET_DIR, seed, password)
} else {
seed = loadSaltedSeed('../' + WALLET_DIR, password)
}
const wallet = xrpl.Wallet.fromSeed(seed)
const client = new xrpl.Client(TESTNET_URL)
await client.connect()
await subscribe(client, wallet, appWindow)
await initialize(client, wallet, appWindow)
})
// We have to wait for the application frontend to be ready, otherwise
// we might run into a race condition and the open-dialog events
// get triggered before the callbacks are attached
appWindow.once('ready-to-show', () => {
// If there is no seed present yet, ask for it, otherwise query for the password
// for the seed that has been saved
if (!fs.existsSync(path.join(__dirname, WALLET_DIR, 'seed.txt'))) {
appWindow.webContents.send('open-seed-dialog')
} else {
appWindow.webContents.send('open-password-dialog')
}
})
}
app.whenReady().then(main)

View File

@@ -0,0 +1,74 @@
const {app, BrowserWindow, ipcMain} = require('electron')
const fs = require('fs')
const path = require('path')
const xrpl = require("xrpl")
const { initialize, subscribe, saveSaltedSeed, loadSaltedSeed } = require('./library/5_helpers')
const TESTNET_URL = "wss://s.altnet.rippletest.net:51233"
const WALLET_DIR = 'Wallet'
const createWindow = () => {
const appWindow = new BrowserWindow({
width: 1024,
height: 768,
webPreferences: {
preload: path.join(__dirname, 'view', '5_preload.js'),
},
})
appWindow.loadFile(path.join(__dirname, 'view', '6_styling.html'))
return appWindow
}
const main = async () => {
const appWindow = createWindow()
if (!fs.existsSync(WALLET_DIR)) {
// Create Wallet directory in case it does not exist yet
fs.mkdirSync(path.join(__dirname, WALLET_DIR));
}
let seed = null;
ipcMain.on('seed-entered', async (event, providedSeed) => {
seed = providedSeed
appWindow.webContents.send('open-password-dialog')
})
ipcMain.on('password-entered', async (event, password) => {
if (!fs.existsSync(path.join(__dirname, WALLET_DIR , 'seed.txt'))) {
saveSaltedSeed('../' + WALLET_DIR, seed, password)
} else {
seed = loadSaltedSeed('../' + WALLET_DIR, password)
}
const wallet = xrpl.Wallet.fromSeed(seed)
const client = new xrpl.Client(TESTNET_URL)
await client.connect()
await subscribe(client, wallet, appWindow)
await initialize(client, wallet, appWindow)
})
// We have to wait for the application frontend to be ready, otherise
// we might run into a race condition and the ope-dialog events
// get triggered before the callbacks are attached
appWindow.once('ready-to-show', () => {
// If there is no seed present yet, ask for it, otherwise query for the password
// for the seed that has been saved
if (!fs.existsSync(path.join(__dirname, WALLET_DIR, 'seed.txt'))) {
appWindow.webContents.send('open-seed-dialog')
} else {
appWindow.webContents.send('open-password-dialog')
}
})
}
app.whenReady().then(main)

View File

@@ -0,0 +1,81 @@
const { app, BrowserWindow, ipcMain } = require('electron')
const fs = require("fs");
const path = require('path')
const xrpl = require("xrpl")
const { initialize, subscribe, saveSaltedSeed, loadSaltedSeed } = require('./library/5_helpers')
const { sendXrp } = require('./library/7_helpers')
const TESTNET_URL = "wss://s.altnet.rippletest.net:51233"
const WALLET_DIR = 'Wallet'
const createWindow = () => {
const appWindow = new BrowserWindow({
width: 1024,
height: 768,
webPreferences: {
preload: path.join(__dirname, 'view', '7_preload.js'),
},
})
appWindow.loadFile(path.join(__dirname, 'view', '7_send-xrp.html'))
return appWindow
}
const main = async () => {
const appWindow = createWindow()
if (!fs.existsSync(WALLET_DIR)) {
// Create Wallet directory in case it does not exist yet
fs.mkdirSync(path.join(__dirname, WALLET_DIR));
}
let seed = null;
ipcMain.on('seed-entered', async (event, providedSeed) => {
seed = providedSeed
appWindow.webContents.send('open-password-dialog')
})
ipcMain.on('password-entered', async (event, password) => {
if (!fs.existsSync(path.join(__dirname, WALLET_DIR , 'seed.txt'))) {
saveSaltedSeed('../' + WALLET_DIR, seed, password)
} else {
seed = loadSaltedSeed('../' + WALLET_DIR, password)
}
const wallet = xrpl.Wallet.fromSeed(seed)
const client = new xrpl.Client(TESTNET_URL)
await client.connect()
await subscribe(client, wallet, appWindow)
await initialize(client, wallet, appWindow)
ipcMain.on('send-xrp-action', (event, paymentData) => {
sendXrp(paymentData, client, wallet).then((result) => {
appWindow.webContents.send('send-xrp-transaction-finish', result)
})
})
})
// We have to wait for the application frontend to be ready, otherise
// we might run into a race condition and the ope-dialog events
// get triggered before the callbacks are attached
appWindow.once('ready-to-show', () => {
// If there is no seed present yet, ask for it, otherwise query for the password
// for the seed that has been saved
if (!fs.existsSync(path.join(__dirname, WALLET_DIR, 'seed.txt'))) {
appWindow.webContents.send('open-seed-dialog')
} else {
appWindow.webContents.send('open-password-dialog')
}
})
}
app.whenReady().then(main)

View File

@@ -0,0 +1,88 @@
const { app, BrowserWindow, ipcMain } = require('electron')
const fs = require("fs");
const path = require('path')
const xrpl = require("xrpl")
const { initialize, subscribe, saveSaltedSeed, loadSaltedSeed } = require('./library/5_helpers')
const { sendXrp } = require('./library/7_helpers')
const { verify } = require('./library/8_helpers')
const TESTNET_URL = "wss://s.altnet.rippletest.net:51233"
const WALLET_DIR = 'Wallet'
const createWindow = () => {
const appWindow = new BrowserWindow({
width: 1024,
height: 768,
webPreferences: {
preload: path.join(__dirname, 'view', '8_preload.js'),
},
})
appWindow.loadFile(path.join(__dirname, 'view', '8_domain-verification.html'))
return appWindow
}
const main = async () => {
const appWindow = createWindow()
if (!fs.existsSync(WALLET_DIR)) {
// Create Wallet directory in case it does not exist yet
fs.mkdirSync(path.join(__dirname, WALLET_DIR));
}
let seed = null;
ipcMain.on('seed-entered', async (event, providedSeed) => {
seed = providedSeed
appWindow.webContents.send('open-password-dialog')
})
ipcMain.on('password-entered', async (event, password) => {
if (!fs.existsSync(path.join(__dirname, WALLET_DIR , 'seed.txt'))) {
saveSaltedSeed('../' + WALLET_DIR, seed, password)
} else {
seed = loadSaltedSeed('../' + WALLET_DIR, password)
}
const wallet = xrpl.Wallet.fromSeed(seed)
const client = new xrpl.Client(TESTNET_URL)
await client.connect()
await subscribe(client, wallet, appWindow)
await initialize(client, wallet, appWindow)
ipcMain.on('send-xrp-action', (event, paymentData) => {
sendXrp(paymentData, client, wallet).then((result) => {
appWindow.webContents.send('send-xrp-transaction-finish', result)
})
})
ipcMain.on('destination-account-change', (event, destinationAccount) => {
verify(destinationAccount, client).then((result) => {
appWindow.webContents.send('update-domain-verification-data', result)
})
})
})
// We have to wait for the application frontend to be ready, otherise
// we might run into a race condition and the ope-dialog events
// get triggered before the callbacks are attached
appWindow.once('ready-to-show', () => {
// If there is no seed present yet, ask for it, otherwise query for the password
// for the seed that has been saved
if (!fs.existsSync(path.join(__dirname, WALLET_DIR, 'seed.txt'))) {
appWindow.webContents.send('open-seed-dialog')
} else {
appWindow.webContents.send('open-password-dialog')
}
})
}
app.whenReady().then(main)

View File

@@ -0,0 +1,42 @@
# Build a Wallet Sample Code (JavaScript) [WIP]
This folder contains sample code for a non-custodial XRP Ledger wallet application in JavaScript. For the full
documentation, refer to the [Build a Wallet in JavaScript tutorial](build-a-wallet-in-javascript.html).
## TL;DR
Setup:
```sh
npm install
```
Run any of the scripts (higher numbers are more complete/advanced examples):
```sh
npm run hello
```
```sh
npm run async-poll
```
```sh
npm run async-subscribe
```
```sh
npm run account
```
```sh
npm run tx-history
```
```sh
npm run send-xrp
```
```sh
npm run domain-verification
```

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,47 @@
const xrpl = require("xrpl");
// The rippled server and its APIs represent time as an unsigned integer.
// This number measures the number of seconds since the "Ripple Epoch" of
// January 1, 2000 (00:00 UTC). This is like the way the Unix epoch works,
// Reference: https://xrpl.org/basic-data-types.html
const RIPPLE_EPOCH = 946684800;
const prepareAccountData = (rawAccountData, reserve) => {
const numOwners = rawAccountData.OwnerCount || 0
let xrpReserve = null
if (reserve) {
//TODO: Decimal?
xrpReserve = reserve.reserveBaseXrp + (reserve.reserveIncrementXrp * numOwners)
}
return {
classicAddress: rawAccountData.Account,
xAddress: xrpl.classicAddressToXAddress(rawAccountData.Account, false, true),
xrpBalance: xrpl.dropsToXrp(rawAccountData.Balance),
xrpReserve: xrpReserve
}
}
const prepareLedgerData = (rawLedgerData) => {
const timestamp = RIPPLE_EPOCH + rawLedgerData.ledger_time
const dateTime = new Date(timestamp * 1000)
const dateTimeString = dateTime.toLocaleDateString() + ' ' + dateTime.toLocaleTimeString()
return {
ledgerIndex: rawLedgerData.ledger_index,
ledgerHash: rawLedgerData.ledger_hash,
ledgerCloseTime: dateTimeString
}
}
const prepareReserve = (ledger) => {
const reserveBaseXrp = xrpl.dropsToXrp(ledger.reserve_base)
const reserveIncrementXrp = xrpl.dropsToXrp(ledger.reserve_inc)
return { reserveBaseXrp, reserveIncrementXrp }
}
module.exports = { prepareAccountData, prepareLedgerData, prepareReserve }

View File

@@ -0,0 +1,14 @@
const xrpl = require("xrpl");
const prepareTxData = (transactions) => {
return transactions.map(transaction => ({
confirmed: transaction.tx.date,
type: transaction.tx.TransactionType,
from: transaction.tx.Account,
to: transaction.tx.Destination,
value: xrpl.dropsToXrp(transaction.tx.Amount),
hash: transaction.tx.hash
}))
}
module.exports = { prepareTxData }

View File

@@ -0,0 +1,139 @@
const {prepareReserve, prepareAccountData, prepareLedgerData} = require("./3_helpers");
const {prepareTxData} = require("./4_helpers");
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const fernet = require("fernet");
/**
* Fetches some initial data to be displayed on application startup
*
* @param client
* @param wallet
* @param appWindow
* @returns {Promise<void>}
*/
const initialize = async (client, wallet, appWindow) => {
// Reference: https://xrpl.org/account_info.html
const accountInfoResponse = await client.request({
"command": "account_info",
"account": wallet.address,
"ledger_index": "current"
})
const accountData = prepareAccountData(accountInfoResponse.result.account_data)
appWindow.webContents.send('update-account-data', accountData)
// Reference: https://xrpl.org/account_tx.html
const txResponse = await client.request({
"command": "account_tx",
"account": wallet.address
})
const transactions = prepareTxData(txResponse.result.transactions)
appWindow.webContents.send('update-transaction-data', transactions)
}
/**
* Handles the subscriptions to ledger events and the internal routing of the responses
*
* @param client
* @param wallet
* @param appWindow
* @returns {Promise<void>}
*/
const subscribe = async (client, wallet, appWindow) => {
let reserve = null
// Reference: https://xrpl.org/subscribe.html
await client.request({
"command": "subscribe",
"streams": ["ledger"],
"accounts": [wallet.address]
})
// Reference: https://xrpl.org/subscribe.html#ledger-stream
client.on("ledgerClosed", async (rawLedgerData) => {
reserve = prepareReserve(rawLedgerData)
const ledger = prepareLedgerData(rawLedgerData)
appWindow.webContents.send('update-ledger-data', ledger)
})
// Wait for transaction on subscribed account and re-request account data
client.on("transaction", async (transaction) => {
// Reference: https://xrpl.org/account_info.html
const accountInfoRequest = {
"command": "account_info",
"account": wallet.address,
"ledger_index": transaction.ledger_index
}
const accountInfoResponse = await client.request(accountInfoRequest)
const accountData = prepareAccountData(accountInfoResponse.result.account_data)
appWindow.webContents.send('update-account-data', accountData)
const transactions = prepareTxData([{tx: transaction.transaction}])
appWindow.webContents.send('update-transaction-data', transactions)
})
}
/**
* Saves the wallet seed using proper cryptographic functions
*
* @param WALLET_DIR
* @param seed
* @param password
*/
const saveSaltedSeed = (WALLET_DIR, seed, password)=> {
const salt = crypto.randomBytes(20).toString('hex')
fs.writeFileSync(path.join(__dirname, WALLET_DIR, 'salt.txt'), salt);
// Hashing salted password using Password-Based Key Derivation Function 2
const derivedKey = crypto.pbkdf2Sync(password, salt, 1000, 32, 'sha256')
// Generate a Fernet secret we can use for symmetric encryption
const secret = new fernet.Secret(derivedKey.toString('base64'));
// Generate encryption token with secret, time and initialization vector
// In a real-world use case we would have current time and a random IV,
// but for demo purposes being deterministic is just fine
const token = new fernet.Token({
secret: secret,
time: Date.parse(1),
iv: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
})
const privateKey = token.encode(seed)
fs.writeFileSync(path.join(__dirname, WALLET_DIR, 'seed.txt'), privateKey)
}
/**
* Loads the plaintext value of the encrypted seed
*
* @param WALLET_DIR
* @param password
* @returns {*}
*/
const loadSaltedSeed = (WALLET_DIR, password) => {
const salt = fs.readFileSync(path.join(__dirname, WALLET_DIR, 'salt.txt')).toString()
const encodedSeed = fs.readFileSync(path.join(__dirname, WALLET_DIR, 'seed.txt')).toString()
// Hashing salted password using Password-Based Key Derivation Function 2
const derivedKey = crypto.pbkdf2Sync(password, salt, 1000, 32, 'sha256')
// Generate a Fernet secret we can use for symmetric encryption
const secret = new fernet.Secret(derivedKey.toString('base64'));
// Generate decryption token
const token = new fernet.Token({
secret: secret,
token: encodedSeed,
ttl: 0
})
return token.decode();
}
module.exports = { initialize, subscribe, saveSaltedSeed, loadSaltedSeed }

View File

@@ -0,0 +1,28 @@
const xrpl = require("xrpl");
/**
* Prepares, signs and submits a payment transaction
*
* @param paymentData
* @param client
* @param wallet
* @returns {Promise<*>}
*/
const sendXrp = async (paymentData, client, wallet) => {
// Reference: https://xrpl.org/submit.html#request-format-1
const paymentTx = {
"TransactionType": "Payment",
"Account": wallet.address,
"Amount": xrpl.xrpToDrops(paymentData.amount),
"Destination": paymentData.destinationAddress,
"DestinationTag": parseInt(paymentData.destinationTag)
}
const preparedTx = await client.autofill(paymentTx)
const signedTx = wallet.sign(preparedTx)
return await client.submitAndWait(signedTx.tx_blob)
}
module.exports = { sendXrp }

View File

@@ -0,0 +1,102 @@
const fetch = require('node-fetch')
const toml = require('toml');
const { convertHexToString } = require("xrpl/dist/npm/utils/stringConversion");
const lsfDisallowXRP = 0x00080000;
/* Example lookups
|------------------------------------|---------------|-----------|
| Address | Domain | Verified |
|------------------------------------|---------------|-----------|
| rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW | mduo13.com | YES |
| rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn | xrpl.org | NO |
| rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe | n/a | NO |
|------------------------------------|---------------|-----------|
*/
/**
* Check a potential destination address's details, and pass them back to the "Send XRP" dialog:
* - Is the account funded? If not, payments below the reserve base will fail
* - Do they have DisallowXRP enabled? If so, the user should be warned they don't want XRP, but can click through.
* - Do they have a verified Domain? If so, we want to show the user the associated domain info.
*
* @param accountData
* @returns {Promise<{domain: string, verified: boolean}|{domain: string, verified: boolean}>}
*/
async function checkDestination(accountData) {
const accountStatus = {
"funded": null,
"disallow_xrp": null,
"domain_verified": null,
"domain_str": "" // the decoded domain, regardless of verification
}
accountStatus["disallow_xrp"] = !!(accountData & lsfDisallowXRP);
return verifyAccountDomain(accountData)
}
/**
* Verify an account using a xrp-ledger.toml file.
* https://xrpl.org/xrp-ledger-toml.html#xrp-ledgertoml-file
*
* @param accountData
* @returns {Promise<{domain: string, verified: boolean}>}
*/
async function verifyAccountDomain(accountData) {
const domainHex = accountData["Domain"]
if (!domainHex) {
return {
domain:"",
verified: false
}
}
let verified = false
const domain = convertHexToString(domainHex)
const tomlUrl = `https://${domain}/.well-known/xrp-ledger.toml`
const tomlResponse = await fetch(tomlUrl)
const tomlData = await tomlResponse.text()
const parsedToml = toml.parse(tomlData)
const tomlAccounts = parsedToml["ACCOUNTS"]
for (const tomlAccount of tomlAccounts) {
if (tomlAccount["address"] === accountData["Account"]) {
verified = true
}
}
return {
domain: domain,
verified: verified
}
}
/**
* Verifies if a given address has validated status
*
* @param accountAddress
* @param client
* @returns {Promise<{domain: string, verified: boolean}>}
*/
async function verify(accountAddress, client) {
// Reference: https://xrpl.org/account_info.html
const request = {
"command": "account_info",
"account": accountAddress,
"ledger_index": "validated"
}
try {
const response = await client.request(request)
return await checkDestination(response.result.account_data)
} catch {
return {
domain: 'domain',
verified: false
}
}
}
module.exports = { verify }

View File

@@ -0,0 +1,28 @@
{
"name": "xrpl-javascript-desktop-wallet",
"version": "1.0.0",
"license": "MIT",
"scripts": {
"hello": "electron ./1_hello.js",
"async-poll": "electron ./2_async-poll.js",
"async-subscribe": "electron ./2_async-subscribe.js",
"account": "electron ./3_account.js",
"tx-history": "electron ./4_tx-history.js",
"password": "electron ./5_password.js",
"styling": "electron ./6_styling.js",
"send-xrp": "electron ./7_send-xrp.js",
"domain-verification": "electron ./8_domain-verification.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"
}
}

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'" />
<meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'" />
<title>XRPL Wallet Tutorial (JavaScript / Electron)</title>
</head>
<body>
<h3>Build a XRPL Wallet - Part 1/8</h3>
Latest validated ledger index: <strong id="ledger-index"></strong>
</body>
<script src="1_renderer.js"></script>
</html>

View File

@@ -0,0 +1,11 @@
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)
}
})

View File

@@ -0,0 +1,7 @@
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
})

View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'" />
<meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'" />
<title>XRPL Wallet Tutorial (JavaScript / Electron)</title>
</head>
<body>
<h3>Build a XRPL Wallet - Part 2/8</h3>
Latest validated ledger <br />
Ledger Index: <strong id="ledger-index"></strong><br />
Ledger Hash: <strong id="ledger-hash"></strong><br />
Close Time: <strong id="ledger-close-time"></strong><br />
</body>
<script src="2_renderer.js"></script>
</html>

View File

@@ -0,0 +1,7 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateLedgerData: (callback) => {
ipcRenderer.on('update-ledger-data', callback)
}
})

View File

@@ -0,0 +1,15 @@
const ledgerIndexEl = document.getElementById('ledger-index')
// Step 2 code additions - start
const ledgerHashEl = document.getElementById('ledger-hash')
const ledgerCloseTimeEl = document.getElementById('ledger-close-time')
// Step 2 code additions - end
window.electronAPI.onUpdateLedgerData((_event, value) => {
ledgerIndexEl.innerText = value.ledger_index
// Step 2 code additions - start
ledgerHashEl.innerText = value.ledger_hash
ledgerCloseTimeEl.innerText = value.ledger_time
// Step 2 code additions - end
})

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"/>
<meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'"/>
<title>XRPL Wallet Tutorial (JavaScript / Electron)</title>
</head>
<body>
<h3>Build a XRPL Wallet - Part 3/8</h3>
<fieldset>
<legend>Account</legend>
Classic Address: <strong id="account-address-classic"></strong><br/>
X-Address: <strong id="account-address-x"></strong><br/>
XRP Balance: <strong id="account-balance"></strong><br/>
XRP Reserved: <strong id="account-reserve"></strong><br/>
</fieldset>
<fieldset>
<legend>Latest validated ledger</legend>
Ledger Index: <strong id="ledger-index"></strong><br/>
Ledger Hash: <strong id="ledger-hash"></strong><br/>
Close Time: <strong id="ledger-close-time"></strong><br/>
</fieldset>
<dialog id="account-address-dialog">
<form method="dialog">
<div>
<label for="address-input">Enter account address:</label>
<input type="text" id="address-input" name="address-input" />
</div>
<div>
<button type="reset">Reset</button>
<button type="submit">Confirm</button>
</div>
</form>
</dialog>
</body>
<script src="3_renderer.js"></script>
</html>

View File

@@ -0,0 +1,16 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateLedgerData: (callback) => {
ipcRenderer.on('update-ledger-data', callback)
},
// Step 3 code additions - start
onEnterAccountAddress: (address) => {
ipcRenderer.send('address-entered', address)
},
onUpdateAccountData: (callback) => {
ipcRenderer.on('update-account-data', callback)
}
//Step 3 code additions - end
})

View File

@@ -0,0 +1,46 @@
// Step 3 code additions - start
document.addEventListener('DOMContentLoaded', openAccountAddressDialog);
function openAccountAddressDialog(){
const accountAddressDialog = document.getElementById('account-address-dialog');
const accountAddressInput = accountAddressDialog.querySelector('input');
const submitButton = accountAddressDialog.querySelector('button[type="submit"]');
const resetButton = accountAddressDialog.querySelector('button[type="reset"]');
submitButton.addEventListener('click', () => {
const address = accountAddressInput.value;
window.electronAPI.onEnterAccountAddress(address)
accountAddressDialog.close()
});
resetButton.addEventListener('click', () => {
accountAddressInput.value = '';
});
accountAddressDialog.showModal()
}
// Step 3 code additions - end
const ledgerIndexEl = document.getElementById('ledger-index')
const ledgerHashEl = document.getElementById('ledger-hash')
const ledgerCloseTimeEl = document.getElementById('ledger-close-time')
window.electronAPI.onUpdateLedgerData((_event, ledger) => {
ledgerIndexEl.innerText = ledger.ledgerIndex
ledgerHashEl.innerText = ledger.ledgerHash
ledgerCloseTimeEl.innerText = ledger.ledgerCloseTime
})
// Step 3 code additions - start
const accountAddressClassicEl = document.getElementById('account-address-classic')
const accountAddressXEl = document.getElementById('account-address-x')
const accountBalanceEl = document.getElementById('account-balance')
const accountReserveEl = document.getElementById('account-reserve')
window.electronAPI.onUpdateAccountData((_event, value) => {
accountAddressClassicEl.innerText = value.classicAddress
accountAddressXEl.innerText = value.xAddress
accountBalanceEl.innerText = value.xrpBalance
accountReserveEl.innerText = value.xrpReserve
})
// Step 3 code additions - end

View File

@@ -0,0 +1,19 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateLedgerData: (callback) => {
ipcRenderer.on('update-ledger-data', callback)
},
onEnterAccountAddress: (address) => {
ipcRenderer.send('address-entered', address)
},
onUpdateAccountData: (callback) => {
ipcRenderer.on('update-account-data', callback)
},
// Step 4 code additions - start
onUpdateTransactionData: (callback) => {
ipcRenderer.on('update-transaction-data', callback)
}
// Step 4 code additions - end
})

View File

@@ -0,0 +1,64 @@
// Step 4 code additions - start
document.addEventListener('DOMContentLoaded', openAccountAddressDialog);
function openAccountAddressDialog(){
const accountAddressDialog = document.getElementById('account-address-dialog');
const accountAddressInput = accountAddressDialog.querySelector('input');
const submitButton = accountAddressDialog.querySelector('button[type="submit"]');
const resetButton = accountAddressDialog.querySelector('button[type="reset"]');
submitButton.addEventListener('click', () => {
const address = accountAddressInput.value;
window.electronAPI.onEnterAccountAddress(address)
accountAddressDialog.close()
});
resetButton.addEventListener('click', () => {
accountAddressInput.value = '';
});
accountAddressDialog.showModal()
}
// Step 4 code additions - end
const ledgerIndexEl = document.getElementById('ledger-index')
const ledgerHashEl = document.getElementById('ledger-hash')
const ledgerCloseTimeEl = document.getElementById('ledger-close-time')
window.electronAPI.onUpdateLedgerData((_event, ledger) => {
ledgerIndexEl.innerText = ledger.ledgerIndex
ledgerHashEl.innerText = ledger.ledgerHash
ledgerCloseTimeEl.innerText = ledger.ledgerCloseTime
})
const accountAddressClassicEl = document.getElementById('account-address-classic')
const accountAddressXEl = document.getElementById('account-address-x')
const accountBalanceEl = document.getElementById('account-balance')
const accountReserveEl = document.getElementById('account-reserve')
window.electronAPI.onUpdateAccountData((_event, value) => {
accountAddressClassicEl.innerText = value.classicAddress
accountAddressXEl.innerText = value.xAddress
accountBalanceEl.innerText = value.xrpBalance
accountReserveEl.innerText = value.xrpReserve
})
// Step 4 code additions - start
const txTableBodyEl = document.getElementById('tx-table').tBodies[0]
window.testEl = txTableBodyEl
window.electronAPI.onUpdateTransactionData((_event, transactions) => {
for (let transaction of transactions) {
txTableBodyEl.insertAdjacentHTML( 'beforeend',
"<tr>" +
"<td>" + transaction.confirmed + "</td>" +
"<td>" + transaction.type + "</td>" +
"<td>" + transaction.from + "</td>" +
"<td>" + transaction.to + "</td>" +
"<td>" + transaction.value + "</td>" +
"<td>" + transaction.hash + "</td>" +
"</tr>"
)
}
})
// Step 4 code additions - end

View File

@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"/>
<meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'"/>
<title>XRPL Wallet Tutorial (JavaScript / Electron)</title>
</head>
<body>
<h3>Build a XRPL Wallet - Part 4/8</h3>
<fieldset>
<legend>Account</legend>
Classic Address: <strong id="account-address-classic"></strong><br/>
X-Address: <strong id="account-address-x"></strong><br/>
XRP Balance: <strong id="account-balance"></strong><br/>
XRP Reserved: <strong id="account-reserve"></strong><br/>
</fieldset>
<fieldset>
<legend>Latest validated ledger</legend>
Ledger Index: <strong id="ledger-index"></strong><br/>
Ledger Hash: <strong id="ledger-hash"></strong><br/>
Close Time: <strong id="ledger-close-time"></strong><br/>
</fieldset>
<fieldset>
<legend>Transactions:</legend>
<table id="tx-table">
<thead>
<tr>
<th>Confirmed</th>
<th>Type</th>
<th>From</th>
<th>To</th>
<th>Value Delivered</th>
<th>Hash</th>
</tr>
</thead>
<tbody></tbody>
</table>
</fieldset>
<dialog id="account-address-dialog">
<form method="dialog">
<div>
<label for="address-input">Enter account address:</label>
<input type="text" id="address-input" name="address-input" />
</div>
<div>
<button type="reset">Reset</button>
<button type="submit">Confirm</button>
</div>
</form>
</dialog>
</body>
<script src="4_renderer.js"></script>
</html>

View File

@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"/>
<meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'"/>
<title>XRPL Wallet Tutorial (JavaScript / Electron)</title>
</head>
<body>
<h3>Build a XRPL Wallet - Part 5/8</h3>
<fieldset>
<legend>Account</legend>
Classic Address: <strong id="account-address-classic"></strong><br/>
X-Address: <strong id="account-address-x"></strong><br/>
XRP Balance: <strong id="account-balance"></strong><br/>
XRP Reserved: <strong id="account-reserve"></strong><br/>
</fieldset>
<fieldset>
<legend>Latest validated ledger</legend>
Ledger Index: <strong id="ledger-index"></strong><br/>
Ledger Hash: <strong id="ledger-hash"></strong><br/>
Close Time: <strong id="ledger-close-time"></strong><br/>
</fieldset>
<fieldset>
<legend>Transactions:</legend>
<table id="tx-table">
<thead>
<tr>
<th>Confirmed</th>
<th>Type</th>
<th>From</th>
<th>To</th>
<th>Value Delivered</th>
<th>Hash</th>
</tr>
</thead>
<tbody></tbody>
</table>
</fieldset>
<dialog id="seed-dialog">
<form method="dialog">
<div>
<label for="seed-input">Enter seed:</label>
<input type="text" id="seed-input" name="seed-input" />
</div>
<div>
<button type="reset">Reset</button>
<button type="submit">Confirm</button>
</div>
</form>
</dialog>
<dialog id="password-dialog">
<form method="dialog">
<div>
<label for="password-input">Enter password (min-length 5):</label>
<input type="text" id="password-input" name="password-input" />
</div>
<div>
<button type="reset">Reset</button>
<button type="submit">Confirm</button>
</div>
</form>
</dialog>
</body>
<script src="5_renderer.js"></script>
</html>

View File

@@ -0,0 +1,28 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
// Step 5 code additions - start
onOpenSeedDialog: (callback) => {
ipcRenderer.on('open-seed-dialog', callback)
},
onEnterSeed: (seed) => {
ipcRenderer.send('seed-entered', seed)
},
onOpenPasswordDialog: (callback) => {
ipcRenderer.on('open-password-dialog', callback)
},
onEnterPassword: (password) => {
ipcRenderer.send('password-entered', password)
},
// Step 5 code additions - end
onUpdateLedgerData: (callback) => {
ipcRenderer.on('update-ledger-data', callback)
},
onUpdateAccountData: (callback) => {
ipcRenderer.on('update-account-data', callback)
},
onUpdateTransactionData: (callback) => {
ipcRenderer.on('update-transaction-data', callback)
}
})

View File

@@ -0,0 +1,79 @@
// Step 5 code additions - start
window.electronAPI.onOpenSeedDialog((_event) => {
const seedDialog = document.getElementById('seed-dialog');
const seedInput = seedDialog.querySelector('input');
const submitButton = seedDialog.querySelector('button[type="submit"]');
const resetButton = seedDialog.querySelector('button[type="reset"]');
submitButton.addEventListener('click', () => {
const seed = seedInput.value;
window.electronAPI.onEnterSeed(seed)
seedDialog.close()
});
resetButton.addEventListener('click', () => {
seedInput.value = '';
});
seedDialog.showModal()
})
window.electronAPI.onOpenPasswordDialog((_event) => {
const passwordDialog = document.getElementById('password-dialog');
const passwordInput = passwordDialog.querySelector('input');
const submitButton = passwordDialog.querySelector('button[type="submit"]');
const resetButton = passwordDialog.querySelector('button[type="reset"]');
submitButton.addEventListener('click', () => {
const password = passwordInput.value;
window.electronAPI.onEnterPassword(password)
passwordDialog.close()
});
resetButton.addEventListener('click', () => {
passwordInput.value = '';
});
passwordDialog.showModal()
});
// Step 5 code additions - end
const ledgerIndexEl = document.getElementById('ledger-index')
const ledgerHashEl = document.getElementById('ledger-hash')
const ledgerCloseTimeEl = document.getElementById('ledger-close-time')
window.electronAPI.onUpdateLedgerData((_eventledger, ledger) => {
ledgerIndexEl.innerText = ledger.ledgerIndex
ledgerHashEl.innerText = ledger.ledgerHash
ledgerCloseTimeEl.innerText = ledger.ledgerCloseTime
})
const accountAddressClassicEl = document.getElementById('account-address-classic')
const accountAddressXEl = document.getElementById('account-address-x')
const accountBalanceEl = document.getElementById('account-balance')
const accountReserveEl = document.getElementById('account-reserve')
window.electronAPI.onUpdateAccountData((_event, value) => {
accountAddressClassicEl.innerText = value.classicAddress
accountAddressXEl.innerText = value.xAddress
accountBalanceEl.innerText = value.xrpBalance
accountReserveEl.innerText = value.xrpReserve
})
const txTableBodyEl = document.getElementById('tx-table').tBodies[0]
window.testEl = txTableBodyEl
window.electronAPI.onUpdateTransactionData((_event, transactions) => {
for (let transaction of transactions) {
txTableBodyEl.insertAdjacentHTML( 'beforeend',
"<tr>" +
"<td>" + transaction.confirmed + "</td>" +
"<td>" + transaction.type + "</td>" +
"<td>" + transaction.from + "</td>" +
"<td>" + transaction.to + "</td>" +
"<td>" + transaction.value + "</td>" +
"<td>" + transaction.hash + "</td>" +
"</tr>"
)
}
})

View File

@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>XRPL Wallet Tutorial (JavaScript / Electron)</title>
<link rel="stylesheet" href="../bootstrap/bootstrap.min.css"/>
<link rel="stylesheet" href="./custom.css"/>
</head>
<body>
<main>
<div class="sidebar d-flex flex-column flex-shrink-0 p-3 text-white bg-dark">
<a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-white text-decoration-none">
<img class="logo" height="40"/>
</a>
<hr>
<ul class="nav nav-pills flex-column mb-auto" role="tablist">
<li class="nav-item">
<button class="nav-link active" id="dashboard-tab" data-bs-toggle="tab" data-bs-target="#dashboard"
type="button" role="tab" aria-controls="dashboard" aria-selected="true">
Dashboard
</button>
</li>
<li>
<button class="nav-link" data-bs-toggle="tab" id="transactions-tab" data-bs-target="#transactions"
type="button" role="tab" aria-controls="transactions" aria-selected="false">
Transactions
</button>
</li>
</ul>
</div>
<div class="divider"></div>
<div class="main-content tab-content d-flex flex-column flex-shrink-0 p-3 bg-light">
<div class="header border-bottom">
<h3>
Build a XRPL Wallet
<small class="text-muted">- Part 6/8</small>
</h3>
</div>
<div class="tab-pane fade show active" id="dashboard" role="tabpanel" aria-labelledby="dashboard-tab">
<h3>Account:</h3>
<ul class="list-group">
<li class="list-group-item">Classic Address: <strong id="account-address-classic"></strong></li>
<li class="list-group-item">X-Address: <strong id="account-address-x"></strong></li>
<li class="list-group-item">XRP Balance: <strong id="account-balance"></strong></li>
<li class="list-group-item">XRP Reserved: <strong id="account-reserve"></strong></li>
</ul>
<div class="spacer"></div>
<h3>
Ledger
<small class="text-muted">(Latest validated ledger)</small>
</h3>
<ul class="list-group">
<li class="list-group-item">Ledger Index: <strong id="ledger-index"></strong></li>
<li class="list-group-item">Ledger Hash: <strong id="ledger-hash"></strong></li>
<li class="list-group-item">Close Time: <strong id="ledger-close-time"></strong></li>
</ul>
</div>
<div class="tab-pane fade" id="transactions" role="tabpanel" aria-labelledby="transactions-tab">
<h3>Transactions:</h3>
<table id="tx-table" class="table">
<thead>
<tr>
<th>Confirmed</th>
<th>Type</th>
<th>From</th>
<th>To</th>
<th>Value Delivered</th>
<th>Hash</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</main>
<dialog id="seed-dialog">
<form method="dialog">
<div>
<label for="seed-input">Enter seed:</label>
<input type="text" id="seed-input" name="seed-input" />
</div>
<div>
<button type="reset">Reset</button>
<button type="submit">Confirm</button>
</div>
</form>
</dialog>
<dialog id="password-dialog">
<form method="dialog">
<div>
<label for="password-input">Enter password (min-length: 5):</label>
<input type="text" id="password-input" name="password-input" />
</div>
<div>
<button type="reset">Reset</button>
<button type="submit">Confirm</button>
</div>
</form>
</dialog>
</body>
<script src="../bootstrap/bootstrap.bundle.min.js"></script>
<script src="5_renderer.js"></script>
</html>

View File

@@ -0,0 +1,34 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
onOpenSeedDialog: (callback) => {
ipcRenderer.on('open-seed-dialog', callback)
},
onEnterSeed: (seed) => {
ipcRenderer.send('seed-entered', seed)
},
onOpenPasswordDialog: (callback) => {
ipcRenderer.on('open-password-dialog', callback)
},
onEnterPassword: (password) => {
ipcRenderer.send('password-entered', password)
},
onUpdateLedgerData: (callback) => {
ipcRenderer.on('update-ledger-data', callback)
},
onUpdateAccountData: (callback) => {
ipcRenderer.on('update-account-data', callback)
},
onUpdateTransactionData: (callback) => {
ipcRenderer.on('update-transaction-data', callback)
},
// Step 7 code additions - start
onClickSendXrp: (paymentData) => {
ipcRenderer.send('send-xrp-action', paymentData)
},
onSendXrpTransactionFinish: (callback) => {
ipcRenderer.on('send-xrp-transaction-finish', callback)
}
// Step 7 code additions - start
})

View File

@@ -0,0 +1,104 @@
window.electronAPI.onOpenSeedDialog((_event) => {
const seedDialog = document.getElementById('seed-dialog');
const seedInput = seedDialog.querySelector('input');
const submitButton = seedDialog.querySelector('button[type="submit"]');
const resetButton = seedDialog.querySelector('button[type="reset"]');
submitButton.addEventListener('click', () => {
const seed = seedInput.value;
window.electronAPI.onEnterSeed(seed)
seedDialog.close()
});
resetButton.addEventListener('click', () => {
seedInput.value = '';
});
seedDialog.showModal()
})
window.electronAPI.onOpenPasswordDialog((_event) => {
const passwordDialog = document.getElementById('password-dialog');
const passwordInput = passwordDialog.querySelector('input');
const submitButton = passwordDialog.querySelector('button[type="submit"]');
const resetButton = passwordDialog.querySelector('button[type="reset"]');
submitButton.addEventListener('click', () => {
const password = passwordInput.value;
window.electronAPI.onEnterPassword(password)
passwordDialog.close()
});
resetButton.addEventListener('click', () => {
passwordInput.value = '';
});
passwordDialog.showModal()
});
const ledgerIndexEl = document.getElementById('ledger-index')
const ledgerHashEl = document.getElementById('ledger-hash')
const ledgerCloseTimeEl = document.getElementById('ledger-close-time')
window.electronAPI.onUpdateLedgerData((_event, ledger) => {
ledgerIndexEl.innerText = ledger.ledgerIndex
ledgerHashEl.innerText = ledger.ledgerHash
ledgerCloseTimeEl.innerText = ledger.ledgerCloseTime
})
const accountAddressClassicEl = document.getElementById('account-address-classic')
const accountAddressXEl = document.getElementById('account-address-x')
const accountBalanceEl = document.getElementById('account-balance')
const accountReserveEl = document.getElementById('account-reserve')
window.electronAPI.onUpdateAccountData((_event, value) => {
accountAddressClassicEl.innerText = value.classicAddress
accountAddressXEl.innerText = value.xAddress
accountBalanceEl.innerText = value.xrpBalance
accountReserveEl.innerText = value.xrpReserve
})
const txTableBodyEl = document.getElementById('tx-table').tBodies[0]
window.electronAPI.onUpdateTransactionData((_event, transactions) => {
for (let transaction of transactions) {
txTableBodyEl.insertAdjacentHTML( 'beforeend',
"<tr>" +
"<td>" + transaction.confirmed + "</td>" +
"<td>" + transaction.type + "</td>" +
"<td>" + transaction.from + "</td>" +
"<td>" + transaction.to + "</td>" +
"<td>" + transaction.value + "</td>" +
"<td>" + transaction.hash + "</td>" +
"</tr>"
)
}
})
// Step 7 code additions - start
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) => {
destinationAddressEl.value = ''
destinationTagEl.value = ''
amountEl.value = ''
})
// Step 7 code additions - start

View File

@@ -0,0 +1,157 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>XRPL Wallet Tutorial (JavaScript / Electron)</title>
<link rel="stylesheet" href="../bootstrap/bootstrap.min.css"/>
<link rel="stylesheet" href="./custom.css"/>
</head>
<body>
<main>
<div class="sidebar d-flex flex-column flex-shrink-0 p-3 text-white bg-dark">
<a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-white text-decoration-none">
<img class="logo" height="40"/>
</a>
<hr>
<ul class="nav nav-pills flex-column mb-auto" role="tablist">
<li class="nav-item">
<button class="nav-link active" id="dashboard-tab" data-bs-toggle="tab" data-bs-target="#dashboard"
type="button" role="tab" aria-controls="dashboard" aria-selected="true">
Dashboard
</button>
</li>
<li>
<button class="nav-link" data-bs-toggle="tab" id="transactions-tab" data-bs-target="#transactions"
type="button" role="tab" aria-controls="transactions" aria-selected="false">
Transactions
</button>
</li>
</ul>
</div>
<div class="divider"></div>
<div class="main-content tab-content d-flex flex-column flex-shrink-0 p-3 bg-light">
<div class="header border-bottom">
<h3>
Build a XRPL Wallet
<small class="text-muted">- Part 7/8</small>
</h3>
<button type="button" class="btn btn-primary" id="send-xrp-modal-button">
Send XRP
</button>
</div>
<div class="tab-pane fade show active" id="dashboard" role="tabpanel" aria-labelledby="dashboard-tab">
<h3>Account:</h3>
<ul class="list-group">
<li class="list-group-item">Classic Address: <strong id="account-address-classic"></strong></li>
<li class="list-group-item">X-Address: <strong id="account-address-x"></strong></li>
<li class="list-group-item">XRP Balance: <strong id="account-balance"></strong></li>
<li class="list-group-item">XRP Reserved: <strong id="account-reserve"></strong></li>
</ul>
<div class="spacer"></div>
<h3>
Ledger
<small class="text-muted">(Latest validated ledger)</small>
</h3>
<ul class="list-group">
<li class="list-group-item">Ledger Index: <strong id="ledger-index"></strong></li>
<li class="list-group-item">Ledger Hash: <strong id="ledger-hash"></strong></li>
<li class="list-group-item">Close Time: <strong id="ledger-close-time"></strong></li>
</ul>
</div>
<div class="tab-pane fade" id="transactions" role="tabpanel" aria-labelledby="transactions-tab">
<h3>Transactions:</h3>
<table id="tx-table" class="table">
<thead>
<tr>
<th>Confirmed</th>
<th>Type</th>
<th>From</th>
<th>To</th>
<th>Value Delivered</th>
<th>Hash</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="modal fade" id="send-xrp-modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="send-xrp-modal-label">Send XRP</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="r9jEyy3nrB8D7uRc5w2k3tizKQ1q8cpeHU"
id="input-destination-address">
<span class="input-group-text">To (Address)</span>
</div>
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="12345"
id="input-destination-tag">
<span class="input-group-text">Destination Tag</span>
</div>
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="100"
id="input-xrp-amount">
<span class="input-group-text">Amount of XRP</span>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="send-xrp-submit-button">Send</button>
</div>
</div>
</div>
</div>
</main>
<dialog id="seed-dialog">
<form method="dialog">
<div>
<label for="seed-input">Enter seed:</label>
<input type="text" id="seed-input" name="seed-input" />
</div>
<div>
<button type="reset">Reset</button>
<button type="submit">Confirm</button>
</div>
</form>
</dialog>
<dialog id="password-dialog">
<form method="dialog">
<div>
<label for="password-input">Enter password (min-length: 5):</label>
<input type="text" id="password-input" name="password-input" />
</div>
<div>
<button type="reset">Reset</button>
<button type="submit">Confirm</button>
</div>
</form>
</dialog>
</body>
<script src="../bootstrap/bootstrap.bundle.min.js"></script>
<script src="7_renderer.js"></script>
</html>

View File

@@ -0,0 +1,160 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>XRPL Wallet Tutorial (JavaScript / Electron)</title>
<link rel="stylesheet" href="../bootstrap/bootstrap.min.css"/>
<link rel="stylesheet" href="./custom.css"/>
</head>
<body>
<main>
<div class="sidebar d-flex flex-column flex-shrink-0 p-3 text-white bg-dark">
<a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-white text-decoration-none">
<img class="logo" height="40"/>
</a>
<hr>
<ul class="nav nav-pills flex-column mb-auto" role="tablist">
<li class="nav-item">
<button class="nav-link active" id="dashboard-tab" data-bs-toggle="tab" data-bs-target="#dashboard"
type="button" role="tab" aria-controls="dashboard" aria-selected="true">
Dashboard
</button>
</li>
<li>
<button class="nav-link" data-bs-toggle="tab" id="transactions-tab" data-bs-target="#transactions"
type="button" role="tab" aria-controls="transactions" aria-selected="false">
Transactions
</button>
</li>
</ul>
</div>
<div class="divider"></div>
<div class="main-content tab-content d-flex flex-column flex-shrink-0 p-3 bg-light">
<div class="header border-bottom">
<h3>
Build a XRPL Wallet
<small class="text-muted">- Part 8/8</small>
</h3>
<button type="button" class="btn btn-primary" id="send-xrp-modal-button">
Send XRP
</button>
</div>
<div class="tab-pane fade show active" id="dashboard" role="tabpanel" aria-labelledby="dashboard-tab">
<h3>Account:</h3>
<ul class="list-group">
<li class="list-group-item">Classic Address: <strong id="account-address-classic"></strong></li>
<li class="list-group-item">X-Address: <strong id="account-address-x"></strong></li>
<li class="list-group-item">XRP Balance: <strong id="account-balance"></strong></li>
<li class="list-group-item">XRP Reserved: <strong id="account-reserve"></strong></li>
</ul>
<div class="spacer"></div>
<h3>
Ledger
<small class="text-muted">(Latest validated ledger)</small>
</h3>
<ul class="list-group">
<li class="list-group-item">Ledger Index: <strong id="ledger-index"></strong></li>
<li class="list-group-item">Ledger Hash: <strong id="ledger-hash"></strong></li>
<li class="list-group-item">Close Time: <strong id="ledger-close-time"></strong></li>
</ul>
</div>
<div class="tab-pane fade" id="transactions" role="tabpanel" aria-labelledby="transactions-tab">
<h3>Transactions:</h3>
<table id="tx-table" class="table">
<thead>
<tr>
<th>Confirmed</th>
<th>Type</th>
<th>From</th>
<th>To</th>
<th>Value Delivered</th>
<th>Hash</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="modal fade" id="send-xrp-modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="send-xrp-modal-label">Send XRP</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="input-group mb-3">
<div class="accountVerificationIndicator">
<span>Verification status:</span>
</div>
<input type="text" class="form-control" placeholder="rn95xwUymaMyzAKnZUGuynjZ6qk9RzV4Q7"
id="input-destination-address">
<span class="input-group-text">To (Address)</span>
</div>
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="12345"
id="input-destination-tag">
<span class="input-group-text">Destination Tag</span>
</div>
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="100"
id="input-xrp-amount">
<span class="input-group-text">Amount of XRP</span>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="send-xrp-submit-button">Send</button>
</div>
</div>
</div>
</div>
</main>
<dialog id="seed-dialog">
<form method="dialog">
<div>
<label for="seed-input">Enter seed:</label>
<input type="text" id="seed-input" name="seed-input" />
</div>
<div>
<button type="reset">Reset</button>
<button type="submit">Confirm</button>
</div>
</form>
</dialog>
<dialog id="password-dialog">
<form method="dialog">
<div>
<label for="password-input">Enter password (min-length: 5):</label>
<input type="text" id="password-input" name="password-input" />
</div>
<div>
<button type="reset">Reset</button>
<button type="submit">Confirm</button>
</div>
</form>
</dialog>
</body>
<script src="../bootstrap/bootstrap.bundle.min.js"></script>
<script src="8_renderer.js"></script>
</html>

View File

@@ -0,0 +1,41 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
onOpenSeedDialog: (callback) => {
ipcRenderer.on('open-seed-dialog', callback)
},
onEnterSeed: (seed) => {
ipcRenderer.send('seed-entered', seed)
},
onOpenPasswordDialog: (callback) => {
ipcRenderer.on('open-password-dialog', callback)
},
onEnterPassword: (password) => {
ipcRenderer.send('password-entered', password)
},
onUpdateLedgerData: (callback) => {
ipcRenderer.on('update-ledger-data', callback)
},
onUpdateAccountData: (callback) => {
ipcRenderer.on('update-account-data', callback)
},
onUpdateTransactionData: (callback) => {
ipcRenderer.on('update-transaction-data', callback)
},
onClickSendXrp: (paymentData) => {
ipcRenderer.send('send-xrp-action', paymentData)
},
onSendXrpTransactionFinish: (callback) => {
ipcRenderer.on('send-xrp-transaction-finish', callback)
},
// Step 8 code additions - start
onDestinationAccountChange: (callback) => {
ipcRenderer.send('destination-account-change', callback)
},
onUpdateDomainVerificationData: (callback) => {
ipcRenderer.on('update-domain-verification-data', callback)
},
// Step 8 code additions - start
})

View File

@@ -0,0 +1,116 @@
window.electronAPI.onOpenSeedDialog((_event) => {
const seedDialog = document.getElementById('seed-dialog');
const seedInput = seedDialog.querySelector('input');
const submitButton = seedDialog.querySelector('button[type="submit"]');
const resetButton = seedDialog.querySelector('button[type="reset"]');
submitButton.addEventListener('click', () => {
const seed = seedInput.value;
window.electronAPI.onEnterSeed(seed)
seedDialog.close()
});
resetButton.addEventListener('click', () => {
seedInput.value = '';
});
seedDialog.showModal()
})
window.electronAPI.onOpenPasswordDialog((_event) => {
const passwordDialog = document.getElementById('password-dialog');
const passwordInput = passwordDialog.querySelector('input');
const submitButton = passwordDialog.querySelector('button[type="submit"]');
const resetButton = passwordDialog.querySelector('button[type="reset"]');
submitButton.addEventListener('click', () => {
const password = passwordInput.value;
window.electronAPI.onEnterPassword(password)
passwordDialog.close()
});
resetButton.addEventListener('click', () => {
passwordInput.value = '';
});
passwordDialog.showModal()
});
const ledgerIndexEl = document.getElementById('ledger-index')
const ledgerHashEl = document.getElementById('ledger-hash')
const ledgerCloseTimeEl = document.getElementById('ledger-close-time')
window.electronAPI.onUpdateLedgerData((_event, ledger) => {
ledgerIndexEl.innerText = ledger.ledgerIndex
ledgerHashEl.innerText = ledger.ledgerHash
ledgerCloseTimeEl.innerText = ledger.ledgerCloseTime
})
const accountAddressClassicEl = document.getElementById('account-address-classic')
const accountAddressXEl = document.getElementById('account-address-x')
const accountBalanceEl = document.getElementById('account-balance')
const accountReserveEl = document.getElementById('account-reserve')
window.electronAPI.onUpdateAccountData((_event, value) => {
accountAddressClassicEl.innerText = value.classicAddress
accountAddressXEl.innerText = value.xAddress
accountBalanceEl.innerText = value.xrpBalance
accountReserveEl.innerText = value.xrpReserve
})
const txTableBodyEl = document.getElementById('tx-table').tBodies[0]
window.electronAPI.onUpdateTransactionData((_event, transactions) => {
for (let transaction of transactions) {
txTableBodyEl.insertAdjacentHTML( 'beforeend',
"<tr>" +
"<td>" + transaction.confirmed + "</td>" +
"<td>" + transaction.type + "</td>" +
"<td>" + transaction.from + "</td>" +
"<td>" + transaction.to + "</td>" +
"<td>" + transaction.value + "</td>" +
"<td>" + transaction.hash + "</td>" +
"</tr>"
)
}
})
const modalButton = document.getElementById('send-xrp-modal-button')
const modalDialog = new bootstrap.Modal(document.getElementById('send-xrp-modal'))
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})
})
window.electronAPI.onSendXrpTransactionFinish((_event) => {
destinationAddressEl.value = ''
destinationTagEl.value = ''
amountEl.value = ''
})

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="XRPLedger_DevPortal-white.svg"
id="svg991"
version="1.1"
fill="none"
viewBox="0 0 468 116"
height="116"
width="468">
<metadata
id="metadata997">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs995" />
<sodipodi:namedview
inkscape:current-layer="svg991"
inkscape:window-maximized="1"
inkscape:window-y="1"
inkscape:window-x="0"
inkscape:cy="58"
inkscape:cx="220.57619"
inkscape:zoom="2.8397436"
inkscape:pagecheckerboard="true"
showgrid="false"
id="namedview993"
inkscape:window-height="1028"
inkscape:window-width="1920"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff" />
<g
style="opacity:1"
id="g989"
opacity="0.9">
<path
style="opacity:1"
id="path979"
fill="white"
d="M191.43 51.8301L197.43 39.7701H207.5L197.29 57.7701L207.76 76.1901H197.66L191.57 63.8701L185.47 76.1901H175.4L185.87 57.7701L175.66 39.7701H185.74L191.43 51.8301ZM223.5 63.2201H218.73V76.0801H210V39.7701H224.3C228.67 39.7701 231.98 40.7001 234.37 42.6901C235.58 43.6506 236.546 44.8828 237.191 46.2866C237.835 47.6905 238.14 49.2265 238.08 50.7701C238.155 52.9971 237.605 55.2005 236.49 57.1301C235.322 58.9247 233.668 60.3501 231.72 61.2401L239.27 75.9401V76.3401H229.86L223.5 63.2201ZM218.86 56.4701H224.43C225.109 56.5414 225.795 56.4589 226.437 56.2286C227.079 55.9984 227.661 55.6263 228.14 55.1401C229.022 54.1082 229.492 52.7871 229.46 51.4301C229.509 50.754 229.416 50.0752 229.189 49.4366C228.962 48.798 228.605 48.2135 228.14 47.7201C227.653 47.2459 227.07 46.8825 226.429 46.6547C225.789 46.4269 225.107 46.34 224.43 46.4001H218.86V56.4701ZM251.73 63.7501V76.0801H243V39.7701H257.58C260.143 39.7175 262.683 40.2618 265 41.3601C267.03 42.3389 268.758 43.8489 270 45.7301C271.199 47.6808 271.797 49.9415 271.72 52.2301C271.773 53.8434 271.455 55.4474 270.789 56.9179C270.123 58.3884 269.128 59.6859 267.88 60.7101C265.36 62.8301 261.88 63.8901 257.41 63.8901H251.72L251.73 63.7501ZM251.73 57.0001H257.42C258.12 57.0708 258.827 56.9885 259.491 56.7588C260.156 56.5292 260.763 56.1577 261.27 55.6701C261.742 55.209 262.106 54.6484 262.334 54.0291C262.563 53.4098 262.65 52.7474 262.59 52.0901C262.68 50.6061 262.209 49.1425 261.27 47.9901C260.812 47.4622 260.24 47.0449 259.597 46.7695C258.955 46.4941 258.258 46.3678 257.56 46.4001H251.73V57.0001ZM296.73 69.4501H312V76.2101H287.9V39.7701H296.65V69.4501H296.73ZM337.81 60.7101H324.07V69.4501H340.37V76.2101H315.37V39.7701H340.55V46.5301H324.25V54.2101H337.9L337.81 60.7101ZM343.37 76.0801V39.7701H355.16C358.25 39.7375 361.295 40.5058 364 42.0001C366.568 43.4425 368.655 45.6093 370 48.2301C371.442 50.9709 372.216 54.0134 372.26 57.1101V58.8301C372.324 61.9609 371.595 65.0569 370.14 67.8301C368.731 70.4528 366.622 72.6337 364.048 74.1306C361.475 75.6275 358.537 76.3819 355.56 76.3101H343.42L343.37 76.0801ZM352.12 46.5301V69.4501H355.12C356.241 69.5121 357.36 69.3038 358.383 68.8426C359.407 68.3814 360.304 67.6809 361 66.8001C362.333 64.9534 363 62.3034 363 58.8501V57.2601C363 53.6801 362.333 51.0301 361 49.3101C360.287 48.4178 359.37 47.7109 358.325 47.2495C357.28 46.7881 356.14 46.5859 355 46.6601H352.09L352.12 46.5301ZM405.83 71.7001C404.158 73.3731 402.096 74.6035 399.83 75.2801C397.058 76.2148 394.145 76.6647 391.22 76.6101C386.45 76.6101 382.6 75.1501 379.82 72.2301C377.04 69.3101 375.45 65.2301 375.18 60.0401V56.8601C375.131 53.6307 375.765 50.4274 377.04 47.4601C378.217 44.9066 380.101 42.7443 382.47 41.2301C384.959 39.7722 387.806 39.038 390.69 39.1101C395.19 39.1101 398.77 40.1701 401.29 42.2901C403.81 44.4101 405.29 47.4601 405.66 51.5601H397.18C397.066 49.909 396.355 48.3558 395.18 47.1901C394.003 46.2386 392.51 45.7671 391 45.8701C389.971 45.8449 388.953 46.0881 388.047 46.5755C387.14 47.0629 386.376 47.7779 385.83 48.6501C384.465 51.0865 383.823 53.8617 383.98 56.6501V58.9001C383.98 62.4801 384.64 65.2601 385.83 67.1201C387.02 68.9801 389.01 69.9001 391.66 69.9001C393.521 70.0287 395.363 69.4621 396.83 68.3101V62.6101H390.74V56.6101H405.58V71.7001H405.83ZM433 60.7001H419.22V69.4401H435.51V76.2001H410.51V39.7701H435.69V46.5301H419.39V54.2101H433.17V60.7101L433 60.7001ZM452.21 63.2101H447.44V76.0801H438.56V39.7701H452.87C457.25 39.7701 460.56 40.7001 462.94 42.6901C464.152 43.649 465.119 44.8809 465.764 46.2851C466.409 47.6893 466.712 49.2261 466.65 50.7701C466.725 52.9971 466.175 55.2005 465.06 57.1301C463.892 58.9247 462.238 60.3501 460.29 61.2401L467.85 75.9401V76.3401H458.44L452.21 63.2101ZM447.44 56.4601H453C453.679 56.5314 454.365 56.4489 455.007 56.2186C455.649 55.9884 456.231 55.6163 456.71 55.1301C457.579 54.0965 458.038 52.7798 458 51.4301C458.046 50.7542 457.953 50.0761 457.726 49.4378C457.499 48.7996 457.143 48.2149 456.68 47.7201C456.197 47.2499 455.618 46.8888 454.983 46.6611C454.348 46.4334 453.672 46.3444 453 46.4001H447.43L447.44 56.4601Z"
opacity="0.9" />
<path
style="opacity:1"
id="path981"
fill="white"
d="M35.4 7.20001H38.2V8.86606e-06H35.4C32.4314 -0.00262172 29.4914 0.580149 26.7482 1.71497C24.0051 2.8498 21.5126 4.5144 19.4135 6.61353C17.3144 8.71266 15.6498 11.2051 14.515 13.9483C13.3801 16.6914 12.7974 19.6314 12.8 22.6V39C12.806 42.4725 11.4835 45.8158 9.10354 48.3445C6.72359 50.8732 3.46651 52.3957 0 52.6L0.2 56.2L0 59.8C3.46651 60.0043 6.72359 61.5268 9.10354 64.0555C11.4835 66.5842 12.806 69.9275 12.8 73.4V92.3C12.7894 98.5716 15.2692 104.591 19.6945 109.035C24.1198 113.479 30.1284 115.984 36.4 116V108.8C32.0513 108.797 27.8814 107.069 24.8064 103.994C21.7314 100.919 20.0026 96.7487 20 92.4V73.4C20.003 70.0079 19.1752 66.6667 17.5889 63.6684C16.0026 60.6701 13.706 58.1059 10.9 56.2C13.698 54.286 15.9885 51.7202 17.5738 48.7237C19.1592 45.7272 19.9918 42.39 20 39V22.6C20.0184 18.5213 21.6468 14.615 24.5309 11.7309C27.415 8.84683 31.3213 7.21842 35.4 7.20001V7.20001Z"
opacity="0.9" />
<path
style="opacity:1"
id="path983"
fill="white"
d="M118.6 7.2H115.8V0H118.6C124.58 0.0158944 130.309 2.40525 134.528 6.643C138.747 10.8808 141.111 16.6202 141.1 22.6V39C141.094 42.4725 142.416 45.8158 144.796 48.3445C147.176 50.8732 150.433 52.3957 153.9 52.6L153.7 56.2L153.9 59.8C150.433 60.0043 147.176 61.5268 144.796 64.0555C142.416 66.5842 141.094 69.9275 141.1 73.4V92.3C141.111 98.5716 138.631 104.591 134.206 109.035C129.78 113.479 123.772 115.984 117.5 116V108.8C121.849 108.797 126.019 107.069 129.094 103.994C132.169 100.919 133.897 96.7487 133.9 92.4V73.4C133.897 70.0079 134.725 66.6667 136.311 63.6684C137.897 60.6701 140.194 58.1059 143 56.2C140.202 54.286 137.911 51.7201 136.326 48.7237C134.741 45.7272 133.908 42.39 133.9 39V22.6C133.911 20.5831 133.523 18.584 132.759 16.7173C131.995 14.8507 130.87 13.1533 129.448 11.7225C128.027 10.2916 126.337 9.1556 124.475 8.37952C122.613 7.60345 120.617 7.20261 118.6 7.2V7.2Z"
opacity="0.9" />
<path
style="opacity:1"
id="path985"
fill="white"
d="M103.2 29H113.9L91.6 49.9C87.599 53.5203 82.3957 55.525 77 55.525C71.6042 55.525 66.4009 53.5203 62.4 49.9L40.1 29H50.8L67.7 44.8C70.2237 47.1162 73.5245 48.4013 76.95 48.4013C80.3754 48.4013 83.6763 47.1162 86.2 44.8L103.2 29Z"
opacity="0.9" />
<path
style="opacity:1"
id="path987"
fill="white"
d="M50.7 87H40L62.4 66C66.3788 62.3351 71.5905 60.3007 77 60.3007C82.4095 60.3007 87.6212 62.3351 91.6 66L114 87H103.3L86.3 71C83.7763 68.6838 80.4755 67.3987 77.05 67.3987C73.6245 67.3987 70.3237 68.6838 67.8 71L50.7 87Z"
opacity="0.9" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -0,0 +1,74 @@
body {
min-height: 100vh;
min-height: -webkit-fill-available;
}
html {
height: -webkit-fill-available;
}
main {
display: flex;
flex-wrap: nowrap;
height: 100vh;
height: -webkit-fill-available;
max-height: 100vh;
overflow-x: auto;
overflow-y: hidden;
}
.sidebar {
width: 200px;
}
.logo {
margin-left: 0;
content: url(XRPLedger_DevPortal-white.svg);
width: 162px;
height: 40px;
display: block;
}
.divider {
flex-shrink: 0;
width: 20px;
height: 100vh;
background-color: rgba(0, 0, 0, .1);
border: solid rgba(0, 0, 0, .15);
border-width: 1px 0;
box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
}
.main-content {
width: 808px;
}
.nav-link, .nav-link:hover {
color: white;
width: 100%;
}
.header {
position: relative;
margin-bottom: 20px;
}
.header button {
position: absolute;
right: 0;
top: -4px;
}
.spacer {
height: 20px;
}
.accountVerificationIndicator{
width: 100%;
}
.accountVerificationIndicator span {
font-size: 9px;
color: grey;
}

View File

@@ -0,0 +1,615 @@
---
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
<!-- STYLE_OVERRIDE: wallet -->
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 takes you through the process of creating a XRP Wallet application from scratch. Starting with a simple,
"Hello World" like example with minimal functionality, step-by-step we will add more and more complex features.
As JavaScript is a programming language that originated in the web browser ecosystem, it's not natively
supporting the creation of desktop applications. We have to pick a frameworks that enable us to write
desktop applications with JavaScript. For this tutorial we will use the [Electron Framework](https://www.electronjs.org/), as it is well
established, documented and will get us up and running without having to care for dependencies and stuff that would make
us divert too much from the goals of this tutorial.
## 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
To initialize the project we will create a package.json file with the following content:
{{ include_code("_code-samples/build-a-wallet/desktop-js/package.json", language="js", lines="1-28") }}
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. To install those dependencies, run 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.
### 1. Hello XRP Ledger
**Full code for this step:**
[`1_hello.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/1_hello.js),
[`view/1_preload.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/1_preload.js),
[`view/1_hello.html`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/1_hello.html),
[`view/1_renderer.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/1_renderer.js).
Our first step is to write a "Hello World" like application, that interacts on a very basic level with the XRP Ledger
and displays some high-level information about the current ledger state on the screen. Nothing too fancy so far, as we
will take care of styling and GUI related coding in a later step:
![Screenshot: Step 1, hello world equivalent](img/javascript-wallet-1.png)
First, we will create an entrypoint for our application, for this we create the file `1_hello.js` with the following content:
`1_hello.js`
{{ include_code("_code-samples/build-a-wallet/desktop-js/1_hello.js", language="js") }}
This code has two parts: one that creates the application window and one that calls the XRP Ledger API's [ledger method](ledger.html). The code then broadcasts an event with the API response as the payload. The frontend picks up this event and uses the payload to display the index of most recently validated ledger.
To display our results to the user, we need to create the view components that we specified in the `createWindow()` function. For this, we will create a `view` folder and add the following files there:
<!-- MULTICODE_BLOCK_START -->
*view/1_preload.js*
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/1_preload.js", language="js") }}
*view/1_hello.html*
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/1_hello.html", language="html") }}
*view/1_renderer.js*
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/1_renderer.js", language="js") }}
<!-- MULTICODE_BLOCK_END-->
The file `view/1_preload.js` does the main wiring of the application. The file `view/1_hello.html` defines the template part of the view. The file `view/1_renderer.js` contains scripts used in the template; you could also have put these in the HTML file, but it's a good practice to separate them so the code is more readable.
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 those two to a backend process and a frontend process when we think in terms of client-server applications. It works as follows:
1. Create a function that enables the frontend to subscribe to backend events (in `view/1_preload.js`)
2. Make the function available by preloading it (webPreferences.preload during window creation)
3. Create a frontend view
4. Use that function in the frontend (e.g. 1_renderer.js, loaded in 1_hello.html) to attach a callback that handles frontend updates when the event is dispatched
5. Dispatch the event from the backend (e.g. appWindow.webContents.send('update-ledger-index', value))
In the package.json file we have already defined some prepared commands to run our application according to the steps comprising the
structure of this tutorial. To get the application running at this early stage of development, run the following command:
```console
npm run hello
```
### 2.A. Show Ledger Updates by using WebSocket subscriptions
**Full code for this step:**
[`2_async-subscribe.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/2_async-subscribe.js),
[`view/2_preload.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/2_preload.js),
[`view/2_async.html`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/2_async.html),
[`view/2_renderer.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/2_renderer.js).
Our "Hello Ledger" application from Step 1. so far only shows the latest validated ledger sequence at the time when we opened it. So let's take things up a notch and add some dashboard like functionality where our wallet app will keep in sync with the ledger and display the latest specs and stats like a clock that is keeping track on time:
![Screenshot: Step 2, show ledger updates](img/javascript-wallet-2.png)
The code has been refactored (`1_hello.js` to `2_async-subscribe.js`) so that the main logic now resides in a main() function. This allows us to handle the application ready event by using an one-liner at the end of the code. We will do such refactorings regularly along our journey in order to keep the code well managed and readable.
{{ include_code("_code-samples/build-a-wallet/desktop-js/2_async-subscribe.js", language="js", lines="33-53") }}
The most relevant piece of code here is the swapping of a single call to the ledger for a subscription: Our client is now connecting to the XRPL via [WebSockets](https://en.wikipedia.org/wiki/WebSocket). This establishes a permanent bidirectional connection to the XRPL, which allows us to subscribe to events that the server sends out. This saves resources on the server, which now only sends out data we explicitly asked for when a change happens, as well as the client which does not have to sort through incoming data for relevant changes. This also reduces the complexity of the application and saves us a couple of lines of code. The subscription is happening here:
{{ include_code("_code-samples/build-a-wallet/desktop-js/2_async-subscribe.js", language="js", lines="42-45") }}
When we [subscribe](subscribe.html) to the `ledger` stream, our code gets a ´ledgerClosed´ event whenever there is a new validated ledger. The following code passes these events to the view as `update-ledger-data` events:
{{ include_code("_code-samples/build-a-wallet/desktop-js/2_async-subscribe.js", language="js", lines="48-50") }}
To get the application running at this stage of development, run the following command:
```console
npm run async-subscribe
```
### 2.B. Show Ledger Updates by Using Polling
**Full code for this step:**
[`2_async-poll.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/2_async-poll.js),
[`view/2_preload.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/2_preload.js),
[`view/2_async.html`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/2_async.html),
[`view/2_renderer.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/2_renderer.js).
In Step 2.A. we used the [subscribe method](https://xrpl.org/subscribe.html) to get the latest changes on the XRPL as soon as they happen. This is the preferred way to get such updates, because it not only reduces the complexity of our application and the data we have to handle, but also is less resource intensive on the servers.
For completeness's sake we will also implement a polling solution to get a feeling on how this would be done in cases where Websocket subscriptions are not an option.
The main difference is that instead of a subscription, The [ledger request](https://xrpl.org/ledger.html#ledger) with which we are familiar from Step 1. is used in an infinite loop:
{{ include_code("_code-samples/build-a-wallet/desktop-js/2_async-poll.js", language="js", lines="58-72") }}
To get the application running using polling, run the following command:
```console
npm run async-poll
```
### 3. Display an Account
**Full code for this step:**
[`3_account.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/3_account.js).
[`library/3_helper.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/3_helper.js).
[`view/3_preload.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/3_preload.js).
[`view/3_account.html`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/3_account.html).
[`view/3_renderer.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/3_renderer.js).
Now that we have a permanent connection to the XRPL and some code to bring the delivered data to life on our screen, it's time to add some "wallet" functionality by managing an individual account.
We will get the address of the account we want to monitor by using a HTML dialog element. We will furthermore refactor the application by encapsulating some functionality in a library. After finishing this step the application should look like this:
![Screenshot: Step 3, show account information](img/javascript-wallet-3.png)
First, we will create a new directory named `library`. In this directory we then create a file `3_helpers.js` with the following content:
`3_helpers.js`
{{ include_code("_code-samples/build-a-wallet/desktop-js/library/3_helpers.js", language="js") }}
Here we define three utility functions that will transform data we receive from the ledger, so it can be conveniently used in the frontend. as we progress in this tutorial, we will keep this pattern of putting reusable functionality in the library.
Our new main file will be called `3_account.js` and have the following content:
{{ include_code("_code-samples/build-a-wallet/desktop-js/3_account.js", language="js") }}
As you may have noticed, this is kind of an evolution from the last step. As these are rather grave changes, it's best to just copy and paste them, the relevant changes will be explained.
To update the view logic, create the following files:
<!-- MULTICODE_BLOCK_START -->
*view/3_preload.js*
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/3_preload.js", language="js") }}
*view/3_account.html*
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/3_account.html", language="html") }}
*view/3_renderer.js*
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/3_renderer.js", language="js") }}
<!-- MULTICODE_BLOCK_END -->
In the new template, we have added a HTML dialog element, which we will use to query the user for the account address we want to monitor:
`view/3_account.html`
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/3_account.html", language="html", lines="30-41") }}
To make the HTML dialog work, the following code snippet has been added to the new renderer:
`view/3_renderer.js`
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/3_renderer.js", language="js", lines="1-22") }}
In order to handle the address the user entered and send it to the main process, we have added the following snippet to `exposeInMainWorld` in `view/3_preload.js`:
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/3_preload.js", language="js", lines="4-6") }}
Note that, in contrast to our previous code, where we subscribed callbacks to events from the main process, we now send an event to the main process from the renderer context. For this we use `ipcRenderer.send()` instead of `ipcRenderer.on()`. Note that the use in the renderer also differs, while we subscribe to events from the main process immediately as soon as an `renderer.js` is loaded, we use our preloaded function only after an user interaction has taken place (`window.electronAPI.onEnterAccountAddress(address)`).
As we will know the account we want to query the leger for is known only after the user enters an address, we wrap our application logic with an event handler:
```javascript
ipcMain.on('address-entered', async (event, address) => {
// ...
})
```
To have some initial data to display for the account we have to add the following code to our main file:
{{ include_code("_code-samples/build-a-wallet/desktop-js/3_account.js", language="js", lines="50-61") }}
To keep the displayed balance of the account up-to-date, we use a transactions subscription for our account. As soon as a new transaction is registered, we issue an account_info request and send the data to the renderer:
{{ include_code("_code-samples/build-a-wallet/desktop-js/3_account.js", language="js", lines="63-71") }}
To get the application running at this stage of development, run the following command:
```console
npm run account
```
### 4. Show Account's Transactions
**Full code for this step:**
[`4_tx-history.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/4_tx-history.js).
[`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).
[`view/4_preload.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/4_tx-preload.js).
[`view/4_tx-history.html`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/4_tx-history.html).
[`view/4_renderer.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/4_tx-renderer.js).
At this point, our wallet shows the account's balance getting updated, but doesn't give us any clue about how this state came about, namely the actual transactions that caused the updates. So, our next step is to display the account's up to date transaction history using subscriptions once again:
![Screenshot: Step 4, show transaction history](img/javascript-wallet-4.png)
First, save the template file from last step as `view/4_tx-history.html`.Update this file to display the transaction list of a given account by adding the following code after the fieldset for the latest validated ledger:
`view/4_tx-history.html`
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/4_tx-history.html", language="html", lines="29-44") }}
Our preloader (`view/4_preload.js`) will be complemented with a function that allows us to subscribe to the 'update-transaction-data' event:
`view/4_preload.js`
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/4_preload.js", language="js", lines="13-15") }}
In the renderer (`view/4_renderer.js`), we define the callback that displays the latest transaction list:
`view/4_renderer.js`
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/4_renderer.js", language="js", lines="47-63") }}
Create a new main file `4_tx-history` with the contents of the file from `3_account.js`. There is already a query for the relevant data in the `client.on('transaction')` subscription. We just have to send it to the renderer by triggering the 'update-transaction-data' event:
`4_tx-history`
{{ include_code("_code-samples/build-a-wallet/desktop-js/4_tx-history.js", language="js", lines="62-63") }}
As this is only called as soon as a new transaction is recorded, our transaction table is empty at first, so we need to issue an initial call for the account transactions:
{{ include_code("_code-samples/build-a-wallet/desktop-js/4_tx-history.js", language="js", lines="76-83") }}
That is it for this step, to get the application running at this stage of development, run the following command:
```console
npm run tx-history
```
### 5. Saving the Private Keys with a Password
**Full code for this step:**
[`5_password.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/5_password.js).
[`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).
[`view/5_preload.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/5_tx-preload.js).
[`view/5_password.html`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/5_password.html).
[`view/5_renderer.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/5_tx-renderer.js).
After finishing this step the application should look like this:
![Screenshot: Step 5, use salted password](img/javascript-wallet-5.png)
By now we always query the user for an account address at application startup. We more or less have a monitoring tool for accounts that queries publicly available data. Because we want to have real wallet functionality including sending XRP, we will have to deal with private keys and seeds, which will have to be handled properly.
In this step we will query the user for an account seed and a password save this seed with a salted password.
<!-- MULTICODE_BLOCK_START -->
*5_password.js*
{{ include_code("_code-samples/build-a-wallet/desktop-js/5_password.js", language="js") }}
*library/5_helpers.js*
{{ include_code("_code-samples/build-a-wallet/desktop-js/library/5_helpers.js", language="js") }}
*view/5_preload.js*
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/5_preload.js", language="js") }}
*view/5_password.html*
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/5_password.html", language="html") }}
*view/5_renderer.js*
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/5_renderer.js", language="js") }}
<!-- MULTICODE_BLOCK_END-->
For this step we will first create a new helper function `library/5_helpers.js`. Add the following required imports to the top of the file:
{{ include_code("_code-samples/build-a-wallet/desktop-js/library/5_helpers.js", language="js", lines="1-6") }}
For saving a seed to disk, create the following function in that helper file:
{{ include_code("_code-samples/build-a-wallet/desktop-js/library/5_helpers.js", language="js", lines="86-109") }}
Here, a random string of 20 bytes is created, hex-encoded and saved in a file `Wallet/salt.txt`. This tutorial assumes that you know what a salt is but if you're new to cryptography this snippet from wikipedia explains what a "salt" is quite well:
In cryptography, a salt is random data that is used as an additional input to a one-way function that hashes data, a password or passphrase. Salts are used to safeguard passwords in storage. Historically, only the output from an invocation of a cryptographic hash function on the password was stored on a system, but, over time, additional safeguards were developed to protect against duplicate or common passwords being identifiable (as their hashes are identical).Salting is one such protection.
Next on a key suitable for symmetric encryption is generated using [Password-Based Key Derivation Function 2](https://en.wikipedia.org/wiki/PBKDF2) which basically hashes and re-hashes the password with the salt multiple times. This key is then used to encrypt the seed with a scheme called [Fernet](https://github.com/csquared/fernet.js). the encrypted key is the saved to `Wallet/seed.txt`. To implement the functionality to load and decrypt the seed add the following function to `library/5_helpers.js`:
{{ include_code("_code-samples/build-a-wallet/desktop-js/library/5_helpers.js", language="js", lines="43-77") }}
This reverses the process as it loads the salt and the encrypted seed from disk, derives a key as before and decrypts the seed.
The functionality for fetching the ledger and account data we want to send to the frontend also gets implemented in the current helper file. This helps to unclutter our main logic file `5_password.js`, which would become unreadable by now. Two functions need to be added, one for fetching the initial data on application startup and one doing the subscriptions:
{{ include_code("_code-samples/build-a-wallet/desktop-js/library/5_helpers.js", language="js", lines="16-33") }}
{{ include_code("_code-samples/build-a-wallet/desktop-js/library/5_helpers.js", language="js", lines="43-59") }}
Finally the helper functions get exported to be used in the main code:
{{ include_code("_code-samples/build-a-wallet/desktop-js/library/5_helpers.js", language="js", lines="139") }}
The main file again gets refactored from `4_transactions.js` to `5_password.js`, note that the main() function has completely changed:
{{ include_code("_code-samples/build-a-wallet/desktop-js/5_password.js", language="js") }}
In the main function, first there is a check if the `Wallet`directory used to store the salt and the encrypted seed does exist. If not, it will be created. Then the application listens for the event when the user enters his seed:
{{ include_code("_code-samples/build-a-wallet/desktop-js/5_password.js", language="js", lines="36-39") }}
This event will trigger the seed dialog in the frontend to close and the password dialog to open up. Then the application listens for the event which is triggered when the password is entered. The application checks if there is already a saved seed to be encrypted, or if it is the first time when the seed will be saved:
{{ include_code("_code-samples/build-a-wallet/desktop-js/5_password.js", language="js", lines="41-57") }}
After the seed is available to the application a wallet is created using the seed, and after creating and connecting the client the heavy lifting is done by the `nitialize` and `subscribe` functions which were implemented in `library/5_helpers.js`. Finally, the application listens to the `ready-to-show` electron event which more or less equivalent to a `domReady` event when we would be dealing with a browser-only environment. Here we trigger the opening of the password or seed dialog at application startup.
Finally, our view files will be updated by adding the following snippets:
`view/5_preload.js`
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/5_preload.js", language="js", lines="5-16") }}
`view/5_password.html`
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/5_password.html", language="html", lines="46-72") }}
This replaces the `account-address-dialog`from Step 4, as the address can be derived from the wallet instantiated with the seed. In `view/5_renderer.js` we replace the dialog logic at the top of the file with the following code:
`view/5_renderer.js`
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/5_renderer.js", language="js", lines="2-38") }}
To get the application running at this stage of development, run the following command:
```console
npm run password
```
### 6. Styling
**Full code for this step:**
[`6_styling.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/6_styling.js).
[`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).
[`view/6_preload.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/6_tx-preload.js).
[`view/6_styling.html`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/6_styling.html).
[`view/6_renderer.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/6_tx-renderer.js).
After finishing this step the application should look like this:
![Screenshot: Step 6, style application with css](img/javascript-wallet-6.png)
In this step, the application will get a facelift. First, copy the folder `bootstrap` and its contents to your project directory. Also, copy the file `view/custom.css` to the `view`directory. The Template for this Step, `view/6_styling.html` gets a complete overhaul:
`view/6_styling.html`
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/6_styling.html", language="html") }}
Note that the Bootstrap Stylesheets and the custom styles get included in the header of the file:
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/6_styling.html", language="html", lines="10-11") }}
Bootstraps minified Javascript files get included in the bottom of the template:
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/6_styling.html", language="html", lines="118-119") }}
Note that `view/5_render.js` and `view/5_preload.js` do get used in this tutorial as nothing has changed in those files. In the main file, which gets refactored from `5_password.js` to `6_styling.js` the only thing that changes is the inclusion of the updated template file:
{{ include_code("_code-samples/build-a-wallet/desktop-js/6_styling.js", language="js", lines="21") }}
That's it for this Step - to get the application running at this stage of development, run the following command:
```console
npm run styling
```
### 7. Send XRP
**Full code for this step:**
[`7_send-xrp.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/7_send-xrp.js).
[`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).
[`view/7_preload.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/7_preload.js).
[`view/7_send-xrp.html`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/7_send-xrp.html).
[`view/7_renderer.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/7_renderer.js).
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)
First, create the file `library/7_helpers.js` and add the following contents:
`library/7_helpers.js`
{{ include_code("_code-samples/build-a-wallet/desktop-js/library/7_helpers.js", language="js") }}
Here a raw payment transaction (short: tx) is created which contains all the necessary information that defines a payment from a user perspective. This payment transaction is then "autofilled", which basically adds a few fields the transaction needs to be processed correctly on the ledger. If you are interested, you could console.log the resulting prepared payment transaction.
After that, the transaction needs to be signed, which is done using the wallet object, after which it gets submitted using the `submitAndWait` function, which basically sends the signed transaction and waits for the next closed ledger to include said transaction after which it is regarded final.
Our template, after saving it as `view/7_send-xrp.html` gets updated with a bootstrap modal dialog at the end of the `<main>`tag:
`view/7_send-xrp.html`
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/7_send-xrp.html", language="html", lines="92-124") }}
The renderer evolves from `view/5_renderer.js` (remember, no modification in Step 6) to `view/7_renderer.js` by adding the following code at the end of the file:
`view/7_renderer.js`
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/7_renderer.js", language="js", lines="79-103") }}
The preload file from Step 5 also basically stays the same baring the addition of two event listeners at the end of the `exposeInMainWorld` function:
`view/7_preload.js`
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/7_preload.js", language="js", lines="27-32") }}
It might become evident by now that the changes needed to add to the applications functionality have become smaller, this is because of smart refactoring early on. The main file, now `7_send-xrp-js` differs from the last step by two small additions:
The new helper function gets included at the imports section at the top:
`7_send-xrp.js`
{{ include_code("_code-samples/build-a-wallet/desktop-js/7_send-xrp.js", language="js", lines="6") }}
Add a listener to the `send-xrp-action` event and payload from the frontend has to be implemented:
{{ include_code("_code-samples/build-a-wallet/desktop-js/7_send-xrp.js", language="js", lines="59-62") }}
That's basically it, the only thing that is missing to modify the imports of the preloader and the template:
{{ include_code("_code-samples/build-a-wallet/desktop-js/7_send-xrp.js", language="js", lines="18") }}
{{ include_code("_code-samples/build-a-wallet/desktop-js/7_send-xrp.js", language="js", lines="22") }}
To get the application running at this stage of development, run the following command:
```console
npm run send-xrp
```
### 8. Domain Verification and Polish
**Full code for this step:**
[`8_domain-verification.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/8_domain-verification.js).
[`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).
[`view/8_prelaod.js`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/desktop-js/view/8_preload.js).
[`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).
[`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 one 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 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)
As in the previous steps, the library get updated with a new helper class. First, create the file `library/8_helpers.js` and add the following contents:
`library/8_helpers.js`
{{ include_code("_code-samples/build-a-wallet/desktop-js/library/8_helpers.js", language="js") }}
Create a new main logic file named `8_domain-verification.js` in the root directory with the contents of `7_send-xrp.js`and modify it as follows, starting with the import of the new `validate`helper function:
`8_domain-verification.js`
{{ include_code("_code-samples/build-a-wallet/desktop-js/8_domain-verification.js", language="js", lines="6") }}
At the end of the callback function `ipcMain.on('send-xrp-action', callback)` add the following event handler:
{{ include_code("_code-samples/build-a-wallet/desktop-js/8_domain-verification.js", language="js", lines="66-70") }}
The code in the helper class basically issues an [`account_info`](account_info.html) request to look up the account in the ledger.
If the account does exist, the code checks for the [`lsfDisallowXRP` flag](accountroot.html#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.
And again, the modified template and preloader have to be included:
{{ include_code("_code-samples/build-a-wallet/desktop-js/8_domain-verification.js", language="js", lines="15-23") }}
Finally, the code decodes the account's `Domain` field, if present, and performs domain verification using the method imported above.
After this, it's time to update the view logic, namely template, preloader and renderer. In `view/8_domain-verification.html` add the following lines just before the `<input>` element with `id="input-destination-address`:
`view/8_domain-verification.html`
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/8_domain-verification.html", language="html", lines="101-103") }}
Now modify the line at the end of the file including the new renderer script:
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/8_domain-verification.html", language="html", lines="158") }}
The renderer script again is created by saving `view/7_renderer.js` as `view/8_renderer.js` and adding the following code after `const sendXrpButtonEl`:
`view/8_renderer.js`:
```javascript
const accountVerificationEl = document.querySelector('.accountVerificationIndicator span')
destinationAddressEl.addEventListener('input', (event) => {
window.electronAPI.onDestinationAccountChange(destinationAddressEl.value)
})
window.electronAPI.onUpdateDomainVerificationData((_event, result) => {
accountVerificationEl.textContent = `Domain: ${result.domain || 'n/a'} Verified: ${result.verified}`
})
```
The updated preloader `view/8_preloader.js` is also modified the same way by adding the following two event listeners:
`view/8_preload.js`
{{ include_code("_code-samples/build-a-wallet/desktop-js/view/8_preload.js", language="js", lines="33-38") }}
To get the application running at this stage of development, run the following command:
```console
npm run domain-verification
```
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 |
| `X7d3eHCXzwBeWrZec1yT24iZerQjYLeTFXz1GU9RBnWr7gZ` | None | No |
## Next Steps & Topics for further research
TBD
- Promises / async
- Electron framework
- Event Handler

View File

@@ -345,7 +345,7 @@ pages:
sidebar: disabled
targets:
- ja
# Redirect old Carbon Calculator page to Impact
- name: Carbon Calculator
@@ -468,7 +468,7 @@ pages:
- labels
targets:
- ja
- name: Documentation Index
longer_name: Full Documentation Index
template: page-docs-index.html.jinja
@@ -765,7 +765,7 @@ pages:
blurb: 誰もがXRP Ledger上でデジタル価値を表すトークンを作ることができます。
targets:
- ja
- name: Tokens
html: issued-currencies-overview.html
template: pagetype-redirect.html.jinja
@@ -1425,7 +1425,12 @@ pages:
targets:
- en
- ja # TODO: translate this page
- md: tutorials/build-apps/build-a-desktop-wallet-in-javascript.md
targets:
- en
- ja # TODO: translate this page
- name: Production Readiness
html: production-readiness.html
parent: tutorials.html
@@ -1921,7 +1926,7 @@ pages:
targets:
- en
- ja
- md: tutorials/manage-the-rippled-server/installation/build-run-rippled-in-reporting-mode.md
targets:
- en
@@ -2607,7 +2612,7 @@ pages:
html: ammbid.html
template: pagetype-redirect.html.jinja
redirect_url: https://opensource.ripple.com/docs/xls-30d-amm/transaction-types/ammbid/
nav_omit: true
nav_omit: true
targets:
- en
- ja
@@ -3524,7 +3529,7 @@ pages:
targets:
- ja
- name: Clio Server
- name: Clio Server
html: clio-methods.html
parent: public-api-methods.html
blurb: Use these methods to retrieve information using Clio server APIs.
@@ -4575,7 +4580,7 @@ pages:
targets:
- en
- ja
- name: XRPL Learning Portal
html: https://learn.xrpl.org/
parent: docs.html
@@ -4764,7 +4769,7 @@ pages:
blurb: The XRP Ledger (XRPL) is a community-driven public blockchain. Heres how you can get involved.
top_nav_name: Events
top_nav_hero_image: top-nav-hero-contribute
filters:
filters:
- categorize_dates
targets:
- en
@@ -4778,7 +4783,7 @@ pages:
blurb: XRP Ledger (XRPL) はコミュニティ主導のパブリックブロックチェーンです。ここでは、その参加方法について説明します。
top_nav_name: イベント
top_nav_hero_image: top-nav-hero-contribute
filters:
filters:
- categorize_dates
targets:
- ja
@@ -4896,7 +4901,7 @@ pages:
nav_omit: true
targets:
- ja
- name: Wallet
html: wallet.html
template: pagetype-redirect.html.jinja

BIN
img/javascript-wallet-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
img/javascript-wallet-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
img/javascript-wallet-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

BIN
img/javascript-wallet-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

BIN
img/javascript-wallet-5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
img/javascript-wallet-6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

BIN
img/javascript-wallet-7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

BIN
img/javascript-wallet-8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB