Skip to content

Commit

Permalink
Migrate the faucet page to Redocly (#2243)
Browse files Browse the repository at this point in the history
* Get basic HTML loading for faucet page

* Add xrpl.js implementation

* Add sidebar and fix throbber

* Add translates

* Try to format sidebar

* Fix formatting

* Support xrpl.js

* Fix links

* Comment out XRPLGuard for now

* Make AMM Devnet faucet work

* Improve readability

* Update all instances of link + fix topnav

* Remove unnecessary file

* Use a more current version of xrpl

* Add missing loader while keys are generating

* Type with xrpl and remove unnecessary script

* Use string interpolation instead of multiple trans

* Move faucets into a json file

* Remove the old faucet code

* Use xrpl-beta directly

* Use dropsToXRP

* Support hooks natively

* Remove AMM-Devnet

* Revert changes to link path

* Revert link changes pt 2

* Revert pt 3

* Use XRPLoader for loading icon

* Fix small mistakes

* Remove unnecessary changes
  • Loading branch information
JST5000 authored and mDuo13 committed Dec 14, 2023
1 parent 0417548 commit ff58379
Show file tree
Hide file tree
Showing 9 changed files with 363 additions and 2 deletions.
28 changes: 28 additions & 0 deletions content/dev-tools/faucets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"knownFaucets": [
{
"id": "faucet-select-testnet",
"wsUrl": "wss://s.altnet.rippletest.net:51233/",
"jsonRpcUrl": "https://s.altnet.rippletest.net:51234/",
"faucetUrl": "faucet.altnet.rippletest.net",
"shortName": "Testnet",
"desc": "Mainnet-like network for testing applications."
},
{
"id": "faucet-select-devnet",
"wsUrl": "wss://s.devnet.rippletest.net:51233/",
"jsonRpcUrl": "https://s.devnet.rippletest.net:51234/",
"faucetUrl": "faucet.devnet.rippletest.net",
"shortName": "Devnet",
"desc": "Preview of upcoming amendments."
},
{
"id": "faucet-select-xahau",
"wsUrl": "wss://xahau-test.net/",
"jsonRpcUrl": "https://xahau-test.net/",
"faucetUrl": "xahau-test.net",
"shortName": "Xahau-Testnet",
"desc": "Hooks (L1 smart contracts) enabled Xahau testnet."
}
]
}
203 changes: 203 additions & 0 deletions content/dev-tools/xrp-faucets.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import * as React from 'react';
import { useTranslate } from '@portal/hooks';
import { useState } from 'react';
import { Client, dropsToXrp, Wallet } from 'xrpl';
import * as faucetData from './faucets.json'
import XRPLoader from 'content/static/components/XRPLoader';

interface FaucetInfo {
id: string,
wsUrl: string,
jsonRpcUrl: string,
faucetUrl: string,
shortName: string,
desc: string,
}

async function waitForSequence(client: Client, address: string):
Promise<{ sequence: string, balance: string }>
{
let response;
while (true) {
try {
response = await client.request({
command: "account_info",
account: address,
ledger_index: "validated"
})
break
} catch(e) {
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
console.log(response)

return { sequence: response.result.account_data.Sequence, balance: response.result.account_data.Balance}
}

function FaucetEndpoints({ faucet, givenKey } : { faucet: FaucetInfo, givenKey: string}) {
const { translate } = useTranslate();

return (<div key={givenKey}>
<h4>{translate(`${faucet.shortName} Servers`)}</h4>
<pre>
<code>
// WebSocket<br/>
{faucet.wsUrl}<br/>
<br/>
// JSON-RPC<br/>
{faucet.jsonRpcUrl}
</code>
</pre>
</div>)
}

function FaucetSidebar({ faucets }: { faucets: FaucetInfo[] }): React.JSX.Element {
return (<aside className="right-sidebar col-lg-6 order-lg-4" role="complementary">
{faucets.map(
(faucet) => <FaucetEndpoints faucet={faucet} key={faucet.shortName + " Endpoints"} givenKey={faucet.shortName + " Endpoints"}/>
)}
</aside>)
}

export default function XRPFaucets(): React.JSX.Element {
const { translate } = useTranslate();

const faucets: FaucetInfo[] = faucetData.knownFaucets

const [selectedFaucet, setSelectedFaucet] = useState(faucets[0])

return (
<div className="container-fluid" role="document" id="main_content_wrapper">
<div className="row">
<FaucetSidebar faucets={faucets}/>
<main className="main col-md-7 col-lg-6 order-md-3" role="main" id="main_content_body">
<section className="container-fluid pt-3 p-md-3">
<h1>{translate("XRP Faucets")}</h1>
<div className="content">
<p>{translate("These ")}<a href="parallel-networks.html">{translate("parallel XRP Ledger test networks")}</a> {translate("provide platforms for testing changes to the XRP Ledger and software built on it, without using real funds.")}</p>
<p>{translate("These funds are intended for")} <strong>{translate("testing")}</strong> {translate("only. Test networks' ledger history and balances are reset as necessary. Devnets may be reset without warning.")}</p>
<p>{translate("All balances and XRP on these networks are separate from Mainnet. As a precaution, do not use the Testnet or Devnet credentials on the Mainnet.")}</p>

<h3>{translate("Choose Network:")}</h3>
{ faucets.map((net) => (
<div className="form-check" key={"network-" + net.shortName}>
<input onChange={() => setSelectedFaucet(net)} className="form-check-input" type="radio"
name="faucet-selector" id={net.id} checked={selectedFaucet.shortName == net.shortName} />
<label className="form-check-label" htmlFor={net.id}>
<strong>{translate(net.shortName)}</strong>: {translate(net.desc)}
</label>
</div>
)) }

<br/>
<TestCredentials selectedFaucet={selectedFaucet}/>
</div>
</section>
</main>
</div>
</div>
)
}

async function generateFaucetCredentialsAndUpdateUI(
selectedFaucet: FaucetInfo,
setButtonClicked: React.Dispatch<React.SetStateAction<boolean>>,
setGeneratedCredentialsFaucet: React.Dispatch<React.SetStateAction<string>>,
setAddress: React.Dispatch<React.SetStateAction<string>>,
setSecret: React.Dispatch<React.SetStateAction<string>>,
setBalance: React.Dispatch<React.SetStateAction<string>>,
setSequence: React.Dispatch<React.SetStateAction<string>>): Promise<void> {

setButtonClicked(true)

// Clear existing credentials
setGeneratedCredentialsFaucet(selectedFaucet.shortName)
setAddress("")
setSecret("")
setBalance("")
setSequence("")
const { translate } = useTranslate();


const wallet = Wallet.generate()

const client = new Client(selectedFaucet.wsUrl)
await client.connect()

try {
setAddress(wallet.address)
setSecret(wallet.seed)

await client.fundWallet(wallet, { faucetHost: selectedFaucet.faucetUrl, usageContext: "xrpl.org-faucet" })

const response = await waitForSequence(client, wallet.address)

setSequence(response.sequence)
setBalance(response.balance)

} catch (e) {
alert(translate(`There was an error with the ${selectedFaucet.shortName} faucet. Please try again.`))
}
setButtonClicked(false)
}

function TestCredentials({selectedFaucet}) {
const { translate } = useTranslate();

const [generatedCredentialsFaucet, setGeneratedCredentialsFaucet] = useState("")
const [address, setAddress] = useState("")
const [secret, setSecret] = useState("")
const [balance, setBalance] = useState("")
const [sequence, setSequence] = useState("")
const [buttonClicked, setButtonClicked] = useState(false)

return (<div>
{/* <XRPLGuard> TODO: Re-add this once we find a good way to avoid browser/server mismatch errors */}
<div className="btn-toolbar" role="toolbar" aria-label="Button">
<button id="generate-creds-button" onClick={
() => generateFaucetCredentialsAndUpdateUI(
selectedFaucet,
setButtonClicked,
setGeneratedCredentialsFaucet,
setAddress,
setSecret,
setBalance,
setSequence)
} className="btn btn-primary mr-2 mb-2">
{translate(`Generate ${selectedFaucet.shortName} credentials`)}
</button>
</div>
{/* </XRPLGuard> */}


{generatedCredentialsFaucet && <div id="your-credentials">
<h2>{translate(`Your ${generatedCredentialsFaucet} Credentials`)}</h2>
</div>}

{(buttonClicked && address === "") && <XRPLoader message={translate("Generating keys..")}/>}

{address && <div id="address"><h3>{translate("Address")}</h3>{address}</div>}

{secret && <div id="secret"><h3>{translate("Secret")}</h3>{secret}</div>}

{(address && !balance) && (<div>
<br/>
<XRPLoader message={translate("Funding account...")}/>
</div>)}

{balance && <div id="balance">
<h3>{translate("Balance")}</h3>
{dropsToXrp(balance).toLocaleString("en")} {translate("XRP")}
</div>}

{sequence && <div id="sequence">
<h3>{translate("Sequence Number")}</h3>
{sequence}
</div>}

{(secret && !sequence) && <XRPLoader message={translate("Waiting...")}/>}

</div>
)
}
2 changes: 2 additions & 0 deletions content/sidebars.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,8 @@
- label: xrp-ledger.toml Checker
- label: Domain Verification Checker
- label: XRP Faucets
href: /dev-tools/xrp-faucets
page: /dev-tools/xrp-faucets.page.tsx
- label: Transaction Sender
- label: XRPL Learning Portal
href: https://learn.xrpl.org/
Expand Down
13 changes: 13 additions & 0 deletions content/static/components/XRPLoader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as React from 'react';

export interface XRPLoaderProps {
message?: string
}

export default function XRPLoader(props: XRPLoaderProps) {
return (
<div id="loader" style={{ display: "inline" }}>
<img alt="(loading)" className="throbber" src="/img/xrp-loader-96.png" />
{props.message}
</div>);
}
27 changes: 27 additions & 0 deletions content/static/js/type-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
type TypeofType =
| 'bigint'
| 'boolean'
| 'function'
| 'number'
| 'object'
| 'string'
| 'undefined'

type TypeCheckFn = (thing: unknown) => boolean


/**
* Curried function for creating typeof checker functions.
* @param {string} type The type to check against (eg 'string', 'number')
* @param {function} [secondaryTest] Optional additional test function to run in cases where a type match isn't always a sure indicator.
* @returns {boolean} Whether the value matches the type
*/
const isTypeof =
<T>(type: TypeofType, secondaryTest?: TypeCheckFn) =>
(thing: unknown): thing is T => {
const matches = typeof thing === type
if (matches && secondaryTest) return secondaryTest(thing)
return matches
}

export const isFunction = isTypeof<(...args: unknown[]) => unknown>('function')
85 changes: 85 additions & 0 deletions content/static/js/xrpl-guard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useTranslate } from '@portal/hooks';
import { isFunction } from './type-helpers'
import { FC } from 'react'
import { useEffect, useState } from 'react'
import React = require('react');
import XRPLoader from '../components/XRPLoader';

export const MIN_LOADER_MS = 1250
export const DEFAULT_TIMEOUT = 1000

const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay))

/**
* Evaluate a check function will eventually resolve to `true`
*
* If check is initially true, immediatly return `isTrue`
* If check is initially false and becomes true, return true after `timeoutMs`
*/
export const useThrottledCheck = (
check: () => boolean,
timeoutMs = DEFAULT_TIMEOUT,
) => {
const [isTrue, setIsTrue] = useState(() => check())

useEffect(() => {
const doCheck = async (tries = 0) => {
const waitMs = 250,
waitedMs = tries * waitMs

if (check()) {
const debouncedDelay =
waitedMs < timeoutMs ? timeoutMs - (waitedMs % timeoutMs) : 0

setTimeout(() => setIsTrue(true), debouncedDelay)
return
}

await sleep(waitMs)

doCheck(tries + 1)
}

if (!isTrue) {
doCheck()
}
}, [check, isTrue])

return isTrue
}

/**
* Show a loading spinner if XRPL isn't loaded yet by
* waiting at least MIN_LOADER_MS before rendering children
* in order to make the visual loading transition smooth
*
* e.g. if xrpl loads after 500ms, wait
* another MIN_LOADER_MS - 500ms before rendering children
*
* @param {function} testCheck for testing only, a check function to use
*/
export const XRPLGuard: FC<{ testCheck?: () => boolean, children }> = ({
testCheck,
children,
}) => {

const { translate } = useTranslate();
const isXRPLLoaded = useThrottledCheck(
// @ts-expect-error - xrpl is added via a script tag (TODO: Directly import when xrpl.js 3.0 is released)
testCheck ?? (() => typeof xrpl === 'object'),
MIN_LOADER_MS,
)

return (
<>
{isXRPLLoaded ? (
isFunction(children) ? (
children()
) : (
children
)
) : <XRPLoader message={translate("Loading...")}/>
}
</>
)
}
3 changes: 2 additions & 1 deletion content/top-nav.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@
- label: Send XRP
href: /tutorials/send-xrp/
- label: XRP Faucets
href: /docs/dev-tools/xrp-faucets/
href: /dev-tools/xrp-faucets
page: /dev-tools/xrp-faucets.page.tsx
- label: XRPL Servers
href: /infrastructure/xrpl-servers/
- label: Dev Tools
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"dependencies": {
"@redocly/portal": "^0.66.0",
"lottie-react": "^2.4.0",
"moment": "^2.29.4"
"moment": "^2.29.4",
"xrpl": "^3.0.0-beta.1"
},
"overrides": {
"react": "^17.0.2"
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"compilerOptions": {
"baseUrl": ".",
"jsx": "react",
"resolveJsonModule": true,
"paths": {
"@theme/*": [
"./@theme/*",
Expand Down

0 comments on commit ff58379

Please sign in to comment.