mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2025-11-21 12:15:50 +00:00
Migrate toml checker tool
Add baseline html Add script tags WIP fetchWallet Make basic page with account step 1 work Add decodeHex and helpers to update logs Add fetchFile to access toml from domain Copy over code & comment out migrated pieces Add toml parsing WIP: Add types and uncomment new code Update the parseToml function to share code Mostly migrate the validateDomain function Fix bug by using for instead of foreach Clean up code part 1 Refactor into separate files Translate everything Componentize the buttons Split out code into separate files Update package-lock Fix spacing and uncomment code Fix indentation Fix direct import of xrpl Fix import cleaned up log entry handling to not build an array of elements moved to resource folder and update css. Move shared components and fix small errors Move file and update sidebars Fix slow load of long list of addresses toml checker - sidebar/width fixes
This commit is contained in:
77
content/resources/dev-tools/components/LogEntry.tsx
Normal file
77
content/resources/dev-tools/components/LogEntry.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
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 LogEntryItem {
|
||||
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<LogEntryItem[]>>,
|
||||
entry: LogEntryItem)
|
||||
{
|
||||
setLogEntries((prev) => {
|
||||
return [...prev, entry]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 entryToUpdate - Updated data for an existing LogEntry.
|
||||
*/
|
||||
export function updateLogEntry(
|
||||
setLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,
|
||||
entryToUpdate: LogEntryItem) {
|
||||
setLogEntries((prev) => {
|
||||
const index = prev.findIndex((entry)=> entryToUpdate.id === entry.id)
|
||||
prev.splice(index, 1, entryToUpdate)
|
||||
return [...prev]
|
||||
})
|
||||
}
|
||||
|
||||
export function LogEntry({
|
||||
message,
|
||||
id,
|
||||
status
|
||||
}: LogEntryItem)
|
||||
{
|
||||
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>
|
||||
)
|
||||
}
|
||||
77
content/resources/dev-tools/components/TextLookupForm.tsx
Normal file
77
content/resources/dev-tools/components/TextLookupForm.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react'
|
||||
import { useTranslate } from '@portal/hooks';
|
||||
import { LogEntry, LogEntryItem } from './LogEntry';
|
||||
|
||||
/**
|
||||
* 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<LogEntryItem[]>>,
|
||||
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<LogEntryItem[]>([])
|
||||
const [fieldValue, setFieldValue] = useState("")
|
||||
|
||||
return (
|
||||
<div className="p-3 pb-5">
|
||||
<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?.length > 0 && <div>
|
||||
<h5 className="result-title">{translate(`Result`)}</h5>
|
||||
<ul id="log">
|
||||
{logEntries.map((log) => {
|
||||
return <LogEntry message={log.message} id={log.id} key={log.id} status={log.status} />
|
||||
})}
|
||||
</ul>
|
||||
</div>}
|
||||
</div>)
|
||||
}
|
||||
163
content/resources/dev-tools/toml-checker/ListTomlFields.tsx
Normal file
163
content/resources/dev-tools/toml-checker/ListTomlFields.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { clsx } from 'clsx'
|
||||
import { Client } from 'xrpl'
|
||||
import React = require("react");
|
||||
import { CLASS_GOOD } from "../components/LogEntry";
|
||||
import { AccountFields } from "./XrplToml";
|
||||
|
||||
// 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
|
||||
// to be a subset of ASCII anyway.
|
||||
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}`
|
||||
const promises = []
|
||||
if(domainToVerify) {
|
||||
const accountEntry = entry as AccountFields
|
||||
if(accountEntry.address) {
|
||||
const net = accountEntry.network ?? "main"
|
||||
const domainIsValid = validateAddressDomainOnNet(accountEntry.address, domainToVerify, net)
|
||||
|
||||
domainIsValid.then((wasValidated) => {
|
||||
if(wasValidated) {
|
||||
displayedFields.push(
|
||||
<li className={CLASS_GOOD} key={`${key}-result`}>DOMAIN VALIDATED <i className="fa fa-check-circle"/></li>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
promises.push(domainIsValid)
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
formattedEntries.push((<li key={key}>
|
||||
<ul className={clsx(domainToVerify && 'mb-3')} key={key + "-ul"}>{displayedFields}</ul>
|
||||
</li>))
|
||||
}
|
||||
}
|
||||
return formattedEntries
|
||||
}
|
||||
427
content/resources/dev-tools/toml-checker/ValidateTomlSteps.tsx
Normal file
427
content/resources/dev-tools/toml-checker/ValidateTomlSteps.tsx
Normal file
@@ -0,0 +1,427 @@
|
||||
import * as React from 'react';
|
||||
import { useTranslate } from '@portal/hooks';
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { parse } from "smol-toml";
|
||||
import { getListEntries } from "./ListTomlFields";
|
||||
import { addNewLogEntry, updateLogEntry, LogEntryItem, LogEntryStatus } from "../components/LogEntry";
|
||||
import { MetadataField, XrplToml, AccountFields, TOML_PATH } from "./XrplToml";
|
||||
|
||||
/**
|
||||
* Helper to log a list of fields from a toml file or display a relevant error message.
|
||||
* Will return true if successfully displays at least one field from fields without erroring.
|
||||
*
|
||||
* @param setLogEntries A setter to update the logs with the new fields.
|
||||
* @param headerText The initial message to include as a header for the list.
|
||||
* @param fields A set of fields to parse and display. May be undefined, but if so,
|
||||
* this function will simply return false. Simplifies typing.
|
||||
* @param domainToVerify The domain to check
|
||||
* @param filterDisplayedFieldsTo Limits the displayed fields to ones which match the predicate.
|
||||
* @returns True if displayed any fields (after applying any given filters)
|
||||
*/
|
||||
async function validateAndDisplayFields(
|
||||
setLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,
|
||||
headerText: string,
|
||||
fields?: Object[],
|
||||
domainToVerify?: string,
|
||||
filterDisplayedFieldsTo?: Function): Promise<boolean> {
|
||||
|
||||
const { translate } = useTranslate()
|
||||
|
||||
// If there's no data, do nothing
|
||||
if(!fields) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Otherwise display all relevant data in the toml file for these field
|
||||
if(Array.isArray(fields)) {
|
||||
let icon = undefined;
|
||||
const formattedEntries = await getListEntries(fields, filterDisplayedFieldsTo, domainToVerify)
|
||||
const relevantTomlFieldsExist = formattedEntries.length > 0
|
||||
if(relevantTomlFieldsExist) {
|
||||
addNewLogEntry(setLogEntries,
|
||||
{
|
||||
message: headerText,
|
||||
id: headerText,
|
||||
status: {
|
||||
followUpMessage: (
|
||||
<ol>
|
||||
{formattedEntries}
|
||||
</ol>
|
||||
),
|
||||
icon: icon
|
||||
}
|
||||
})
|
||||
}
|
||||
return relevantTomlFieldsExist
|
||||
} else {
|
||||
// Invalid toml data
|
||||
addNewLogEntry(setLogEntries, {
|
||||
message: headerText,
|
||||
id: headerText,
|
||||
status: {
|
||||
icon: {
|
||||
label: translate("WRONG TYPE - SHOULD BE TABLE-ARRAY"),
|
||||
type: "ERROR",
|
||||
}
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a metadata field on a toml file is valid, then display logs with the results.
|
||||
*
|
||||
* @param setLogEntries - A setter to update the logs
|
||||
* @param metadata - Metadata from a toml file being verified
|
||||
*/
|
||||
function validateAndDisplayMetadata(
|
||||
setLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,
|
||||
metadata?: MetadataField) {
|
||||
|
||||
const { translate } = useTranslate()
|
||||
|
||||
if (metadata) {
|
||||
const metadataId = 'metadata-log'
|
||||
const metadataLogEntry = {
|
||||
message: translate("Metadata section: "),
|
||||
id: metadataId
|
||||
}
|
||||
addNewLogEntry(setLogEntries, metadataLogEntry)
|
||||
|
||||
// Uniquely checks if array, instead of if not array
|
||||
if (Array.isArray(metadata)) {
|
||||
updateLogEntry(setLogEntries, {...metadataLogEntry, status: {
|
||||
icon: {
|
||||
label: translate("WRONG TYPE - SHOULD BE TABLE"),
|
||||
type: "ERROR",
|
||||
},
|
||||
}})
|
||||
} else {
|
||||
updateLogEntry(setLogEntries, {...metadataLogEntry, status: {
|
||||
icon: {
|
||||
label: translate("FOUND"),
|
||||
type: "SUCCESS",
|
||||
},
|
||||
}})
|
||||
|
||||
if (metadata.modified) {
|
||||
const modifiedLogId = 'modified-date-log'
|
||||
const modifiedLogEntry = {
|
||||
message: translate("Modified date: "),
|
||||
id: modifiedLogId
|
||||
}
|
||||
addNewLogEntry(setLogEntries, modifiedLogEntry)
|
||||
try {
|
||||
updateLogEntry(setLogEntries, { ...modifiedLogEntry, status: {
|
||||
icon: {
|
||||
label: metadata.modified.toISOString(),
|
||||
type: "SUCCESS",
|
||||
},
|
||||
}})
|
||||
} catch(e) {
|
||||
updateLogEntry(setLogEntries, { ...modifiedLogEntry, status: {
|
||||
icon: {
|
||||
label: translate("INVALID"),
|
||||
type: "ERROR",
|
||||
},
|
||||
}})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read in a toml file and verify it has the proper fields, then display those fields in the logs.
|
||||
* This is the 3rd step for verifying a wallet, and the 2nd step for verifying a toml file itself.
|
||||
*
|
||||
* @param setLogEntries A setter to update the logs.
|
||||
* @param tomlData Toml data to parse.
|
||||
* @param addressToVerify The address we're actively looking to verify matches with this toml file.
|
||||
* @param domain A website to look up further information about the toml file.
|
||||
* @returns Nothing.
|
||||
*/
|
||||
async function parseXRPLToml(
|
||||
setLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,
|
||||
tomlData,
|
||||
addressToVerify?: string,
|
||||
domain?: string) {
|
||||
const { translate } = useTranslate()
|
||||
|
||||
const parsingTomlLogEntry: LogEntryItem = {
|
||||
message: translate("Parsing TOML data..."),
|
||||
id: 'parsing-toml-data-log',
|
||||
}
|
||||
addNewLogEntry(setLogEntries, parsingTomlLogEntry)
|
||||
|
||||
let parsed: XrplToml
|
||||
try {
|
||||
parsed = parse(tomlData)
|
||||
updateLogEntry(setLogEntries, {...parsingTomlLogEntry, status: {
|
||||
icon: {
|
||||
label: translate("SUCCESS"),
|
||||
type: "SUCCESS",
|
||||
},
|
||||
}})
|
||||
} catch(e) {
|
||||
updateLogEntry(setLogEntries, {...parsingTomlLogEntry, status: {
|
||||
icon: {
|
||||
label: e,
|
||||
type: "ERROR",
|
||||
},
|
||||
}})
|
||||
return
|
||||
}
|
||||
|
||||
validateAndDisplayMetadata(setLogEntries, parsed.METADATA)
|
||||
|
||||
const accountHeader = translate("Accounts:")
|
||||
if(addressToVerify) {
|
||||
const filterToSpecificAccount = (entry: AccountFields) => entry.address === addressToVerify
|
||||
const accountFound = await validateAndDisplayFields(
|
||||
setLogEntries,
|
||||
accountHeader,
|
||||
parsed.ACCOUNTS,
|
||||
undefined,
|
||||
filterToSpecificAccount)
|
||||
|
||||
const statusLogId = 'account-found-status-log'
|
||||
if(accountFound) {
|
||||
// Then share whether the domain / account pair as a whole has been validated
|
||||
addNewLogEntry(setLogEntries, {
|
||||
message: translate('Account has been found in TOML file and validated.'),
|
||||
id: statusLogId,
|
||||
status: {
|
||||
icon: {
|
||||
label: translate("DOMAIN VALIDATED"),
|
||||
type: "SUCCESS",
|
||||
check: true,
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// We failed to find any entries which match the account we're looking for
|
||||
addNewLogEntry(setLogEntries, {
|
||||
message: translate("Account:"),
|
||||
id: 'toml-account-entry-log',
|
||||
status: {
|
||||
icon: {
|
||||
label: translate("NOT FOUND"),
|
||||
type: "ERROR"
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
addNewLogEntry(setLogEntries, {
|
||||
message: translate("Account not found in TOML file. Domain can not be verified."),
|
||||
id: statusLogId,
|
||||
status: {
|
||||
icon: {
|
||||
label: translate("VALIDATION FAILED"),
|
||||
type: "ERROR",
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// The final validation message is displayed under the validated account since in this case we're
|
||||
// verifying a wallet address, not the toml file itself.
|
||||
await validateAndDisplayFields(setLogEntries, translate(accountHeader), parsed.ACCOUNTS, domain)
|
||||
|
||||
// We then display the rest of the toml as additional information
|
||||
await validateAndDisplayFields(setLogEntries, translate("Validators:"), parsed.VALIDATORS)
|
||||
await validateAndDisplayFields(setLogEntries, translate("Principals:"), parsed.PRINCIPALS)
|
||||
await validateAndDisplayFields(setLogEntries, translate("Servers:"), parsed.SERVERS)
|
||||
await validateAndDisplayFields(setLogEntries, translate("Currencies:"), parsed.CURRENCIES)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert HTML error odes to status messages to display.
|
||||
*
|
||||
* @param status - HTML Error code
|
||||
* @returns A human readable explanation for the HTML based on error code categories.
|
||||
*/
|
||||
function getHttpErrorCode(status?: number) {
|
||||
let errCode;
|
||||
if(status === 408) {
|
||||
errCode = 'TIMEOUT'
|
||||
} else if(status >= 400 && status < 500) {
|
||||
errCode = 'CLIENT ERROR'
|
||||
} else if (status >= 500 && status < 600) {
|
||||
errCode = 'SERVER ERROR'
|
||||
} else {
|
||||
errCode = 'UNKNOWN'
|
||||
}
|
||||
return errCode
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract and parse a toml file from a url derived via domain. If accountToVerify is
|
||||
* passed in, this specifically verifies that address is in the toml file.
|
||||
* For verifying a wallet, this is the 2nd step. For verifying a toml file itself, this is the 1st step.
|
||||
*
|
||||
* @param setLogEntries - A setter to update the log files.
|
||||
* @param domain = The main section of a url - ex. validator.xrpl-labs.com
|
||||
* @param accountToVerify - A wallet to optionally specifically check for.
|
||||
*/
|
||||
export async function fetchFile(
|
||||
setLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,
|
||||
domain: string,
|
||||
accountToVerify?: string) {
|
||||
|
||||
const { translate } = useTranslate()
|
||||
|
||||
const url = "https://" + domain + TOML_PATH
|
||||
const checkUrlId = `check-url-log`
|
||||
const logEntry = {
|
||||
message: translate(`Checking ${url} ...`),
|
||||
id: checkUrlId,
|
||||
}
|
||||
addNewLogEntry(setLogEntries, logEntry)
|
||||
|
||||
try {
|
||||
const response = await axios.get(url)
|
||||
const data: string = response.data
|
||||
updateLogEntry(setLogEntries, {...logEntry, status: {
|
||||
icon: {
|
||||
label: translate("FOUND"),
|
||||
type: "SUCCESS",
|
||||
},
|
||||
}})
|
||||
|
||||
// Continue to the next step of verification
|
||||
parseXRPLToml(setLogEntries, data, accountToVerify, domain)
|
||||
|
||||
} catch (e) {
|
||||
const errorUpdate: LogEntryItem = {...logEntry, status: {
|
||||
icon: {
|
||||
label: translate(getHttpErrorCode((e as AxiosError)?.status)),
|
||||
type: "ERROR",
|
||||
},
|
||||
followUpMessage: (<p>
|
||||
{translate("Check if the file is actually hosted at the URL above, ")
|
||||
+ translate("check your server's HTTPS settings and certificate, and make sure your server provides the required ")}
|
||||
<a href="xrp-ledger-toml.html#cors-setup">{translate("CORS header.")}</a>
|
||||
</p>)
|
||||
}}
|
||||
updateLogEntry(setLogEntries, errorUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to display the result of trying to decode the domain decoding.
|
||||
*
|
||||
* @param setAccountLogEntries - A setter to update the displayed logs.
|
||||
*/
|
||||
function displayDecodedWalletLog(
|
||||
setAccountLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,) {
|
||||
|
||||
const { translate } = useTranslate()
|
||||
|
||||
const logId = 'decoding-domain-hex'
|
||||
addNewLogEntry(setAccountLogEntries, {
|
||||
message: translate('Decoding domain hex'),
|
||||
id: logId,
|
||||
status: {
|
||||
icon: {
|
||||
label: translate('SUCCESS'),
|
||||
type: 'SUCCESS',
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode ascii hex into a string.
|
||||
*
|
||||
* @param hex - a hex string encoded in ascii.
|
||||
* @returns The decoded string
|
||||
*/
|
||||
function decodeHexWallet(hex: string): string {
|
||||
let decodedDomain = '';
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
decodedDomain += String.fromCharCode(parseInt(hex.substring(i, i + 2), 16))
|
||||
}
|
||||
return decodedDomain
|
||||
}
|
||||
|
||||
/**
|
||||
* The first step to verify an XRPL Wallet is verified with a toml file.
|
||||
* Looks up the domain associated with the given accountToVerify and the status on success or failure.
|
||||
*
|
||||
* @param accountToVerify
|
||||
* @param setAccountLogEntries
|
||||
* @param socket
|
||||
*/
|
||||
export function fetchWallet(
|
||||
setAccountLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,
|
||||
accountToVerify: string,
|
||||
socket?: WebSocket)
|
||||
{
|
||||
const {translate} = useTranslate()
|
||||
|
||||
// Reset the logs
|
||||
setAccountLogEntries([])
|
||||
|
||||
const walletLogEntry = {
|
||||
message: translate(`Checking domain of account`),
|
||||
id: 'check-domain-account',
|
||||
}
|
||||
addNewLogEntry(setAccountLogEntries, walletLogEntry)
|
||||
|
||||
const url = "wss://xrplcluster.com"
|
||||
if (typeof socket !== "undefined" && socket.readyState < 2) {
|
||||
socket.close()
|
||||
}
|
||||
|
||||
const data = {
|
||||
"command": "account_info",
|
||||
"account": accountToVerify,
|
||||
}
|
||||
socket = new WebSocket(url)
|
||||
socket.addEventListener('message', (event) => {
|
||||
let data;
|
||||
// Defaults to error to simplify logic later on
|
||||
let response: LogEntryStatus = {
|
||||
icon: {
|
||||
label: translate(`ERROR`),
|
||||
type: `ERROR`,
|
||||
},
|
||||
};
|
||||
try {
|
||||
data = JSON.parse(event.data)
|
||||
if (data.status === 'success') {
|
||||
if (data.result.account_data.Domain) {
|
||||
try {
|
||||
response = {
|
||||
icon: {
|
||||
label: translate('SUCCESS'),
|
||||
type: 'SUCCESS',
|
||||
},
|
||||
}
|
||||
// Continue to the next step of validation
|
||||
const decodedDomain = decodeHexWallet(data.result.account_data.Domain)
|
||||
displayDecodedWalletLog(setAccountLogEntries)
|
||||
fetchFile(setAccountLogEntries, decodedDomain, accountToVerify)
|
||||
} catch(e) {
|
||||
console.log(e)
|
||||
response.followUpMessage = <p>{translate(`Error decoding domain field: ${data.result.account_data.Domain}`)}</p>
|
||||
}
|
||||
} else {
|
||||
response.followUpMessage = <p>{translate("Make sure the account has the Domain field set.")}</p>
|
||||
}
|
||||
} else {
|
||||
response.followUpMessage = <p>{translate("Make sure you are entering a valid XRP Ledger address.")}</p>
|
||||
}
|
||||
updateLogEntry(setAccountLogEntries, { ...walletLogEntry, status: response })
|
||||
} catch {
|
||||
socket.close()
|
||||
return false
|
||||
}
|
||||
})
|
||||
socket.addEventListener('open', () => {
|
||||
socket.send(JSON.stringify(data))
|
||||
})
|
||||
}
|
||||
49
content/resources/dev-tools/toml-checker/XrplToml.tsx
Normal file
49
content/resources/dev-tools/toml-checker/XrplToml.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
export const TOML_PATH = "/.well-known/xrp-ledger.toml"
|
||||
|
||||
export interface AccountFields {
|
||||
address: string,
|
||||
network: string,
|
||||
desc: string
|
||||
}
|
||||
|
||||
export interface ValidatorFields {
|
||||
public_key: string,
|
||||
network: string,
|
||||
owner_country: string,
|
||||
server_country: string,
|
||||
unl: string
|
||||
}
|
||||
|
||||
export interface PrincipalFields {
|
||||
name: string,
|
||||
email: string,
|
||||
}
|
||||
|
||||
export interface ServerFields {
|
||||
json_rpc: string,
|
||||
ws: string,
|
||||
peer: string,
|
||||
network: string,
|
||||
}
|
||||
|
||||
export interface CurrencyFields {
|
||||
code: string,
|
||||
display_decimals: string,
|
||||
issuer: string,
|
||||
network: string,
|
||||
symbol: string
|
||||
}
|
||||
|
||||
export interface MetadataField {
|
||||
// TODO: There could be other fields here, but this is all the existing code used
|
||||
modified: Date
|
||||
}
|
||||
|
||||
export interface XrplToml {
|
||||
ACCOUNTS?: AccountFields[],
|
||||
VALIDATORS?: ValidatorFields[],
|
||||
PRINCIPALS?: PrincipalFields[],
|
||||
SERVERS?: ServerFields[],
|
||||
CURRENCIES?: CurrencyFields[],
|
||||
METADATA?: MetadataField
|
||||
}
|
||||
79
content/resources/dev-tools/xrp-ledger-toml-checker.page.tsx
Normal file
79
content/resources/dev-tools/xrp-ledger-toml-checker.page.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import * as React from 'react';
|
||||
import { useTranslate } from '@portal/hooks';
|
||||
import { TextLookupForm, type TextLookupFormProps } from './components/TextLookupForm';
|
||||
import { fetchFile, fetchWallet } from './toml-checker/ValidateTomlSteps';
|
||||
import { LogEntryItem } from './components/LogEntry';
|
||||
/**
|
||||
* Example data to test the tool with
|
||||
*
|
||||
* Domains:
|
||||
* - Valid: validator.xrpl-labs.com
|
||||
* - Not valid: sologenic.com
|
||||
*
|
||||
* Addresses:
|
||||
* - Valid: rSTAYKxF2K77ZLZ8GoAwTqPGaphAqMyXV
|
||||
* - No toml: rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz
|
||||
* - No domain: rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh
|
||||
*/
|
||||
|
||||
function handleSubmitWallet(
|
||||
setAccountLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,
|
||||
event: React.FormEvent<HTMLFormElement>,
|
||||
addressToVerify: string) {
|
||||
|
||||
event.preventDefault()
|
||||
setAccountLogEntries([])
|
||||
fetchWallet(setAccountLogEntries, addressToVerify)
|
||||
}
|
||||
|
||||
function handleSubmitDomain(
|
||||
setDomainLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,
|
||||
event: React.FormEvent<HTMLFormElement>,
|
||||
domainAddress: string) {
|
||||
|
||||
event.preventDefault();
|
||||
setDomainLogEntries([])
|
||||
fetchFile(setDomainLogEntries, domainAddress)
|
||||
}
|
||||
|
||||
export default function TomlChecker() {
|
||||
const { translate } = useTranslate();
|
||||
|
||||
const domainButtonProps: TextLookupFormProps = {
|
||||
title: `Look Up By Domain`,
|
||||
description: <p>{translate(`This tool allows you to verify that your `)}<code>{translate(`xrp-ledger.toml`)}</code>
|
||||
{translate(` file is syntactically correct and deployed properly.`)}</p>,
|
||||
buttonDescription: `Check toml file`,
|
||||
formPlaceholder: "example.com (Domain name to check)",
|
||||
handleSubmit: handleSubmitDomain,
|
||||
}
|
||||
|
||||
const addressButtonProps: TextLookupFormProps = {
|
||||
title: `Look Up By Account`,
|
||||
description: <p>{translate(`Enter an XRP Ledger address to see if that account is claimed by the domain it says owns it.`)}</p>,
|
||||
buttonDescription: `Check account`,
|
||||
formPlaceholder: `r... (${translate("Wallet Address to check")})`,
|
||||
handleSubmit: handleSubmitWallet
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="toml-checker row">
|
||||
{/* This aside is empty but it keeps the formatting similar to other pages */}
|
||||
<aside className="right-sidebar col-lg-3 order-lg-4" role="complementary"/>
|
||||
|
||||
<main className="main col-lg-9" role="main" id="main_content_body">
|
||||
<section className="container-fluid">
|
||||
<div className="p-3">
|
||||
<h1>{translate(`xrp-ledger.toml Checker`)}</h1>
|
||||
<p>{translate(`If you run an XRP Ledger validator or use the XRP Ledger for your business,
|
||||
you can provide information about your usage of the XRP Ledger to the world in a machine-readable `)}
|
||||
<a href="https://xrpl.org/xrp-ledger-toml.html"><code>{translate(`xrp-ledger.toml`)}</code>{translate(` file`)}</a>.</p>
|
||||
</div>
|
||||
|
||||
<TextLookupForm {...domainButtonProps} />
|
||||
<TextLookupForm {...addressButtonProps} />
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -635,6 +635,7 @@
|
||||
- label: WebSocket API Tool
|
||||
- label: ripple.txt Validator
|
||||
- label: xrp-ledger.toml Checker
|
||||
page: resources/dev-tools/xrp-ledger-toml-checker.page.tsx
|
||||
- label: Domain Verification Checker
|
||||
- label: XRP Faucets
|
||||
page: resources/dev-tools/xrp-faucets.page.tsx
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -16,6 +16,7 @@ const ACCOUNT_FIELDS = [
|
||||
const WORKS = 'rSTAYKxF2K77ZLZ8GoAwTqPGaphAqMyXV'
|
||||
const NOTOML = 'rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz'
|
||||
const NODOMAIN = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'
|
||||
// validator.xrpl-labs.com (Works for the domain side)
|
||||
|
||||
let socket;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FC } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import React = require('react');
|
||||
import XRPLoader from '../components/XRPLoader';
|
||||
import * as xrpl from 'xrpl'
|
||||
|
||||
export const MIN_LOADER_MS = 1250
|
||||
export const DEFAULT_TIMEOUT = 1000
|
||||
@@ -65,7 +66,6 @@ export const XRPLGuard: FC<{ testCheck?: () => boolean, 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,
|
||||
)
|
||||
|
||||
65
package-lock.json
generated
65
package-lock.json
generated
@@ -15,12 +15,14 @@
|
||||
"@redocly/realm": "0.69.4",
|
||||
"@uiw/codemirror-themes": "4.21.21",
|
||||
"@uiw/react-codemirror": "^4.21.21",
|
||||
"axios": "^1.6.2",
|
||||
"clsx": "^2.0.0",
|
||||
"lottie-react": "^2.4.0",
|
||||
"moment": "^2.29.4",
|
||||
"react": "^18.2.0",
|
||||
"react-alert": "^7.0.3",
|
||||
"react18-json-view": "^0.2.6",
|
||||
"smol-toml": "^1.1.3",
|
||||
"xrpl": "^3.0.0-beta.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -2994,29 +2996,6 @@
|
||||
"@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.87.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swagger-api/apidom-reference/node_modules/axios": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
|
||||
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swagger-api/apidom-reference/node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
@@ -3570,11 +3549,26 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.26.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
|
||||
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
|
||||
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.8"
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios/node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
@@ -10148,6 +10142,15 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/smol-toml": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.1.3.tgz",
|
||||
"integrity": "sha512-qTyy6Owjho1ISBmxj4HdrFWB2kMQ5RczU6J04OqslSfdSH656OIHuomHS4ZDvhwm37nig/uXyiTMJxlC9zIVfw==",
|
||||
"engines": {
|
||||
"node": ">= 18",
|
||||
"pnpm": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
@@ -10903,6 +10906,14 @@
|
||||
"@babel/runtime": "^7.17.2"
|
||||
}
|
||||
},
|
||||
"node_modules/typesense/node_modules/axios": {
|
||||
"version": "0.26.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
|
||||
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.8"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
||||
|
||||
@@ -18,12 +18,14 @@
|
||||
"@redocly/realm": "0.69.4",
|
||||
"@uiw/codemirror-themes": "4.21.21",
|
||||
"@uiw/react-codemirror": "^4.21.21",
|
||||
"axios": "^1.6.2",
|
||||
"clsx": "^2.0.0",
|
||||
"lottie-react": "^2.4.0",
|
||||
"moment": "^2.29.4",
|
||||
"react": "^18.2.0",
|
||||
"react-alert": "^7.0.3",
|
||||
"react18-json-view": "^0.2.6",
|
||||
"smol-toml": "^1.1.3",
|
||||
"xrpl": "^3.0.0-beta.1"
|
||||
},
|
||||
"overrides": {
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
.toml-checker {
|
||||
#result {
|
||||
display: none;
|
||||
}
|
||||
#verify-domain-result {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,6 @@ $line-height-base: 1.5;
|
||||
@import "_video.scss";
|
||||
@import "_contribute.scss";
|
||||
// @import "_top-banner.scss";
|
||||
@import "_toml-checker.scss";
|
||||
@import "_tutorials.scss";
|
||||
@import "_docs-landing.scss";
|
||||
@import "_xrplai.scss";
|
||||
|
||||
Reference in New Issue
Block a user