Move browser wallet code samples

This commit is contained in:
mDuo13
2023-09-22 18:20:41 -07:00
parent 2405bffcde
commit 7b4dc9bd6e
20 changed files with 28 additions and 19 deletions

View File

@@ -0,0 +1,3 @@
CLIENT="wss://s.altnet.rippletest.net:51233/"
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.5"
},
"dependencies": {
"dotenv": "^16.0.3",
"xrpl": "^2.11.0"
}
}

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,172 @@
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 Delivered</th>
<th>Transaction Type</th>
<th>Result</th>
<th>Link</th>
`;
txHistoryElement.appendChild(header);
// Converts the hex value to a string
function getTokenName(currencyCode) {
if (!currencyCode) return "";
if (currencyCode.length === 3 && currencyCode.trim().toLowerCase() !== 'xrp') {
// "Standard" currency code
return currencyCode.trim();
}
if (currencyCode.match(/^[a-fA-F0-9]{40}$/)) {
// Hexadecimal currency code
const text_code = convertHexToString(value).replaceAll('\u0000', '')
if (text_code.match(/[a-zA-Z0-9]{3,}/) && text_code.trim().toLowerCase() !== 'xrp') {
// ASCII or UTF-8 encoded alphanumeric code, 3+ characters long
return text_code;
}
// Other hex format, return as-is.
// For parsing other rare formats, see https://github.com/XRPLF/xrpl-dev-portal/blob/master/content/_code-samples/normalize-currency-codes/js/normalize-currency-code.js
return currencyCode;
}
return "";
}
function renderAmount(delivered) {
if (delivered === 'unavailable') {
// special case for pre-2014 partial payments
return 'unavailable';
} else if (typeof delivered === 'string') {
// It's an XRP amount in drops. Convert to decimal.
return `${dropsToXrp(delivered)} XRP`;
} else if (typeof delivered === 'object') {
// It's a token amount.
return `${delivered.value} ${getTokenName(delivered.currency)}.${delivered.issuer}`;
} else {
// Could be undefined -- not all transactions deliver value
return "-"
}
}
// 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,
Hash: tx.hash,
TransactionType: tx.TransactionType,
result: meta?.TransactionResult,
delivered: meta?.delivered_amount
};
});
// 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>` : '-'}
${renderAmount(value.delivered)}
${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