mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2025-11-04 11:55:50 +00:00
Build a wallet using js - JS Bounty (#1752)
Created javascript tutorial with feedback from @mDuo13 and @JST5000 to teach folks how to build a wallet for the xrpl using javascript.
This commit is contained in:
3
content/_code-samples/build-a-wallet/js/.env
Normal file
3
content/_code-samples/build-a-wallet/js/.env
Normal file
@@ -0,0 +1,3 @@
|
||||
CLIENT="wss://s.altnet.rippletest.net/"
|
||||
EXPLORER_NETWORK="testnet"
|
||||
SEED="s████████████████████████████"
|
||||
24
content/_code-samples/build-a-wallet/js/.gitignore
vendored
Normal file
24
content/_code-samples/build-a-wallet/js/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
7
content/_code-samples/build-a-wallet/js/.prettierrc
Normal file
7
content/_code-samples/build-a-wallet/js/.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 150,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true,
|
||||
"semi": true
|
||||
}
|
||||
24
content/_code-samples/build-a-wallet/js/README.md
Normal file
24
content/_code-samples/build-a-wallet/js/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Pre-requisites
|
||||
|
||||
To implement this tutorial you should have a basic understanding of JavaScript and Node.js. You should also have a basic idea about XRP Ledger. For more information, visit the [XRP Ledger Dev Portal](https://xrpl.org) and the [XRPL Learning Portal](https://learn.xrpl.org/) for videos, libraries, and other resources.
|
||||
|
||||
Follow the steps below to get started:
|
||||
|
||||
1. [Node.js](https://nodejs.org/en/download/) (v10.15.3 or higher)
|
||||
2. Install [Yarn](https://yarnpkg.com/en/docs/install) (v1.17.3 or higher) or [NPM](https://www.npmjs.com/get-npm) (v6.4.1 or higher)
|
||||
3. Add your Seed, Client, and specify testnet/mainnet in .env file. Example .env file is provided in the repo.
|
||||
4. Run `yarn install` or `npm install` to install dependencies
|
||||
5. Start the app with `yarn dev` or `npm dev`
|
||||
|
||||
# Goals
|
||||
|
||||
At the end of this tutorial, you should be able to build a simple XRP wallet that can:
|
||||
|
||||
- Shows updates to the XRP Ledger in real-time.
|
||||
- Can view any XRP Ledger account's activity "read-only" including showing how much XRP was delivered by each transaction.
|
||||
- Shows how much XRP is set aside for the account's reserve requirement.
|
||||
- Can send direct XRP payments, and provides feedback about the intended destination address, including:
|
||||
- Displays available balance in your account
|
||||
- Verifies that the destination address is valid
|
||||
- Validates amount input to ensure it is a valid number and that the account has enough XRP to send
|
||||
- Allows addition of the destination tag
|
||||
148
content/_code-samples/build-a-wallet/js/index.css
Normal file
148
content/_code-samples/build-a-wallet/js/index.css
Normal file
@@ -0,0 +1,148 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.main_content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
.main_logo {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.logo_link {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.vanilla:hover {
|
||||
filter: drop-shadow(0 0 2em #848080f5);
|
||||
}
|
||||
|
||||
.wallet_details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
padding: 20px;
|
||||
border: 1px solid white;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.ledger_details, .send_xrp_container, .tx_history_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
padding: 20px;
|
||||
border: 1px solid white;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.send_xrp_container label {
|
||||
padding: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
border: 1px solid red !important;
|
||||
}
|
||||
|
||||
.send_xrp_container input {
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid black;
|
||||
background: lightgray;
|
||||
color: black;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.heading_h3 {
|
||||
font-size: 25px;
|
||||
font-weight: bold;
|
||||
padding: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 5px 12px;
|
||||
background: inherit;
|
||||
cursor: pointer;
|
||||
border: 1px solid white;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
color: black;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.submit_tx_button {
|
||||
color: black;
|
||||
background: white;
|
||||
margin: 30px 0 0 0;
|
||||
}
|
||||
|
||||
.submit_tx_button:disabled {
|
||||
color: gray;
|
||||
background: lightgray;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.tx_history_data {
|
||||
display: table;
|
||||
text-align: center;
|
||||
border-spacing: 10px;
|
||||
}
|
||||
|
||||
.tx_history_data th {
|
||||
border-bottom: 1px solid white;
|
||||
padding: 0 0 5px 0;
|
||||
}
|
||||
41
content/_code-samples/build-a-wallet/js/index.html
Normal file
41
content/_code-samples/build-a-wallet/js/index.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="./src/assets/xrpl.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Simple XRPL Wallet</title>
|
||||
<link rel="preload" href="./src/send-xrp/send-xrp.html">
|
||||
<link rel="preload" href="./src/transaction-history/transaction-history.html">
|
||||
<link rel="preload" href="index.css" as="style">
|
||||
<link rel="stylesheet" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="main_content">
|
||||
<div class="main_logo" id="heading_logo"></div>
|
||||
<div class="links">
|
||||
<button class="send_xrp" id="send_xrp_button">Send XRP</button>
|
||||
<button class="transaction_history" id="transaction_history_button">Transaction History</button>
|
||||
</div>
|
||||
<div class="wallet_details" id="wallet">
|
||||
<div class="heading_h3">Account Info:</div>
|
||||
<div id="loading_wallet_details">Loading Wallet Details...</div>
|
||||
<span class="wallet_address"></span>
|
||||
<span class="wallet_balance"></span>
|
||||
<span class="wallet_reserve"></span>
|
||||
<span class="wallet_xaddress"></span>
|
||||
<span class="view_more"><a id="view_more_button">View More</a></span>
|
||||
</div>
|
||||
<div class="ledger_details">
|
||||
<div class="heading_h3">Latest Validated Ledger:</div>
|
||||
<div id="loading_ledger_details">Loading Ledger Details...</div>
|
||||
<span class="ledger_index" id="ledger_index"></span>
|
||||
<span class="ledger_hash" id="ledger_hash"></span>
|
||||
<span class="close_time" id="close_time"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
71
content/_code-samples/build-a-wallet/js/index.js
Normal file
71
content/_code-samples/build-a-wallet/js/index.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Client, dropsToXrp, rippleTimeToISOTime } from 'xrpl';
|
||||
|
||||
import addXrplLogo from './src/helpers/render-xrpl-logo';
|
||||
import getWalletDetails from './src/helpers/get-wallet-details.js';
|
||||
|
||||
// Optional: Render the XRPL logo
|
||||
addXrplLogo();
|
||||
|
||||
const client = new Client(process.env.CLIENT); // Get the client from the environment variables
|
||||
|
||||
// Get the elements from the DOM
|
||||
const sendXrpButton = document.querySelector('#send_xrp_button');
|
||||
const txHistoryButton = document.querySelector('#transaction_history_button');
|
||||
const walletElement = document.querySelector('#wallet');
|
||||
const walletLoadingDiv = document.querySelector('#loading_wallet_details');
|
||||
const ledgerLoadingDiv = document.querySelector('#loading_ledger_details');
|
||||
|
||||
// Add event listeners to the buttons
|
||||
sendXrpButton.addEventListener('click', () => {
|
||||
window.location.pathname = '/src/send-xrp/send-xrp.html';
|
||||
});
|
||||
|
||||
txHistoryButton.addEventListener('click', () => {
|
||||
window.location.pathname = '/src/transaction-history/transaction-history.html';
|
||||
});
|
||||
|
||||
// Self-invoking function to connect to the client
|
||||
(async () => {
|
||||
try {
|
||||
await client.connect(); // Connect to the client
|
||||
|
||||
// Subscribe to the ledger stream
|
||||
await client.request({
|
||||
command: 'subscribe',
|
||||
streams: ['ledger'],
|
||||
});
|
||||
|
||||
// Fetch the wallet details
|
||||
getWalletDetails({ client })
|
||||
.then(({ account_data, accountReserves, xAddress, address }) => {
|
||||
walletElement.querySelector('.wallet_address').textContent = `Wallet Address: ${account_data.Account}`;
|
||||
walletElement.querySelector('.wallet_balance').textContent = `Wallet Balance: ${dropsToXrp(account_data.Balance)} XRP`;
|
||||
walletElement.querySelector('.wallet_reserve').textContent = `Wallet Reserve: ${accountReserves} XRP`;
|
||||
walletElement.querySelector('.wallet_xaddress').textContent = `X-Address: ${xAddress}`;
|
||||
|
||||
// Redirect on View More link click
|
||||
walletElement.querySelector('#view_more_button').addEventListener('click', () => {
|
||||
window.open(`https://${process.env.EXPLORER_NETWORK}.xrpl.org/accounts/${address}`, '_blank');
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
walletLoadingDiv.style.display = 'none';
|
||||
});
|
||||
|
||||
|
||||
// Fetch the latest ledger details
|
||||
client.on('ledgerClosed', (ledger) => {
|
||||
ledgerLoadingDiv.style.display = 'none';
|
||||
const ledgerIndex = document.querySelector('#ledger_index');
|
||||
const ledgerHash = document.querySelector('#ledger_hash');
|
||||
const closeTime = document.querySelector('#close_time');
|
||||
ledgerIndex.textContent = `Ledger Index: ${ledger.ledger_index}`;
|
||||
ledgerHash.textContent = `Ledger Hash: ${ledger.ledger_hash}`;
|
||||
closeTime.textContent = `Close Time: ${rippleTimeToISOTime(ledger.ledger_time)}`;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
await client.disconnect();
|
||||
console.log(error);
|
||||
}
|
||||
})();
|
||||
21
content/_code-samples/build-a-wallet/js/package.json
Normal file
21
content/_code-samples/build-a-wallet/js/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "simple-xrpl-wallet",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"events": "^3.3.0",
|
||||
"https-browserify": "^1.0.0",
|
||||
"rollup-plugin-polyfill-node": "^0.12.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"stream-http": "^3.2.0",
|
||||
"vite": "^4.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.0.3",
|
||||
"xrpl": "^2.7.0-beta.2"
|
||||
}
|
||||
}
|
||||
20
content/_code-samples/build-a-wallet/js/src/assets/xrpl.svg
Normal file
20
content/_code-samples/build-a-wallet/js/src/assets/xrpl.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" 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/>
|
||||
</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>
|
||||
<script xmlns=""/></svg>
|
||||
|
After Width: | Height: | Size: 7.9 KiB |
@@ -0,0 +1,44 @@
|
||||
import { Client, Wallet, classicAddressToXAddress } from 'xrpl';
|
||||
|
||||
export default async function getWalletDetails({ client }) {
|
||||
try {
|
||||
const wallet = Wallet.fromSeed(process.env.SEED); // Convert the seed to a wallet : https://xrpl.org/cryptographic-keys.html
|
||||
|
||||
// Get the wallet details: https://xrpl.org/account_info.html
|
||||
const {
|
||||
result: { account_data },
|
||||
} = await client.request({
|
||||
command: 'account_info',
|
||||
account: wallet.address,
|
||||
ledger_index: 'validated',
|
||||
});
|
||||
|
||||
const ownerCount = account_data.OwnerCount || 0;
|
||||
|
||||
// Get the reserve base and increment
|
||||
const {
|
||||
result: {
|
||||
info: {
|
||||
validated_ledger: { reserve_base_xrp, reserve_inc_xrp },
|
||||
},
|
||||
},
|
||||
} = await client.request({
|
||||
command: 'server_info',
|
||||
});
|
||||
|
||||
// Calculate the reserves by multiplying the owner count by the increment and adding the base reserve to it.
|
||||
const accountReserves = ownerCount * reserve_inc_xrp + reserve_base_xrp;
|
||||
|
||||
console.log('Got wallet details!');
|
||||
|
||||
return {
|
||||
account_data,
|
||||
accountReserves,
|
||||
xAddress: classicAddressToXAddress(wallet.address, false, false), // Learn more: https://xrpaddress.info/
|
||||
address: wallet.address
|
||||
};
|
||||
} catch (error) {
|
||||
console.log('Error getting wallet details', error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import xrplLogo from '../assets/xrpl.svg';
|
||||
|
||||
export default function renderXrplLogo() {
|
||||
document.getElementById('heading_logo').innerHTML = `
|
||||
<a
|
||||
href="https://xrpl.org/"
|
||||
target="_blank"
|
||||
class="logo_link"
|
||||
>
|
||||
<img id="xrpl_logo" class="logo vanilla" alt="XRPL logo" src="${xrplLogo}" />
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Wallet } from 'xrpl';
|
||||
|
||||
export default async function submitTransaction({ client, tx }) {
|
||||
try {
|
||||
// Create a wallet using the seed
|
||||
const wallet = await Wallet.fromSeed(process.env.SEED);
|
||||
tx.Account = wallet.address;
|
||||
|
||||
// Sign and submit the transaction : https://xrpl.org/send-xrp.html#send-xrp
|
||||
const response = await client.submit(tx, { wallet });
|
||||
console.log(response);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="../assets/xrpl.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Simple XRPL Wallet</title>
|
||||
<link rel="preload" href="../../index.css" as="style">
|
||||
<link rel="stylesheet" href="../../index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="main_content">
|
||||
<div class="main_logo" id="heading_logo"></div>
|
||||
<div class="links">
|
||||
<button class="home" id="home_button">Home</button>
|
||||
<button class="transaction_history" id="transaction_history_button">Transaction History</button>
|
||||
</div>
|
||||
<div class="send_xrp_container">
|
||||
<div class="heading_h3">Send XRP</div>
|
||||
<div class="available_balance" id="available_balance"></div>
|
||||
<label for="destination_address">Destination Address:</label>
|
||||
<input type="text" id="destination_address" placeholder="Destination Address" maxlength="35" />
|
||||
<span class="isvalid_destination_address" id="isvalid_destination_address"></span>
|
||||
<label for="amount">Amount:</label>
|
||||
<input type="text" id="amount" placeholder="Amount" type="mobile" />
|
||||
<label for="destination_tag">Destination Tag:</label>
|
||||
<input type="text" id="destination_tag" placeholder="Destination Tag" />
|
||||
<button class="submit_tx_button" id="submit_tx_button">Submit Transaction</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="./send-xrp.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
146
content/_code-samples/build-a-wallet/js/src/send-xrp/send-xrp.js
Normal file
146
content/_code-samples/build-a-wallet/js/src/send-xrp/send-xrp.js
Normal file
@@ -0,0 +1,146 @@
|
||||
import { Client, Wallet, dropsToXrp, isValidClassicAddress, xrpToDrops } from 'xrpl';
|
||||
|
||||
import getWalletDetails from '../helpers/get-wallet-details';
|
||||
import renderXrplLogo from '../helpers/render-xrpl-logo';
|
||||
import submitTransaction from '../helpers/submit-transaction';
|
||||
|
||||
// Optional: Render the XRPL logo
|
||||
renderXrplLogo();
|
||||
|
||||
const client = new Client(process.env.CLIENT); // Get the client from the environment variables
|
||||
|
||||
// Self-invoking function to connect to the client
|
||||
(async () => {
|
||||
try {
|
||||
await client.connect(); // Connect to the client
|
||||
|
||||
const wallet = Wallet.fromSeed(process.env.SEED); // Convert the seed to a wallet : https://xrpl.org/cryptographic-keys.html
|
||||
|
||||
// Subscribe to account transaction stream
|
||||
await client.request({
|
||||
command: 'subscribe',
|
||||
accounts: [wallet.address],
|
||||
});
|
||||
|
||||
// Fetch the wallet details and show the available balance
|
||||
await getWalletDetails({ client }).then(({ accountReserves, account_data }) => {
|
||||
availableBalanceElement.textContent = `Available Balance: ${dropsToXrp(account_data.Balance) - accountReserves} XRP`;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
await client.disconnect();
|
||||
console.log(error);
|
||||
}
|
||||
})();
|
||||
|
||||
// Get the elements from the DOM
|
||||
const homeButton = document.querySelector('#home_button');
|
||||
const txHistoryButton = document.querySelector('#transaction_history_button');
|
||||
const destinationAddress = document.querySelector('#destination_address');
|
||||
const amount = document.querySelector('#amount');
|
||||
const destinationTag = document.querySelector('#destination_tag');
|
||||
const submitTxBtn = document.querySelector('#submit_tx_button');
|
||||
const availableBalanceElement = document.querySelector('#available_balance');
|
||||
|
||||
// Disable the submit button by default
|
||||
submitTxBtn.disabled = true;
|
||||
let isValidDestinationAddress = false;
|
||||
const allInputs = document.querySelectorAll('#destination_address, #amount');
|
||||
|
||||
// Add event listener to the redirect buttons
|
||||
homeButton.addEventListener('click', () => {
|
||||
window.location.pathname = '/index.html';
|
||||
});
|
||||
|
||||
txHistoryButton.addEventListener('click', () => {
|
||||
window.location.pathname = '/src/transaction-history/transaction-history.html';
|
||||
});
|
||||
|
||||
// Update the account balance on successful transaction
|
||||
client.on('transaction', (response) => {
|
||||
if (response.validated && response.transaction.TransactionType === 'Payment') {
|
||||
getWalletDetails({ client }).then(({ accountReserves, account_data }) => {
|
||||
availableBalanceElement.textContent = `Available Balance: ${dropsToXrp(account_data.Balance) - accountReserves} XRP`;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const validateAddress = () => {
|
||||
destinationAddress.value = destinationAddress.value.trim();
|
||||
// Check if the address is valid
|
||||
if (isValidClassicAddress(destinationAddress.value)) {
|
||||
// Remove the invalid class if the address is valid
|
||||
destinationAddress.classList.remove('invalid');
|
||||
isValidDestinationAddress = true;
|
||||
} else {
|
||||
// Add the invalid class if the address is invalid
|
||||
isValidDestinationAddress = false;
|
||||
destinationAddress.classList.add('invalid');
|
||||
}
|
||||
};
|
||||
|
||||
// Add event listener to the destination address
|
||||
destinationAddress.addEventListener('input', validateAddress);
|
||||
|
||||
// Add event listener to the amount input
|
||||
amount.addEventListener('keydown', (event) => {
|
||||
const codes = [8, 190];
|
||||
const regex = /^[0-9\b.]+$/;
|
||||
|
||||
// Allow: backspace, delete, tab, escape, enter and .
|
||||
if (!(regex.test(event.key) || codes.includes(event.keyCode))) {
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// NOTE: Keep this code at the bottom of the other input event listeners
|
||||
// All the inputs should have a value to enable the submit button
|
||||
for (let i = 0; i < allInputs.length; i++) {
|
||||
allInputs[i].addEventListener('input', () => {
|
||||
let values = [];
|
||||
allInputs.forEach((v) => values.push(v.value));
|
||||
submitTxBtn.disabled = !isValidDestinationAddress || values.includes('');
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listener to the submit button
|
||||
submitTxBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
console.log('Submitting transaction');
|
||||
submitTxBtn.disabled = true;
|
||||
submitTxBtn.textContent = 'Submitting...';
|
||||
|
||||
// Create the transaction object: https://xrpl.org/transaction-common-fields.html
|
||||
const txJson = {
|
||||
TransactionType: 'Payment',
|
||||
Amount: xrpToDrops(amount.value), // Convert XRP to drops: https://xrpl.org/basic-data-types.html#specifying-currency-amounts
|
||||
Destination: destinationAddress.value,
|
||||
};
|
||||
|
||||
// Get the destination tag if it exists
|
||||
if (destinationTag?.value !== '') {
|
||||
txJson.DestinationTag = destinationTag.value;
|
||||
}
|
||||
|
||||
// Submit the transaction to the ledger
|
||||
const { result } = await submitTransaction({ client, tx: txJson });
|
||||
const txResult = result?.meta?.TransactionResult || result?.engine_result || ''; // Response format: https://xrpl.org/transaction-results.html
|
||||
|
||||
// Check if the transaction was successful or not and show the appropriate message to the user
|
||||
if (txResult === 'tesSUCCESS') {
|
||||
alert('Transaction submitted successfully!');
|
||||
} else {
|
||||
throw new Error(txResult);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error submitting transaction, Please try again.');
|
||||
console.error(error);
|
||||
} finally {
|
||||
// Re-enable the submit button after the transaction is submitted so the user can submit another transaction
|
||||
submitTxBtn.disabled = false;
|
||||
submitTxBtn.textContent = 'Submit Transaction';
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="../assets/xrpl.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Simple XRPL Wallet</title>
|
||||
<link rel="preload" href="../../index.css" as="style">
|
||||
<link rel="stylesheet" href="../../index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="main_content">
|
||||
<div class="main_logo" id="heading_logo"></div>
|
||||
<div class="links">
|
||||
<button class="home" id="home_button">Home</button>
|
||||
<button class="send_xrp" id="send_xrp_button">Send XRP</button>
|
||||
</div>
|
||||
<div class="tx_history_container">
|
||||
<div class="heading_h3">Transaction History</div>
|
||||
<div class="tx_history_data" id="tx_history_data"></div>
|
||||
<button class="load_more_button" id="load_more_button">Load More</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="./transaction-history.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,146 @@
|
||||
import { Client, Wallet, convertHexToString, dropsToXrp } from 'xrpl';
|
||||
|
||||
import renderXrplLogo from '../helpers/render-xrpl-logo';
|
||||
|
||||
// Optional: Render the XRPL logo
|
||||
renderXrplLogo();
|
||||
|
||||
// Declare the variables
|
||||
let marker = null;
|
||||
|
||||
// Get the elements from the DOM
|
||||
const txHistoryElement = document.querySelector('#tx_history_data');
|
||||
const sendXrpButton = document.querySelector('#send_xrp_button');
|
||||
const homeButton = document.querySelector('#home_button');
|
||||
const loadMore = document.querySelector('#load_more_button');
|
||||
|
||||
// Add event listeners to the buttons
|
||||
sendXrpButton.addEventListener('click', () => {
|
||||
window.location.pathname = '/src/send-xrp/send-xrp.html';
|
||||
});
|
||||
|
||||
homeButton.addEventListener('click', () => {
|
||||
window.location.pathname = '/index.html';
|
||||
});
|
||||
|
||||
// Add the header to the table
|
||||
const header = document.createElement('tr');
|
||||
header.innerHTML = `
|
||||
<th>Account</th>
|
||||
<th>Destination</th>
|
||||
<th>Fee (XRP)</th>
|
||||
<th>Amount</th>
|
||||
<th>Transaction Type</th>
|
||||
<th>Result</th>
|
||||
<th>Link</th>
|
||||
`;
|
||||
txHistoryElement.appendChild(header);
|
||||
|
||||
// Converts the hex value to a string
|
||||
const getTokenName = (value) => (value.length === 40 ? convertHexToString(value).replaceAll('\u0000', '') : value);
|
||||
|
||||
function renderTokenValueColumn(value) {
|
||||
return value.Amount
|
||||
? `<td>${
|
||||
typeof value.Amount === 'object' ? `${value.Amount.value} ${getTokenName(value.Amount.currency)}` : `${dropsToXrp(value.Amount)} XRP`
|
||||
}</td>`
|
||||
: '-';
|
||||
}
|
||||
|
||||
// Fetches the transaction history from the ledger
|
||||
async function fetchTxHistory() {
|
||||
try {
|
||||
loadMore.textContent = 'Loading...';
|
||||
loadMore.disabled = true;
|
||||
const wallet = Wallet.fromSeed(process.env.SEED);
|
||||
const client = new Client(process.env.CLIENT);
|
||||
|
||||
// Wait for the client to connect
|
||||
await client.connect();
|
||||
|
||||
// Get the transaction history
|
||||
const payload = {
|
||||
command: 'account_tx',
|
||||
account: wallet.address,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
if (marker) {
|
||||
payload.marker = marker;
|
||||
}
|
||||
|
||||
// Wait for the response: use the client.request() method to send the payload
|
||||
const { result } = await client.request(payload);
|
||||
|
||||
const { transactions, marker: nextMarker } = result;
|
||||
|
||||
// Add the transactions to the table
|
||||
const values = transactions.map((transaction) => {
|
||||
const { meta, tx } = transaction;
|
||||
return {
|
||||
Account: tx.Account,
|
||||
Destination: tx.Destination,
|
||||
Fee: tx.Fee,
|
||||
Amount: tx.Amount,
|
||||
Hash: tx.hash,
|
||||
TransactionType: tx.TransactionType,
|
||||
result: meta?.TransactionResult,
|
||||
};
|
||||
});
|
||||
|
||||
// If there are no more transactions, hide the load more button
|
||||
loadMore.style.display = nextMarker ? 'block' : 'none';
|
||||
|
||||
// If there are no transactions, show a message
|
||||
// Create a new row: https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement
|
||||
// Add the row to the table: https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild
|
||||
|
||||
if (values.length === 0) {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `<td colspan="6">No transactions found</td>`;
|
||||
txHistoryElement.appendChild(row);
|
||||
} else {
|
||||
// Otherwise, show the transactions by iterating over each transaction and adding it to the table
|
||||
values.forEach((value) => {
|
||||
const row = document.createElement('tr');
|
||||
// Add the transaction details to the row
|
||||
row.innerHTML = `
|
||||
${value.Account ? `<td>${value.Account}</td>` : '-'}
|
||||
${value.Destination ? `<td>${value.Destination}</td>` : '-'}
|
||||
${value.Fee ? `<td>${dropsToXrp(value.Fee)}</td>` : '-'}
|
||||
${renderTokenValueColumn(value)}
|
||||
${value.TransactionType ? `<td>${value.TransactionType}</td>` : '-'}
|
||||
${value.result ? `<td>${value.result}</td>` : '-'}
|
||||
${value.Hash ? `<td><a href="https://${process.env.EXPLORER_NETWORK}.xrpl.org/transactions/${value.Hash}" target="_blank">View</a></td>` : '-'}`;
|
||||
// Add the row to the table
|
||||
txHistoryElement.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Disconnect
|
||||
await client.disconnect();
|
||||
|
||||
// Enable the load more button only if there are more transactions
|
||||
loadMore.textContent = 'Load More';
|
||||
loadMore.disabled = false;
|
||||
|
||||
// Return the marker
|
||||
return nextMarker ?? null;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Render the transaction history
|
||||
async function renderTxHistory() {
|
||||
// Fetch the transaction history
|
||||
marker = await fetchTxHistory();
|
||||
loadMore.addEventListener('click', async () => {
|
||||
const nextMarker = await fetchTxHistory();
|
||||
marker = nextMarker;
|
||||
});
|
||||
}
|
||||
|
||||
// Call the renderTxHistory() function
|
||||
renderTxHistory();
|
||||
43
content/_code-samples/build-a-wallet/js/vite.config.js
Normal file
43
content/_code-samples/build-a-wallet/js/vite.config.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
|
||||
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
|
||||
import polyfillNode from 'rollup-plugin-polyfill-node';
|
||||
|
||||
const viteConfig = ({ mode }) => {
|
||||
process.env = { ...process.env, ...loadEnv(mode, '', '') };
|
||||
return defineConfig({
|
||||
define: {
|
||||
'process.env': process.env,
|
||||
},
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
define: {
|
||||
global: 'globalThis',
|
||||
},
|
||||
plugins: [
|
||||
NodeGlobalsPolyfillPlugin({
|
||||
process: true,
|
||||
buffer: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
plugins: [polyfillNode()],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
events: 'events',
|
||||
crypto: 'crypto-browserify',
|
||||
stream: 'stream-browserify',
|
||||
http: 'stream-http',
|
||||
https: 'https-browserify',
|
||||
ws: 'xrpl/dist/npm/client/WSWrapper',
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default viteConfig;
|
||||
1024
content/_code-samples/build-a-wallet/js/yarn.lock
Normal file
1024
content/_code-samples/build-a-wallet/js/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
211
content/tutorials/build-apps/build-a-browser-wallet-in-js.md
Normal file
211
content/tutorials/build-apps/build-a-browser-wallet-in-js.md
Normal file
@@ -0,0 +1,211 @@
|
||||
---
|
||||
parent: build-apps.html
|
||||
targets:
|
||||
- en
|
||||
- ja # TODO: translate this page
|
||||
blurb: Build a graphical browser wallet for the XRPL using Javascript.
|
||||
---
|
||||
# Build A Browser Wallet Using JS
|
||||
<!-- STYLE_OVERRIDE: wallet -->
|
||||
|
||||
This tutorial demonstrates how to build a browser wallet for the XRP Ledger using the Javascript programming language and various libraries. This application can be used as a starting point for building a more complete and powerful application, as a reference point for building comparable apps, or as a learning experience to better understand how to integrate XRP Ledger functionality into a larger project.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
To complete this tutorial, you should meet the following guidelines:
|
||||
|
||||
1. You have [Node.js](https://nodejs.org/en/download/) v14 or higher installed.
|
||||
2. You have [Yarn](https://yarnpkg.com/en/docs/install) (v1.17.3 or higher) installed.
|
||||
3. You are somewhat familiar with coding with JavaScript and have completed the [Get Started Using JavaScript](get-started-using-javascript.html) tutorial.
|
||||
|
||||
## Source Code
|
||||
|
||||
You can find the complete source code for all of this tutorial's examples in the [code samples section of this website's repository]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/js/).
|
||||
|
||||
## Goals
|
||||
|
||||
At the end of this tutorial, you should be able to build a simple XRP wallet displayed below.
|
||||
|
||||

|
||||
|
||||
This application can:
|
||||
|
||||
- Show updates to the XRP Ledger in real-time.
|
||||
- View any XRP Ledger account's activity, including showing how much XRP was delivered by each transaction.
|
||||
- Show how much XRP is set aside for the account's [reserve requirement](reserves.html).
|
||||
- Send [direct XRP payments](direct-xrp-payments.html), and provide feedback about the intended destination address, including:
|
||||
- Displaying your account's available balance
|
||||
- Verifying that the destination address is valid
|
||||
- Validating the account has enough XRP to send
|
||||
- Allowing you to specify a destination tag
|
||||
|
||||
## Steps
|
||||
|
||||
Before you begin, make sure you have the prerequisites installed. Check your node version by running `node -v`. If necessary, [download Node.js](https://nodejs.org/en/download/).
|
||||
|
||||
**Tip:** If you get stuck while doing this tutorial, or working on another project, feel free to ask for help in the XRPL's [Developer Discord](https://discord.com/invite/KTNmhJDXqa).
|
||||
|
||||
### 1. Set up the project
|
||||
|
||||
1. Navigate to the directory that you want to create the project in.
|
||||
2. Create a new Vite project:
|
||||
|
||||
```bash
|
||||
yarn create vite simple-xrpl-wallet --template vanilla
|
||||
```
|
||||
|
||||
3. Create or modify the file `package.json` to have the following contents:
|
||||
|
||||
{{ include_code("_code-samples/build-a-wallet/js/package.json", language="js") }}
|
||||
|
||||
- Alternatively you can also do `yarn add <package-name>` for each individual package to add them to your `package.json` file.
|
||||
|
||||
4. Install dependencies:
|
||||
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
|
||||
5. Create a new file `.env` in the root directory of the project and add the following variables:
|
||||
|
||||
```bash
|
||||
CLIENT="wss://s.altnet.rippletest.net:51233/"
|
||||
EXPLORER_NETWORK="testnet"
|
||||
SEED="s████████████████████████████"
|
||||
```
|
||||
|
||||
6. Change the seed to your own seed. You can get credentials from [the Testnet faucet](xrp-test-net-faucet.html).
|
||||
|
||||
7. Set up a Vite bundler. Create a file named `vite.config.js` in the root directory of the project and fill it with the following code:
|
||||
|
||||
{{ include_code("_code-samples/build-a-wallet/js/vite.config.js", language="js") }}
|
||||
|
||||
This example includes the necessary configuration to make [xrpl.js work with Vite](https://github.com/XRPLF/xrpl.js/blob/main/UNIQUE_SETUPS.md#using-xrpljs-with-vite-react).
|
||||
|
||||
8. Add script to `package.json`
|
||||
|
||||
In your `package.json` file, add the following section if it's not there already:
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
"dev": "vite"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Create the Home Page (Displaying Account & Ledger Details)
|
||||
|
||||
In this step, we create a home page that displays account and ledger details.
|
||||
|
||||

|
||||
|
||||
1. If not already present, create new files in the root folder named `index.html`, `index.js` and `index.css`.
|
||||
|
||||
2. Make a new folder named `src` in the root directory of the project.
|
||||
|
||||
3. Copy the contents of [index.html]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/js/index.html) in your code.
|
||||
|
||||
4. Add styling to your [index.css]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/js/index.css) file by following the link.
|
||||
|
||||
This basic setup creates a homepage and applies some visual styles. The goal is for the homepage to:
|
||||
|
||||
- Display our account info
|
||||
- Show what's happening on the ledger
|
||||
- And add a little logo for fun
|
||||
|
||||
To make that happen, we need to connect to the XRP Ledger and look up the account and the latest validated ledger.
|
||||
|
||||
5. In the `src/` directory, make a new folder named `helpers`. Create a new file there named `get-wallet-details.js` and define a function named `getWalletDetails` there. This function uses the [account_info method](account_info.html) to fetch account details and the [server_info method](server_info.html) to calculate the current [reserves](reserves.html). The code to do all this is as follows:
|
||||
|
||||
{{ include_code("_code-samples/build-a-wallet/js/src/helpers/get-wallet-details.js", language="js") }}
|
||||
|
||||
6. Now, let's add the code to `index.js` file to fetch the account and ledger details and display them on the home page. Copy the code written below to the `index.js` file. Here we render the wallet details using the function we defined in `get-wallet-details.js`. In order to make sure we have up to date ledger data, we are using the [ledger stream](subscribe.html#ledger-stream) to listen for ledger close events.
|
||||
|
||||
{{ include_code("_code-samples/build-a-wallet/js/index.js", language="js") }}
|
||||
|
||||
7. In the `helpers` folder, add [render-xrpl-logo.js]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/js/src/helpers/render-xrpl-logo.js) to handle displaying a logo.
|
||||
|
||||
8. Finally create a new folder named `assets` in the `src/` directory and add the file [`xrpl.svg`]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/js/src/assets/xrpl.svg) there.
|
||||
|
||||
These files are used to render the XRPL logo for aesthetic purposes.
|
||||
|
||||
The one other thing we do here is add events to two buttons - one to send XRP and one to view transaction history. They won't work just yet — we'll implement them in the next steps.
|
||||
|
||||
Now the application is ready to run. You can start it in dev mode using the following command:
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Your terminal should output a URL which you can use to open your app in a browser. This dev site automatically updates to reflect any changes you make to the code.
|
||||
|
||||
### 3. Create the Send XRP Page
|
||||
|
||||
Now that we've created the home page, we can move on to the "Send XRP" page. This is what allows this wallet to manage your account's funds.
|
||||
|
||||

|
||||
|
||||
1. Create a folder named `send-xrp` in the `src` directory.
|
||||
|
||||
2. Inside the `send-xrp` folder, create two files named `send-xrp.js` and `send-xrp.html`.
|
||||
|
||||
3. Copy the contents of the [send-xrp.html]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/js/src/send-xrp/send-xrp.html) file to your `send-xrp.html` file. The provided HTML code includes three input fields for the destination address, amount, and destination tag, each with their corresponding labels.
|
||||
|
||||
4. Now that we have the HTML code, let's add the JavaScript code. In the `helpers` folder, create a new file named `submit-transaction.js` and copy the code written below to the file. In this file, we are using the [submit](submit.html) method to submit the transaction to the XRPL. Before submitting every transaction needs to be signed by a wallet, learn more about [signing](sign.html) a transaction.
|
||||
|
||||
{{ include_code("_code-samples/build-a-wallet/js/src/helpers/submit-transaction.js", language="js") }}
|
||||
|
||||
5. Now back to the `send-xrp.js` file, copy the code written below to the file. In this piece of code we are first getting all the DOM elements from HTML and adding event listners to update & validate the fields based on the user input. Using `renderAvailableBalance` method we display the current available balance of the wallet. `validateAddress` function validates the user address, and the amount is validated using a regular expression. When all the fields are filled with correct inputs, we call the `submitTransaction` function to submit the transaction to the ledger.
|
||||
|
||||
{{ include_code("_code-samples/build-a-wallet/js/src/send-xrp/send-xrp.js", language="js") }}
|
||||
|
||||
You can now click 'Send XRP' to try creating your own transaction! You can use this example to send XRP to the testnet faucet to try it out.
|
||||
|
||||
Testnet faucet account: `rHbZCHJSGLWVMt8D6AsidnbuULHffBFvEN`
|
||||
|
||||
Amount: 9
|
||||
|
||||
Destination Tag: (Not usually necessary unless you're paying an account tied to an exchange)
|
||||
|
||||

|
||||
|
||||
### 4. Create the Transactions Page
|
||||
|
||||
Now that we have created the home page and the send XRP page, let's create the transactions page that will display the transaction history of the account. In order to see what's happening on the ledger, we're going to display the following fields:
|
||||
|
||||
- Account: The account that sent the transaction.
|
||||
- Destination: The account that received the transaction.
|
||||
- Amount: The amount of XRP sent in the transaction.
|
||||
- Transaction Type: The type of transaction.
|
||||
- Result: The result of the transaction.
|
||||
- Link: A link to the transaction on the XRP Ledger Explorer.
|
||||
|
||||

|
||||
|
||||
1. Create a folder named `transaction-history` in the src directory.
|
||||
2. Create a file named `transaction-history.js` and copy the code written below.
|
||||
|
||||
{{ include_code("_code-samples/build-a-wallet/js/src/transaction-history/transaction-history.js", language="js") }}
|
||||
|
||||
This code uses [account_tx](account_tx.html) to fetch transactions we've sent to and from this account. In order to get all the results, we're using the `marker` parameter to paginate through the incomplete list of transactions until we reach the end.
|
||||
|
||||
3. Create a file named `transaction-history.html` and copy the code from [transaction-history.html]({{target.github_forkurl}}/tree/{{target.github_branch}}/content/_code-samples/build-a-wallet/js/src/transaction-history/transaction-history.html) into it.
|
||||
|
||||
`transaction-history.html` defines a table which displays the fields mentioned above.
|
||||
|
||||
You can use this code as a starting point for displaying your account's transaction history. If you want an additional challenge, try expanding it to support different transaction types (e.g. [TrustSet](trustset.html)). If you want inspiration for how to handle this, you can check out the [XRP Ledger Explorer](https://livenet.xrpl.org/) to see how the transaction details are displayed.
|
||||
|
||||
## Next Steps
|
||||
|
||||
Now that you have a functional wallet, you can take it in several new directions. The following are a few ideas:
|
||||
|
||||
- You could support more of the XRP Ledger's [transaction types](transaction-types.html) including [tokens](issued-currencies.html) and [cross-currency payments](cross-currency-payments.html)
|
||||
- You could add support for displaying multiple tokens, beyond just XRP
|
||||
- You could support creating [offers](offers.html) in the [decentralized exchange](decentralized-exchange.html)
|
||||
- You could add new ways to request payments, such as with QR codes or URIs that open in your wallet.
|
||||
- You could support better account security including allowing users to set [regular key pairs](cryptographic-keys.html#regular-key-pair) or handle [multi-signing](multi-signing.html).
|
||||
- Or you could take your code to production by following the [Building for Production with Vite](https://vitejs.dev/guide/build.html#public-base-path) guide.
|
||||
|
||||
<!--{## common link defs #}-->
|
||||
{% include '_snippets/rippled-api-links.md' %}
|
||||
{% include '_snippets/tx-type-links.md' %}
|
||||
{% include '_snippets/rippled_versions.md' %}
|
||||
@@ -1406,6 +1406,11 @@ pages:
|
||||
- en
|
||||
- ja # TODO: translate this page
|
||||
|
||||
- md: tutorials/build-apps/build-a-browser-wallet-in-js.md
|
||||
targets:
|
||||
- en
|
||||
- ja # TODO: translate this page
|
||||
|
||||
- name: Production Readiness
|
||||
html: production-readiness.html
|
||||
parent: tutorials.html
|
||||
|
||||
BIN
img/js-wallet-home.png
Normal file
BIN
img/js-wallet-home.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 239 KiB |
BIN
img/js-wallet-send-xrp-transaction-details.png
Normal file
BIN
img/js-wallet-send-xrp-transaction-details.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
BIN
img/js-wallet-send-xrp.png
Normal file
BIN
img/js-wallet-send-xrp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 148 KiB |
BIN
img/js-wallet-transaction.png
Normal file
BIN
img/js-wallet-transaction.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 472 KiB |
Reference in New Issue
Block a user