mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2025-11-20 03:35:51 +00:00
Merge branch 'Bounties-JavaScriptSamples-DesktopWallet' into merge_desktop_js_wallet
This commit is contained in:
132
content/_code-samples/build-a-wallet/desktop-js/.gitignore
vendored
Normal file
132
content/_code-samples/build-a-wallet/desktop-js/.gitignore
vendored
Normal 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/
|
||||
@@ -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()
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -0,0 +1,7 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
onUpdateLedgerData: (callback) => {
|
||||
ipcRenderer.on('update-ledger-data', callback)
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -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
|
||||
})
|
||||
@@ -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
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -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
|
||||
})
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
@@ -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>"
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
@@ -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>"
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -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
|
||||
})
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
})
|
||||
@@ -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 = ''
|
||||
})
|
||||
@@ -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>
|
||||
42
content/_code-samples/build-a-wallet/desktop-js/README.md
Normal file
42
content/_code-samples/build-a-wallet/desktop-js/README.md
Normal 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
|
||||
```
|
||||
@@ -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 |
7
content/_code-samples/build-a-wallet/desktop-js/bootstrap/bootstrap.bundle.min.js
vendored
Normal file
7
content/_code-samples/build-a-wallet/desktop-js/bootstrap/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
content/_code-samples/build-a-wallet/desktop-js/bootstrap/bootstrap.min.css
vendored
Normal file
7
content/_code-samples/build-a-wallet/desktop-js/bootstrap/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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;
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
28
content/_code-samples/build-a-wallet/desktop-js/package.json
Normal file
28
content/_code-samples/build-a-wallet/desktop-js/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user