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>, headerText: string, fields?: Object[], domainToVerify?: string, filterDisplayedFieldsTo?: Function): Promise { 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: (
    {formattedEntries}
), 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>, 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>, 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>, 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: (

{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 ")} {translate("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 { 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>, 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 =

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

} } else { response.followUpMessage =

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

} } else { response.followUpMessage =

{translate("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)) }) }