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:
Tushar Pardhe
2023-05-10 00:52:21 +01:00
committed by GitHub
parent 673b0082ac
commit 8c888bd248
24 changed files with 2072 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
CLIENT="wss://s.altnet.rippletest.net/"
EXPLORER_NETWORK="testnet"
SEED="s████████████████████████████"

View 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?

View File

@@ -0,0 +1,7 @@
{
"trailingComma": "es5",
"printWidth": 150,
"tabWidth": 4,
"singleQuote": true,
"semi": true
}

View 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

View 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;
}

View 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>

View 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);
}
})();

View 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"
}
}

View 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

View File

@@ -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;
}
}

View File

@@ -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>
`;
}

View File

@@ -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;
}
}

View File

@@ -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>

View 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';
}
});

View File

@@ -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>

View File

@@ -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();

View 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;

File diff suppressed because it is too large Load Diff

View 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.
![Home Page Screenshot](img/js-wallet-home.png)
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.
![Home Page Screenshot](img/js-wallet-home.png)
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.
![Send XRP Page Screenshot](img/js-wallet-send-xrp.png)
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)
![Send XRP Transaction Screenshot](img/js-wallet-send-xrp-transaction-details.png)
### 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.
![Transactions Page Screenshot](img/js-wallet-transaction.png)
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' %}

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

BIN
img/js-wallet-send-xrp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB