Merge branch 'Bounties-JavaScriptSamples-DesktopWallet' into merge_desktop_js_wallet

This commit is contained in:
mDuo13
2023-09-22 18:31:07 -07:00
58 changed files with 4396 additions and 2 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,27 @@
const { app, BrowserWindow } = require('electron')
const path = require('path')
/**
* This is our main function, it creates our application window, preloads the code we will need to communicate
* between the renderer Process and the main Process, loads a layout and performs the main logic
*/
const createWindow = () => {
// Creates the application window
const appWindow = new BrowserWindow({
width: 1024,
height: 768
})
// Loads a layout
appWindow.loadFile(path.join(__dirname, 'view', 'template.html'))
return appWindow
}
// Here we have to wait for the application to signal that it is ready
// to execute our code. In this case we just create a main window.
app.whenReady().then(() => {
createWindow()
})

View File

@@ -0,0 +1,18 @@
<!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 0/8</h3>
<span>Hello world!</span>
</body>
</html>

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', 'preload.js'),
},
})
// Loads a layout
appWindow.loadFile(path.join(__dirname, 'view', 'template.html'))
return appWindow
}
// Here we have to wait for the application to signal that it is ready
// to execute our code. In this case we 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,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,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="renderer.js"></script>
</html>

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', 'preload.js'),
},
})
appWindow.loadFile(path.join(__dirname, 'view', 'template.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,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,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>
<b>Latest validated ledger stats</b><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="renderer.js"></script>
</html>

View File

@@ -0,0 +1,78 @@
const { app, BrowserWindow, ipcMain} = require('electron')
const path = require('path')
const xrpl = require("xrpl")
const { prepareAccountData, prepareLedgerData} = require('../library/3_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', 'preload.js'),
},
})
appWindow.loadFile(path.join(__dirname, 'view', 'template.html'))
return appWindow
}
const main = async () => {
const appWindow = createWindow()
ipcMain.on('address-entered', async (event, address) => {
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) => {
const ledger = prepareLedgerData(rawLedgerData)
appWindow.webContents.send('update-ledger-data', ledger)
})
// Initial Ledger Request -> Get account details on startup
// Reference: https://xrpl.org/ledger.html
const ledgerResponse = await client.request({
"command": "ledger"
})
const initialLedgerData = prepareLedgerData(ledgerResponse.result.closed.ledger)
appWindow.webContents.send('update-ledger-data', initialLedgerData)
// 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)
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 initialAccountData = prepareAccountData(accountInfoResponse.result.account_data)
appWindow.webContents.send('update-account-data', initialAccountData)
})
}
app.whenReady().then(main)

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,35 @@
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"]');
submitButton.addEventListener('click', () => {
const address = accountAddressInput.value;
window.electronAPI.onEnterAccountAddress(address)
accountAddressDialog.close()
});
accountAddressDialog.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')
window.electronAPI.onUpdateAccountData((_event, value) => {
accountAddressClassicEl.innerText = value.classicAddress
accountAddressXEl.innerText = value.xAddress
accountBalanceEl.innerText = value.xrpBalance
})

View File

@@ -0,0 +1,45 @@
<!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/>
</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="submit">Confirm</button>
</div>
</form>
</dialog>
</body>
<script src="renderer.js"></script>
</html>

View File

@@ -0,0 +1,84 @@
const {app, BrowserWindow, ipcMain} = require('electron')
const path = require('path')
const xrpl = require("xrpl")
const { 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', 'preload.js'),
},
})
appWindow.loadFile(path.join(__dirname, 'view', 'template.html'))
return appWindow
}
const main = async () => {
const appWindow = createWindow()
ipcMain.on('address-entered', async (event, address) => {
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) => {
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)
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,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,55 @@
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"]');
submitButton.addEventListener('click', () => {
const address = accountAddressInput.value;
window.electronAPI.onEnterAccountAddress(address)
accountAddressDialog.close()
});
accountAddressDialog.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')
window.electronAPI.onUpdateAccountData((_event, value) => {
accountAddressClassicEl.innerText = value.classicAddress
accountAddressXEl.innerText = value.xAddress
accountBalanceEl.innerText = value.xrpBalance
})
// 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,61 @@
<!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/>
</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="submit">Submit</button>
</div>
</form>
</dialog>
</body>
<script src="renderer.js"></script>
</html>

View File

@@ -0,0 +1,84 @@
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', 'preload.js'),
},
})
appWindow.loadFile(path.join(__dirname, 'view', 'template.html'))
return appWindow
}
const main = async () => {
const appWindow = createWindow()
// Create Wallet directory in case it does not exist yet
if (!fs.existsSync(path.join(__dirname, 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 {
try {
seed = loadSaltedSeed(WALLET_DIR, password)
} catch (error) {
appWindow.webContents.send('open-password-dialog', true)
return
}
}
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('request-seed-change', (event) => {
fs.rmSync(path.join(__dirname, WALLET_DIR , 'seed.txt'))
fs.rmSync(path.join(__dirname, WALLET_DIR , 'salt.txt'))
appWindow.webContents.send('open-seed-dialog')
})
// 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,31 @@
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)
},
requestSeedChange: () => {
ipcRenderer.send('request-seed-change')
},
// 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,81 @@
// Step 5 code additions - start
const seedDialog = document.getElementById('seed-dialog')
const seedInput = seedDialog.querySelector('input')
const seedSubmitButton = seedDialog.querySelector('button[type="submit"]')
const seedSubmitFn = () => {
const seed = seedInput.value
window.electronAPI.onEnterSeed(seed)
seedDialog.close()
}
window.electronAPI.onOpenSeedDialog((_event) => {
seedSubmitButton.addEventListener('click', seedSubmitFn, {once : true});
seedDialog.showModal()
})
const passwordDialog = document.getElementById('password-dialog')
const passwordInput = passwordDialog.querySelector('input')
const passwordError = passwordDialog.querySelector('span.invalid-password')
const passwordSubmitButton = passwordDialog.querySelector('button[type="submit"]')
const changeSeedButton = passwordDialog.querySelector('button[type="button"]')
const handlePasswordSubmitFn = () => {
const password = passwordInput.value
window.electronAPI.onEnterPassword(password)
passwordDialog.close()
}
const handleChangeSeedFn = () => {
passwordDialog.close()
window.electronAPI.requestSeedChange()
}
window.electronAPI.onOpenPasswordDialog((_event, showInvalidPassword = false) => {
if (showInvalidPassword) {
passwordError.innerHTML = 'INVALID PASSWORD'
}
passwordSubmitButton.addEventListener('click', handlePasswordSubmitFn, {once : true});
changeSeedButton.addEventListener('click', handleChangeSeedFn, {once : true});
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')
window.electronAPI.onUpdateAccountData((_event, value) => {
accountAddressClassicEl.innerText = value.classicAddress
accountAddressXEl.innerText = value.xAddress
accountBalanceEl.innerText = value.xrpBalance
})
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,75 @@
<!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/>
</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="submit">Submit</button>
</div>
</form>
</dialog>
<dialog id="password-dialog">
<form method="dialog">
<div>
<label for="password-input">Enter password (min-length 5):</label><br />
<input type="text" id="password-input" name="password-input" /><br />
<span class="invalid-password"></span>
</div>
<div>
<button type="button">Change Seed</button>
<button type="submit">Submit</button>
</div>
</form>
</dialog>
</body>
<script src="renderer.js"></script>
</html>

View File

@@ -0,0 +1,84 @@
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', 'preload.js'),
},
})
appWindow.loadFile(path.join(__dirname, 'view', 'template.html'))
return appWindow
}
const main = async () => {
const appWindow = createWindow()
// Create Wallet directory in case it does not exist yet
if (!fs.existsSync(path.join(__dirname, 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 {
try {
seed = loadSaltedSeed(WALLET_DIR, password)
} catch (error) {
appWindow.webContents.send('open-password-dialog', true)
return
}
}
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('request-seed-change', (event) => {
fs.rmSync(path.join(__dirname, WALLET_DIR , 'seed.txt'))
fs.rmSync(path.join(__dirname, WALLET_DIR , 'salt.txt'))
appWindow.webContents.send('open-seed-dialog')
})
// We have to wait for the application frontend to be ready, otherwise
// 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,31 @@
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)
},
requestSeedChange: () => {
ipcRenderer.send('request-seed-change')
},
// 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 @@
const seedDialog = document.getElementById('seed-dialog')
const seedInput = seedDialog.querySelector('input')
const seedSubmitButton = seedDialog.querySelector('button[type="submit"]')
const seedSubmitFn = () => {
const seed = seedInput.value
window.electronAPI.onEnterSeed(seed)
seedDialog.close()
}
window.electronAPI.onOpenSeedDialog((_event) => {
seedSubmitButton.addEventListener('click', seedSubmitFn, {once : true});
seedDialog.showModal()
})
const passwordDialog = document.getElementById('password-dialog')
const passwordInput = passwordDialog.querySelector('input')
const passwordError = passwordDialog.querySelector('span.invalid-password')
const passwordSubmitButton = passwordDialog.querySelector('button[type="submit"]')
const changeSeedButton = passwordDialog.querySelector('button[type="button"]')
const handlePasswordSubmitFn = () => {
const password = passwordInput.value
window.electronAPI.onEnterPassword(password)
passwordDialog.close()
}
const handleChangeSeedFn = () => {
passwordDialog.close()
window.electronAPI.requestSeedChange()
}
window.electronAPI.onOpenPasswordDialog((_event, showInvalidPassword = false) => {
if (showInvalidPassword) {
passwordError.innerHTML = 'INVALID PASSWORD'
}
passwordSubmitButton.addEventListener('click', handlePasswordSubmitFn, {once : true});
changeSeedButton.addEventListener('click', handleChangeSeedFn, {once : true});
passwordDialog.showModal()
});
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')
window.electronAPI.onUpdateAccountData((_event, value) => {
accountAddressClassicEl.innerText = value.classicAddress
accountAddressXEl.innerText = value.xAddress
accountBalanceEl.innerText = value.xrpBalance
})
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,120 @@
<!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="../../bootstrap/custom.css"/>
</head>
<body>
<main class="bg-light">
<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">
<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>
</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="submit">Submit</button>
</div>
</form>
</dialog>
<dialog id="password-dialog">
<form method="dialog">
<div>
<label for="password-input">Enter password (min-length 5):</label><br />
<input type="text" id="password-input" name="password-input" /><br />
<span class="invalid-password"></span>
</div>
<div>
<button type="button">Change Seed</button>
<button type="submit">Submit</button>
</div>
</form>
</dialog>
</body>
<script src="../../bootstrap/bootstrap.bundle.min.js"></script>
<script src="renderer.js"></script>
</html>

View File

@@ -0,0 +1,92 @@
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', 'preload.js'),
},
})
appWindow.loadFile(path.join(__dirname, 'view', 'template.html'))
return appWindow
}
const main = async () => {
const appWindow = createWindow()
// Create Wallet directory in case it does not exist yet
if (!fs.existsSync(path.join(__dirname, 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 {
try {
seed = loadSaltedSeed(WALLET_DIR, password)
} catch (error) {
appWindow.webContents.send('open-password-dialog', true)
return
}
}
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('request-seed-change', (event) => {
fs.rmSync(path.join(__dirname, WALLET_DIR , 'seed.txt'))
fs.rmSync(path.join(__dirname, WALLET_DIR , 'salt.txt'))
appWindow.webContents.send('open-seed-dialog')
})
// We have to wait for the application frontend to be ready, otherwise
// 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,37 @@
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)
},
requestSeedChange: () => {
ipcRenderer.send('request-seed-change')
},
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,107 @@
const seedDialog = document.getElementById('seed-dialog')
const seedInput = seedDialog.querySelector('input')
const seedSubmitButton = seedDialog.querySelector('button[type="submit"]')
const seedSubmitFn = () => {
const seed = seedInput.value
window.electronAPI.onEnterSeed(seed)
seedDialog.close()
}
window.electronAPI.onOpenSeedDialog((_event) => {
seedSubmitButton.addEventListener('click', seedSubmitFn, {once : true});
seedDialog.showModal()
})
const passwordDialog = document.getElementById('password-dialog')
const passwordInput = passwordDialog.querySelector('input')
const passwordError = passwordDialog.querySelector('span.invalid-password')
const passwordSubmitButton = passwordDialog.querySelector('button[type="submit"]')
const changeSeedButton = passwordDialog.querySelector('button[type="button"]')
const handlePasswordSubmitFn = () => {
const password = passwordInput.value
window.electronAPI.onEnterPassword(password)
passwordDialog.close()
}
const handleChangeSeedFn = () => {
passwordDialog.close()
window.electronAPI.requestSeedChange()
}
window.electronAPI.onOpenPasswordDialog((_event, showInvalidPassword = false) => {
if (showInvalidPassword) {
passwordError.innerHTML = 'INVALID PASSWORD'
}
passwordSubmitButton.addEventListener('click', handlePasswordSubmitFn, {once : true});
changeSeedButton.addEventListener('click', handleChangeSeedFn, {once : true});
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')
window.electronAPI.onUpdateAccountData((_event, value) => {
accountAddressClassicEl.innerText = value.classicAddress
accountAddressXEl.innerText = value.xAddress
accountBalanceEl.innerText = value.xrpBalance
})
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, result) => {
alert('Result: ' + result.result.meta.TransactionResult)
destinationAddressEl.value = ''
destinationTagEl.value = ''
amountEl.value = ''
})
// Step 7 code additions - end

View File

@@ -0,0 +1,156 @@
<!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="../../bootstrap/custom.css"/>
</head>
<body>
<main class="bg-light">
<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">
<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>
</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" value="rP4zcp52pa7ZjhjtU9LrnFcitBUadNW8Xz"
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" value="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" value="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="submit">Confirm</button>
</div>
</form>
</dialog>
<dialog id="password-dialog">
<form method="dialog">
<div>
<label for="password-input">Enter password (min-length 5):</label><br />
<input type="text" id="password-input" name="password-input" /><br />
<span class="invalid-password"></span>
</div>
<div>
<button type="button">Change Seed</button>
<button type="submit">Submit</button>
</div>
</form>
</dialog>
</body>
<script src="../../bootstrap/bootstrap.bundle.min.js"></script>
<script src="renderer.js"></script>
</html>

View File

@@ -0,0 +1,99 @@
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', 'preload.js'),
},
})
appWindow.loadFile(path.join(__dirname, 'view', 'template.html'))
return appWindow
}
const main = async () => {
const appWindow = createWindow()
// Create Wallet directory in case it does not exist yet
if (!fs.existsSync(path.join(__dirname, 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 {
try {
seed = loadSaltedSeed(WALLET_DIR, password)
} catch (error) {
appWindow.webContents.send('open-password-dialog', true)
return
}
}
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)
})
})
})
ipcMain.on('request-seed-change', (event) => {
fs.rmSync(path.join(__dirname, WALLET_DIR , 'seed.txt'))
fs.rmSync(path.join(__dirname, WALLET_DIR , 'salt.txt'))
appWindow.webContents.send('open-seed-dialog')
})
// We have to wait for the application frontend to be ready, otherwise
// 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,44 @@
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)
},
requestSeedChange: () => {
ipcRenderer.send('request-seed-change')
},
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,119 @@
const seedDialog = document.getElementById('seed-dialog')
const seedInput = seedDialog.querySelector('input')
const seedSubmitButton = seedDialog.querySelector('button[type="submit"]')
const seedSubmitFn = () => {
const seed = seedInput.value
window.electronAPI.onEnterSeed(seed)
seedDialog.close()
}
window.electronAPI.onOpenSeedDialog((_event) => {
seedSubmitButton.addEventListener('click', seedSubmitFn, {once : true});
seedDialog.showModal()
})
const passwordDialog = document.getElementById('password-dialog')
const passwordInput = passwordDialog.querySelector('input')
const passwordError = passwordDialog.querySelector('span.invalid-password')
const passwordSubmitButton = passwordDialog.querySelector('button[type="submit"]')
const changeSeedButton = passwordDialog.querySelector('button[type="button"]')
const handlePasswordSubmitFn = () => {
const password = passwordInput.value
window.electronAPI.onEnterPassword(password)
passwordDialog.close()
}
const handleChangeSeedFn = () => {
passwordDialog.close()
window.electronAPI.requestSeedChange()
}
window.electronAPI.onOpenPasswordDialog((_event, showInvalidPassword = false) => {
if (showInvalidPassword) {
passwordError.innerHTML = 'INVALID PASSWORD'
}
passwordSubmitButton.addEventListener('click', handlePasswordSubmitFn, {once : true});
changeSeedButton.addEventListener('click', handleChangeSeedFn, {once : true});
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')
window.electronAPI.onUpdateAccountData((_event, value) => {
accountAddressClassicEl.innerText = value.classicAddress
accountAddressXEl.innerText = value.xAddress
accountBalanceEl.innerText = value.xrpBalance
})
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, result) => {
alert('Result: ' + result.result.meta.TransactionResult)
destinationAddressEl.value = ''
destinationTagEl.value = ''
amountEl.value = ''
})

View File

@@ -0,0 +1,159 @@
<!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="../../bootstrap/custom.css"/>
</head>
<body>
<main class="bg-light">
<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">
<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>
</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" value="rP4zcp52pa7ZjhjtU9LrnFcitBUadNW8Xz"
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" value="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" value="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="submit">Confirm</button>
</div>
</form>
</dialog>
<dialog id="password-dialog">
<form method="dialog">
<div>
<label for="password-input">Enter password (min-length 5):</label><br />
<input type="text" id="password-input" name="password-input" /><br />
<span class="invalid-password"></span>
</div>
<div>
<button type="button">Change Seed</button>
<button type="submit">Submit</button>
</div>
</form>
</dialog>
</body>
<script src="../../bootstrap/bootstrap.bundle.min.js"></script>
<script src="renderer.js"></script>
</html>

View File

@@ -0,0 +1,42 @@
# Build a Desktop Wallet Sample Code (JavaScript)
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
```

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

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,78 @@
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;
}
.invalid-password {
color: #dc3545;
}
.accountVerificationIndicator{
width: 100%;
}
.accountVerificationIndicator span {
font-size: 9px;
color: grey;
}

View File

@@ -0,0 +1,29 @@
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) => {
return {
classicAddress: rawAccountData.Account,
xAddress: xrpl.classicAddressToXAddress(rawAccountData.Account, false, true),
xrpBalance: xrpl.dropsToXrp(rawAccountData.Balance)
}
}
const prepareLedgerData = (rawLedgerData) => {
const timestamp = RIPPLE_EPOCH + (rawLedgerData.ledger_time ?? rawLedgerData.close_time)
const dateTime = new Date(timestamp * 1000)
const dateTimeString = dateTime.toLocaleDateString() + ' ' + dateTime.toLocaleTimeString()
return {
ledgerIndex: rawLedgerData.ledger_index,
ledgerHash: rawLedgerData.ledger_hash,
ledgerCloseTime: dateTimeString
}
}
module.exports = { prepareAccountData, prepareLedgerData }

View File

@@ -0,0 +1,34 @@
const xrpl = require("xrpl");
const prepareTxData = (transactions) => {
return transactions.map(transaction => {
let tx_value = "-"
if (transaction.meta !== undefined && transaction.meta.delivered_amount !== undefined) {
tx_value = getDisplayableAmount(transaction.meta.delivered_amount)
}
return {
confirmed: transaction.tx.date,
type: transaction.tx.TransactionType,
from: transaction.tx.Account,
to: transaction.tx.Destination ?? "-",
value: tx_value,
hash: transaction.tx.hash
}
})
}
const getDisplayableAmount = (rawAmount) => {
if (rawAmount === 'unavailable') {
// Special case for pre-2014 partial payments.
return rawAmount
} else if (typeof rawAmount === 'string') {
// It's an XRP amount in drops. Convert to decimal.
return xrpl.dropsToXrp(rawAmount) + ' XRP'
} else {
//It's a token (IOU) amount.
return rawAmount.value + ' ' + rawAmount.currency
}
}
module.exports = { prepareTxData }

View File

@@ -0,0 +1,136 @@
const {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) => {
// 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) => {
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,110 @@
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)
if (!tomlResponse.ok) {
return {
domain: domain,
verified: false
}
}
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 0-hello/index.js",
"ledger-index": "electron 1-ledger-index/index.js",
"async": "electron 2-async/index.js",
"account": "electron 3-account/index.js",
"tx-history": "electron 4-tx-history/index.js",
"password": "electron 5-password/index.js",
"styling": "electron 6-styling/index.js",
"send-xrp": "electron 7-send-xrp/index.js",
"domain-verification": "electron 8-domain-verification/index.js"
},
"dependencies": {
"async": "^3.2.4",
"fernet": "^0.4.0",
"node-fetch": "^2.6.9",
"pbkdf2-hmac": "^1.1.0",
"open": "^8.4.0",
"toml": "^3.0.0",
"xrpl": "^2.6.0"
},
"devDependencies": {
"electron": "22.3.2"
}
}

View File

@@ -18,7 +18,11 @@ async function main() {
console.log("Requesting addresses from the Testnet faucet...")
const hot_wallet = (await client.fundWallet()).wallet
const cold_wallet = (await client.fundWallet()).wallet
const customer_one_wallet = (await client.fundWallet()).wallet
const customer_two_wallet = (await client.fundWallet()).wallet
console.log(`Got hot address ${hot_wallet.address} and cold address ${cold_wallet.address}.`)
console.log(`Got customer_one address ${hot_wallet.address} and customer_two address ${cold_wallet.address}.`)
// Configure issuer (cold address) settings ----------------------------------
const cold_settings_tx = {
@@ -90,9 +94,54 @@ async function main() {
throw `Error sending transaction: ${ts_result.result.meta.TransactionResult}`
}
// Create trust line from customer_one to cold address --------------------------------
const trust_set_tx2 = {
"TransactionType": "TrustSet",
"Account": customer_one_wallet.address,
"LimitAmount": {
"currency": currency_code,
"issuer": cold_wallet.address,
"value": "10000000000" // Large limit, arbitrarily chosen
}
}
const ts_prepared2 = await client.autofill(trust_set_tx2)
const ts_signed2 = customer_one_wallet.sign(ts_prepared2)
console.log("Creating trust line from customer_one address to issuer...")
const ts_result2 = await client.submitAndWait(ts_signed2.tx_blob)
if (ts_result2.result.meta.TransactionResult == "tesSUCCESS") {
console.log(`Transaction succeeded: https://testnet.xrpl.org/transactions/${ts_signed2.hash}`)
} else {
throw `Error sending transaction: ${ts_result2.result.meta.TransactionResult}`
}
const trust_set_tx3 = {
"TransactionType": "TrustSet",
"Account": customer_two_wallet.address,
"LimitAmount": {
"currency": currency_code,
"issuer": cold_wallet.address,
"value": "10000000000" // Large limit, arbitrarily chosen
}
}
const ts_prepared3 = await client.autofill(trust_set_tx3)
const ts_signed3 = customer_two_wallet.sign(ts_prepared3)
console.log("Creating trust line from customer_two address to issuer...")
const ts_result3 = await client.submitAndWait(ts_signed3.tx_blob)
if (ts_result3.result.meta.TransactionResult == "tesSUCCESS") {
console.log(`Transaction succeeded: https://testnet.xrpl.org/transactions/${ts_signed3.hash}`)
} else {
throw `Error sending transaction: ${ts_result3.result.meta.TransactionResult}`
}
// Send token ----------------------------------------------------------------
const issue_quantity = "3840"
let issue_quantity = "3800"
const send_token_tx = {
"TransactionType": "Payment",
"Account": cold_wallet.address,
@@ -108,14 +157,68 @@ async function main() {
const pay_prepared = await client.autofill(send_token_tx)
const pay_signed = cold_wallet.sign(pay_prepared)
console.log(`Sending ${issue_quantity} ${currency_code} to ${hot_wallet.address}...`)
console.log(`Cold to hot - Sending ${issue_quantity} ${currency_code} to ${hot_wallet.address}...`)
const pay_result = await client.submitAndWait(pay_signed.tx_blob)
if (pay_result.result.meta.TransactionResult == "tesSUCCESS") {
console.log(`Transaction succeeded: https://testnet.xrpl.org/transactions/${pay_signed.hash}`)
} else {
console.log(pay_result)
throw `Error sending transaction: ${pay_result.result.meta.TransactionResult}`
}
issue_quantity = "100"
const send_token_tx2 = {
"TransactionType": "Payment",
"Account": hot_wallet.address,
"Amount": {
"currency": currency_code,
"value": issue_quantity,
"issuer": cold_wallet.address
},
"Destination": customer_one_wallet.address,
"DestinationTag": 1 // Needed since we enabled Require Destination Tags
// on the hot account earlier.
}
const pay_prepared2 = await client.autofill(send_token_tx2)
const pay_signed2 = hot_wallet.sign(pay_prepared2)
console.log(`Hot to customer_one - Sending ${issue_quantity} ${currency_code} to ${customer_one_wallet.address}...`)
const pay_result2 = await client.submitAndWait(pay_signed2.tx_blob)
if (pay_result2.result.meta.TransactionResult == "tesSUCCESS") {
console.log(`Transaction succeeded: https://testnet.xrpl.org/transactions/${pay_signed2.hash}`)
} else {
console.log(pay_result2)
throw `Error sending transaction: ${pay_result2.result.meta.TransactionResult}`
}
issue_quantity = "12"
const send_token_tx3 = {
"TransactionType": "Payment",
"Account": customer_one_wallet.address,
"Amount": {
"currency": currency_code,
"value": issue_quantity,
"issuer": cold_wallet.address
},
"Destination": customer_two_wallet.address,
"DestinationTag": 1 // Needed since we enabled Require Destination Tags
// on the hot account earlier.
}
const pay_prepared3 = await client.autofill(send_token_tx3)
const pay_signed3 = customer_one_wallet.sign(pay_prepared3)
console.log(`Customer_one to customer_two - Sending ${issue_quantity} ${currency_code} to ${customer_two_wallet.address}...`)
const pay_result3 = await client.submitAndWait(pay_signed3.tx_blob)
if (pay_result3.result.meta.TransactionResult == "tesSUCCESS") {
console.log(`Transaction succeeded: https://testnet.xrpl.org/transactions/${pay_signed3.hash}`)
} else {
console.log(pay_result3)
throw `Error sending transaction: ${pay_result3.result.meta.TransactionResult}`
}
// Check balances ------------------------------------------------------------
console.log("Getting hot address balances...")
const hot_balances = await client.request({