import * as React from 'react'; import { Link } from "@redocly/theme/components/Link/Link"; 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>, headerText: string, fields?: Object[], domainToVerify?: string, filterDisplayedFieldsTo?: Function): Promise { // Note, apparently this is not an appropriate place // to call useThemeHooks for translation. // TODO: Find a way to translate the outputs from this tool. // 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: (
    {formattedEntries}
), icon: icon } }) } return relevantTomlFieldsExist } else { // Invalid toml data addNewLogEntry(setLogEntries, { message: headerText, id: headerText, status: { icon: { label: "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>, metadata?: MetadataField) { if (metadata) { const metadataId = 'metadata-log' const metadataLogEntry = { message: "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: "WRONG TYPE - SHOULD BE TABLE", type: "ERROR", }, }}) } else { updateLogEntry(setLogEntries, {...metadataLogEntry, status: { icon: { label: "FOUND", type: "SUCCESS", }, }}) if (metadata.modified) { const modifiedLogId = 'modified-date-log' const modifiedLogEntry = { message: "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: "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>, tomlData, addressToVerify?: string, domain?: string) { const parsingTomlLogEntry: LogEntryItem = { message: "Parsing TOML data...", id: 'parsing-toml-data-log', } addNewLogEntry(setLogEntries, parsingTomlLogEntry) let parsed: XrplToml try { parsed = parse(tomlData) updateLogEntry(setLogEntries, {...parsingTomlLogEntry, status: { icon: { label: "SUCCESS", type: "SUCCESS", }, }}) } catch(e) { updateLogEntry(setLogEntries, {...parsingTomlLogEntry, status: { icon: { label: e, type: "ERROR", }, }}) return } validateAndDisplayMetadata(setLogEntries, parsed.METADATA) const accountHeader = "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: 'Account has been found in TOML file and validated.', id: statusLogId, status: { icon: { label: "DOMAIN VALIDATED", type: "SUCCESS", check: true, } } }) } else { // We failed to find any entries which match the account we're looking for addNewLogEntry(setLogEntries, { message: "Account:", id: 'toml-account-entry-log', status: { icon: { label: "NOT FOUND", type: "ERROR" } } }) addNewLogEntry(setLogEntries, { message: "Account not found in TOML file. Domain cannot be verified.", id: statusLogId, status: { icon: { label: "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, accountHeader, parsed.ACCOUNTS, domain) // We then display the rest of the toml as additional information await validateAndDisplayFields(setLogEntries, "Validators:", parsed.VALIDATORS) await validateAndDisplayFields(setLogEntries, "Principals:", parsed.PRINCIPALS) await validateAndDisplayFields(setLogEntries, "Servers:", parsed.SERVERS) await validateAndDisplayFields(setLogEntries, "Currencies:", parsed.CURRENCIES) } } /** * 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>, domain: string, accountToVerify?: string) { const url = "https://" + domain + TOML_PATH const checkUrlId = `check-url-log` const logEntry = { message: `Checking ${url} ...`, id: checkUrlId, } addNewLogEntry(setLogEntries, logEntry) try { const data = await fetch(url).then( (response) => { if (response.status >= 400) { throw new Error(`Server returned status code ${response.status}`) } return response.text() }) updateLogEntry(setLogEntries, {...logEntry, status: { icon: { label: "FOUND", type: "SUCCESS", }, }}) // Continue to the next step of verification parseXRPLToml(setLogEntries, data, accountToVerify, domain) } catch (e) { const errorUpdate: LogEntryItem = {...logEntry, status: { icon: { label: e, type: "ERROR", }, followUpMessage: (

{"Check if the file is actually hosted at the URL above, " + "check your server's HTTPS settings and certificate, and make sure your server provides the required "} CORS header.

) }} 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>,) { const logId = 'decoding-domain-hex' addNewLogEntry(setAccountLogEntries, { message: 'Decoding domain hex', id: logId, status: { icon: { label: '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>, accountToVerify: string, socket?: WebSocket) { // Reset the logs setAccountLogEntries([]) const walletLogEntry = { message: `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: `ERROR`, type: `ERROR`, }, }; try { data = JSON.parse(event.data) if (data.status === 'success') { if (data.result.account_data.Domain) { try { response = { icon: { label: '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 =

{`Error decoding domain field: ${data.result.account_data.Domain}`}

} } else { response.followUpMessage =

{"Make sure the account has the Domain field set."}

} } else { response.followUpMessage =

{"Make sure you are entering a valid XRP Ledger address."}

} updateLogEntry(setAccountLogEntries, { ...walletLogEntry, status: response }) } catch { socket.close() return false } }) socket.addEventListener('open', () => { socket.send(JSON.stringify(data)) }) }