Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate the toml checker to Redocly (https://xrpl.org/xrp-ledger-toml-checker.html) #2274

Merged
merged 27 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
406166b
Add baseline html
JST5000 Nov 22, 2023
2cb162b
Add script tags
JST5000 Nov 22, 2023
92ffdc9
WIP fetchWallet
JST5000 Nov 22, 2023
68a2c60
Make basic page with account step 1 work
JST5000 Nov 22, 2023
d13965b
Add decodeHex and helpers to update logs
JST5000 Nov 23, 2023
997da17
Add fetchFile to access toml from domain
JST5000 Nov 23, 2023
6b626f1
Copy over code & comment out migrated pieces
JST5000 Nov 23, 2023
3e31903
Add toml parsing
JST5000 Nov 28, 2023
ad63c0b
WIP: Add types and uncomment new code
JST5000 Nov 28, 2023
7a9926b
Update the parseToml function to share code
JST5000 Nov 29, 2023
90bad76
Mostly migrate the validateDomain function
JST5000 Nov 30, 2023
0886b85
Fix bug by using for instead of foreach
JST5000 Nov 30, 2023
d17890d
Clean up code part 1
JST5000 Dec 1, 2023
7357a3a
Refactor into separate files
JST5000 Dec 2, 2023
e74ef63
Translate everything
JST5000 Dec 4, 2023
d9fccaa
Componentize the buttons
JST5000 Dec 4, 2023
ce3d1ed
Split out code into separate files
JST5000 Dec 5, 2023
86232cd
Update package-lock
JST5000 Dec 19, 2023
471d356
Fix spacing and uncomment code
JST5000 Dec 19, 2023
163ab79
Fix indentation
JST5000 Dec 19, 2023
599c475
Fix direct import of xrpl
JST5000 Dec 19, 2023
3e2e670
Fix import
JST5000 Dec 19, 2023
5880097
cleaned up log entry handling to not build an array of elements
ckniffen Dec 21, 2023
651c278
moved to resource folder and update css.
ckniffen Dec 21, 2023
81d2718
Move shared components and fix small errors
JST5000 Dec 21, 2023
3201a24
Move file and update sidebars
JST5000 Jan 3, 2024
b07ff34
Fix slow load of long list of addresses
JST5000 Jan 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions content/dev-tools/toml-checker/ListTomlFields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { clsx } from 'clsx'
import { Client } from 'xrpl'

// Decode a hexadecimal string into a regular string, assuming 8-bit characters.
// Not proper unicode decoding, but it'll work for domains which are supposed

import React = require("react");
import { CLASS_GOOD } from "./LogEntry";
import { AccountFields } from "./XrplToml";

// to be a subset of ASCII anyway.
JST5000 marked this conversation as resolved.
Show resolved Hide resolved
function decodeHex(hex) {
let str = '';
for (let i = 0; i < hex.length; i += 2) {
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16))
}
return str
}

function getWsUrlForNetwork(net: string) {
let wsNetworkUrl: string
if (net === "main") {
wsNetworkUrl = 'wss://s1.ripple.com:51233'
} else if (net == "testnet") {
wsNetworkUrl = 'wss://s.altnet.rippletest.net:51233'
} else if (net === "devnet") {
wsNetworkUrl = 'wss://s.devnet.rippletest.net:51233/'
} else if (net === "xahau") {
wsNetworkUrl = 'wss://xahau-test.net:51233'
} else {
wsNetworkUrl = undefined
}
return wsNetworkUrl
}

async function validateAddressDomainOnNet(addressToVerify: string, domain: string, net: string) {
if (!domain) { return undefined } // Can't validate an empty domain value

const wsNetworkUrl = getWsUrlForNetwork(net)
if(!wsNetworkUrl) {
console.error(`The XRPL TOML Checker does not currently support verifying addresses on ${net}.
Please open an issue to add support for this network.`)
return undefined
}
const api = new Client(wsNetworkUrl)
await api.connect()

let accountInfoResponse
try {
accountInfoResponse = await api.request({
"command": "account_info",
"account": addressToVerify
})
} catch(e) {
console.warn(`failed to look up address ${addressToVerify} on ${net} network"`, e)
return undefined
} finally {
await api.disconnect()
}

if (accountInfoResponse.result.account_data.Domain === undefined) {
console.info(`Address ${addressToVerify} has no Domain defined on-ledger`)
return undefined
}

let decodedDomain
try {
decodedDomain = decodeHex(accountInfoResponse.result.account_data.Domain)
} catch(e) {
console.warn("error decoding domain value", accountInfoResponse.result.account_data.Domain, e)
return undefined
}

if(decodedDomain) {
const doesDomainMatch = decodedDomain === domain
if(!doesDomainMatch) {
console.debug(addressToVerify, ": Domain mismatch ("+decodedDomain+" vs. "+domain+")")
}
return doesDomainMatch
} else {
console.debug(addressToVerify, ": Domain is undefined in settings")
return undefined
}
}

/**
* A formatted list item displaying content from a single field of a toml file.
*
* @param props Field info to display
* @returns A formatted list item
*/
function FieldListItem(props: { fieldName: string, fieldValue: string}) {
return (
<li key={props.fieldName}>
<strong>{props.fieldName}: </strong>
<span className={`fieldName`}>
{props.fieldValue}
</span>
</li>)
}

/**
* Get an array of HTML lists that can be used to display toml data.
* If no data exists or none matches the filter it will return an empty array instead.
*
* @param fields An array of objects to parse into bullet points
* @param filter Optional function to filter displayed fields to only ones which return true.
*/
export async function getListEntries(fields: Object[], filter?: Function, domainToVerify?: string) {
const formattedEntries: JSX.Element[] = []
for(let i = 0; i < fields.length; i++) {
const entry = fields[i]
if(!filter || filter(entry)) {

const fieldNames = Object.keys(entry)
const displayedFields: JSX.Element[] = []

fieldNames.forEach((fieldName) => {
if(entry[fieldName] && Array.isArray(entry[fieldName])) {

const internalList = []
entry[fieldName].forEach((value) => {
internalList.push(
<FieldListItem key={value} fieldName={fieldName} fieldValue={value}/>
)
})

displayedFields.push(<ol key={`ol-${displayedFields.length}`}>{internalList}</ol>)

} else {
displayedFields.push(
<FieldListItem key={fieldName} fieldName={fieldName} fieldValue={entry[fieldName]}/>
)
}
})

const key = `entry-${formattedEntries.length}`
if(domainToVerify) {
const accountEntry = entry as AccountFields
if(accountEntry.address) {
const net = accountEntry.network ?? "main"
const domainIsValid = await validateAddressDomainOnNet(accountEntry.address, domainToVerify, net)

if(domainIsValid) {
displayedFields.push(
<li className={CLASS_GOOD} key={`${key}-result`}>DOMAIN VALIDATED <i className="fa fa-check-circle"/></li>
)
}
}
}

formattedEntries.push((<li key={key}>
<ul className={clsx(domainToVerify && 'mb-3')} key={key + "-ul"}>{displayedFields}</ul>
</li>))
}
}
return formattedEntries
}
89 changes: 89 additions & 0 deletions content/dev-tools/toml-checker/LogEntry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import * as React from 'react';
import { useTranslate } from '@portal/hooks';
import { clsx } from 'clsx'

export const CLASS_GOOD = "badge badge-success"
export const CLASS_BAD = "badge badge-danger"

export interface LogEntryStatus {
icon?: {
label: string,
type: "SUCCESS" | "ERROR"
check?: boolean
}
followUpMessage?: JSX.Element
}

export interface LogEntryProps {
message: string
id: string
status?: LogEntryStatus
}

/**
* Add entry to the end of the value that setLogEntries modifies.
*
* @param setLogEntries - A setter to modify a list of LogEntries
* @param entry - Data for a new LogEntry
*/
export function addNewLogEntry(
setLogEntries: React.Dispatch<React.SetStateAction<JSX.Element[]>>,
entry: LogEntryProps)
{
setLogEntries((prev) => {
const updated: JSX.Element[] = [].concat(prev)
const index = updated.length
updated.push(<LogEntry
message={entry.message}
id={entry.id}
key={`log-${index}`} status={entry.status}/>)
return updated
})
}

/**
* Looks up an existing log entry from the previous value within setLogEntries which has
* the same id as entry.id. Then it updates that value to equal entry.
*
* Primarily used to update the "status" after verifying a field.
*
* @param setLogEntries - A setter to modify a list of LogEntries.
* @param entry - Updated data for an existing LogEntry.
*/
export function updateLogEntry(
setLogEntries: React.Dispatch<React.SetStateAction<JSX.Element[]>>,
entry: LogEntryProps) {
setLogEntries((prev) => {
const updated = [].concat(prev ?? [])
const index = updated.findIndex((log) => {
return log?.props?.id && log.props.id === entry.id
})
updated[index] = (<LogEntry
message={entry.message}
id={entry.id}
key={`log-${index}`} status={entry.status}/>)
return updated
})
}

export function LogEntry({
message,
id,
status
}: LogEntryProps)
{
const {translate} = useTranslate()
let icon = undefined
if(!!(status?.icon)) {
icon = <span className={
clsx(status.icon?.type === "SUCCESS" && CLASS_GOOD,
status.icon?.type === "ERROR" && CLASS_BAD)}>
{status.icon?.label}
{status.icon?.check && <i className="fa fa-check-circle"/>}
</span>
}

return (
<li id={id}>{translate(`${message} `)}{icon}{status?.followUpMessage}</li>
)
}
74 changes: 74 additions & 0 deletions content/dev-tools/toml-checker/TextLookupForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as React from 'react';
import { useState } from 'react'
import { useTranslate } from '@portal/hooks';

/**
* A button that allows a single field to be submitted & logs displayed underneath.
*/
export interface TextLookupFormProps {
/**
* The big header above the button.
*/
title: string
/**
* Main description for what the button will do. Usually wrapped in <p> with <a>'s inside.
* All translation must be done before passing in the description.
*/
description: React.JSX.Element,
/**
* 2-3 words that appear on the button itself.
*/
buttonDescription: string
/*
* Triggered when users click the button to submit the form.
* setLogEntries is internally used to display logs to the user as handleSubmit executes.
* fieldValue represents the value they submitted with the form.
*/
handleSubmit: (
setLogEntries: React.Dispatch<React.SetStateAction<JSX.Element[]>>,
event: React.FormEvent<HTMLFormElement>,
fieldValue: string) => void
/**
* Optionally include this as an example in the form to hint to users what they should type in.
*/
formPlaceholder?: string
}

/**
* A form to look up a single text field and display logs to the user.
*
* @param props Text fields for the form / button and a handler when the button is clicked.
* @returns A single-entry form which displays logs after submitting.
*/
export function TextLookupForm(props: TextLookupFormProps) {
const { translate } = useTranslate()

const { title, description, buttonDescription, formPlaceholder, handleSubmit } = props

const [logEntries, setLogEntries] = useState<JSX.Element[]>(undefined)
const [fieldValue, setFieldValue] = useState("")

return (
<div className="p-3 pb-5">
<form id="text-lookup-form" onSubmit={(event) => handleSubmit(setLogEntries, event, fieldValue)}>
<h4>{translate(title)}</h4>
{description}
<div className="input-group">
<input type="text" className="form-control" required
placeholder={translate(formPlaceholder)}
onChange={(event) => setFieldValue(event.target.value)}
/>
<br />
<button className="btn btn-primary form-control">{translate(buttonDescription)}</button>
</div>
</form>
<br/>
<br/>
{logEntries && <div id="result">
<h5 className="result-title">{translate(`Result`)}</h5>
<ul id="log">
{logEntries}
</ul>
</div>}
</div>)
}
Loading