Re-level non-docs content to top of repo and rename content→docs

This commit is contained in:
mDuo13
2024-01-31 16:24:01 -08:00
parent f841ef173c
commit c10beb85c2
2907 changed files with 1 additions and 1 deletions

View File

@@ -0,0 +1,43 @@
import clsx from 'clsx'
import * as React from 'react';
import { useTranslate } from '@portal/hooks';
const alertStyle = {
position: "relative",
margin: "0px",
zIndex: "9999",
}
function typeToClass(type: string): string {
if(type === "error") {
return "alert-danger"
} else if(type === "success") {
return "alert-success"
} else if(type === "info") {
return "alert-info"
} else {
return ""
}
}
interface AlertTemplateProps {
message: string
options: {
type: string
}
style: any
close: any // Callback to close the alert early
}
export default function AlertTemplate ({ message, options, style, close }: AlertTemplateProps): React.JSX.Element {
const { translate } = useTranslate()
return(
<div className={clsx("bootstrap-growl alert alert-dismissible", typeToClass(options.type))} style={{ ...alertStyle, ...style }}>
<button className="close" data-dismiss="alert" type="button" onClick={close}>
<span aria-hidden="true">×</span>
<span className="sr-only">{translate("Close")}</span>
</button>
{message}
</div>
)
}

View File

@@ -0,0 +1,56 @@
import * as React from 'react';
import { useState } from 'react';
import { useTranslate } from '@portal/hooks';
import { clsx } from 'clsx'
import { isValidAddress } from 'xrpl'
function onDestinationAddressChange(
event: React.ChangeEvent<HTMLInputElement>,
setDestinationAddress: React.Dispatch<React.SetStateAction<string>>,
setIsValidDestinationAddress: React.Dispatch<React.SetStateAction<boolean>>
): void {
const newAddress = event.target.value
setDestinationAddress(newAddress)
setIsValidDestinationAddress(isValidAddress(newAddress))
}
export interface DestinationAddressInputProps {
defaultDestinationAddress: string,
destinationAddress: string,
setDestinationAddress: React.Dispatch<React.SetStateAction<string>>,
}
export function DestinationAddressInput(
{
defaultDestinationAddress,
destinationAddress,
setDestinationAddress,
} : DestinationAddressInputProps
): React.JSX.Element {
const { translate } = useTranslate()
const [ isValidDestinationAddress, setIsValidDestinationAddress ] = useState(true)
return (
<div>
<div className="form-group">
<label htmlFor="destination_address">
{translate("Destination Address")}
</label>
<input type="text" className={clsx("form-control",
// Defaults to not having "is-valid" / "is-invalid" classes
(destinationAddress !== defaultDestinationAddress) && (isValidDestinationAddress ? "is-valid" : "is-invalid"))}
id="destination_address"
onChange={(event) => onDestinationAddressChange(event, setDestinationAddress, setIsValidDestinationAddress)}
aria-describedby="destination_address_help"
defaultValue={destinationAddress} />
<small id="destination_address_help" className="form-text text-muted">
{translate("Send transactions to this XRP Testnet address")}
</small>
</div>
<p className={clsx("devportal-callout caution", !(isValidDestinationAddress && destinationAddress[0] === 'X') && "collapse")}
id="x-address-warning">
<strong>{translate("Caution:")}</strong>
{translate(" This X-address is intended for use on Mainnet. Testnet X-addresses have a \"T\" prefix instead.")}
</p>
</div>)
}

View File

@@ -0,0 +1,199 @@
import * as React from 'react';
import { useTranslate } from '@portal/hooks';
import { clsx } from 'clsx'
import { Client, type Wallet, type TxResponse, dropsToXrp } from 'xrpl'
import { errorNotif, TESTNET_URL } from '../utils'
export interface InitializationProps {
existingClient: Client | undefined,
alert, // From useAlert()
setClient: React.Dispatch<React.SetStateAction<Client | undefined>>,
setBalance: React.Dispatch<React.SetStateAction<number>>,
setSendingWallet: React.Dispatch<React.SetStateAction<Wallet | undefined>>,
setIsInitEnabled: React.Dispatch<React.SetStateAction<boolean>>,
setConnectionReady: React.Dispatch<React.SetStateAction<boolean>>,
partialPaymentParams: {
setPpIssuerWallet: React.Dispatch<React.SetStateAction<Wallet | undefined>>,
setPpWidthPercent: React.Dispatch<React.SetStateAction<number>>,
ppCurrencyCode: string
}
}
async function setUpForPartialPayments
(
client: Client,
sendingWallet: Wallet,
setPpIssuerWallet: React.Dispatch<React.SetStateAction<Wallet | undefined>>,
setPpWidthPercent: React.Dispatch<React.SetStateAction<number>>,
ppCurrencyCode: string,
) {
console.debug("Starting partial payment setup...")
// Causing loader to appear because no longer 0%
setPpWidthPercent(1)
let ppIssuerWallet;
// 1. Get a funded address to use as issuer
try {
ppIssuerWallet = (await client.fundWallet()).wallet
setPpIssuerWallet(ppIssuerWallet)
} catch(error) {
console.log("Error getting issuer address for partial payments:", error)
return
}
setPpWidthPercent(20)
// 2. Set Default Ripple on issuer
let resp: TxResponse = await client.submitAndWait({
TransactionType: "AccountSet",
Account: ppIssuerWallet.address,
SetFlag: 8 // asfDefaultRipple
}, { wallet: ppIssuerWallet })
if (resp === undefined) {
console.log("Couldn't set Default Ripple for partial payment issuer")
return
}
setPpWidthPercent(40)
// 3. Make a trust line from sending address to issuer
resp = await client.submitAndWait({
TransactionType: "TrustSet",
Account: sendingWallet.address,
LimitAmount: {
currency: ppCurrencyCode,
value: "1000000000", // arbitrarily, 1 billion fake currency
issuer: ppIssuerWallet.address
}
}, { wallet: sendingWallet })
if (resp === undefined) {
console.log("Error making trust line to partial payment issuer")
return
}
setPpWidthPercent(60)
// 4. Issue fake currency to main sending address
resp = await client.submitAndWait({
TransactionType: "Payment",
Account: ppIssuerWallet.address,
Destination: sendingWallet.address,
Amount: {
currency: ppCurrencyCode,
value: "1000000000",
issuer: ppIssuerWallet.address
}
}, { wallet: ppIssuerWallet })
if (resp === undefined) {
console.log("Error sending fake currency from partial payment issuer")
return
}
setPpWidthPercent(80)
// 5. Place offer to buy issued currency for XRP
// When sending the partial payment, the sender consumes their own offer (!)
// so they end up paying themselves issued currency then delivering XRP.
resp = await client.submitAndWait({
TransactionType: "OfferCreate",
Account: sendingWallet.address,
TakerGets: "1000000000000000", // 1 billion XRP
TakerPays: {
currency: ppCurrencyCode,
value: "1000000000",
issuer: ppIssuerWallet.address
}
}, { wallet: sendingWallet })
if (resp === undefined) {
console.log("Error placing order to enable partial payments")
return
}
setPpWidthPercent(100)
// Done. Enable "Send Partial Payment" button
console.log("Done getting ready to send partial payments.")
}
async function onInitClick(
props: InitializationProps
): Promise<void> {
const {
existingClient,
alert, // From useAlert()
setClient,
setBalance,
setSendingWallet,
setIsInitEnabled,
setConnectionReady,
partialPaymentParams
} = {...props}
if(existingClient) {
console.log("Already initializing!")
return
}
console.log("Connecting to Testnet WebSocket...")
const client = new Client(TESTNET_URL)
client.on('connected', () => {
setConnectionReady(true)
})
client.on('disconnected', (code) => {
setConnectionReady(false)
})
setClient(client)
await client.connect()
console.debug("Getting a sending address from the faucet...")
try {
const fundResponse = await client.fundWallet()
const sendingWallet = fundResponse.wallet
setSendingWallet(sendingWallet)
// Using Number(...) can result in loss of precision since Number is smaller than the precision of XRP,
// but this shouldn't affect the learning tool as that much XRP is not given to any test account.
setBalance(Number(dropsToXrp(fundResponse.balance)))
setIsInitEnabled(false)
await setUpForPartialPayments(
client,
sendingWallet,
partialPaymentParams.setPpIssuerWallet,
partialPaymentParams.setPpWidthPercent,
partialPaymentParams.ppCurrencyCode,
)
} catch(error) {
console.error(error)
errorNotif(alert, "There was an error with the XRP Ledger Testnet Faucet. Reload this page to try again.")
return
}
}
export function InitButton({
isInitEnabled,
toInit
}: {
isInitEnabled: boolean,
toInit: InitializationProps
}): React.JSX.Element {
const { translate } = useTranslate()
return (<div className="form-group">
<button className={clsx("btn btn-primary form-control", isInitEnabled ? "" : "disabled")}
type="button" id="init_button"
onClick={() => {
onInitClick(
toInit,
)
}}
disabled={!isInitEnabled}
title={isInitEnabled ? "" : "done"}>
{translate("Initialize")}
</button>
{!isInitEnabled && (<div>&nbsp;<i className="fa fa-check-circle"></i></div>)}
<small className="form-text text-muted">
{translate("Set up the necessary Testnet XRP addresses to send test payments.")}
</small>
</div>)
}

View File

@@ -0,0 +1,9 @@
import * as React from 'react';
import { useTranslate } from "@portal/hooks";
export const Loader = () => {
const { translate } = useTranslate();
return <img className="throbber" src="/img/xrp-loader-96.png" alt={translate("(loading)")} />
}

View 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>
)
}

View File

@@ -0,0 +1,81 @@
import React, { JSX, ReactElement, ReactNode } from 'react';
import { useTranslate } from '@portal/hooks';
interface ModalProps {
id: string // used for targeting animations
title: string,
children: ReactNode,
footer?: ReactNode,
onClose: () => void;
}
/**
* Reusable component that leverages bootstrap's jquery library
*/
export const Modal = ({title, footer, children, onClose, id}: ModalProps) => {
return <div
className="modal fade"
id={id}
tabIndex={-1}
role="dialog"
aria-hidden="true"
>
<div className="modal-dialog modal-dialog-centered" role="document">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">{title}</h5>
<button
type="button"
className="close"
aria-label="Close"
onClick={onClose}
data-dismiss="modal"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div className="modal-body">
{children}
</div>
<div className="modal-footer">
{ footer ? footer : (
<ModalCloseBtn onClick={onClose} />
)}
</div>
</div>
</div>
</div>
}
export const ModalCloseBtn = ({onClick}) => {
const { translate } = useTranslate();
return <button
type="button"
className="btn btn-outline-secondary"
data-dismiss="modal"
onClick={onClick}
>
{translate('Close')}
</button>
}
export const ModalClipboardBtn = ({textareaRef}) => {
const { translate } = useTranslate();
return <button
title={translate('Copy to clipboard')}
className="btn btn-outline-secondary clipboard-btn"
onClick={() => copyToClipboard(textareaRef)}
>
<i className="fa fa-clipboard"></i>
</button>
}
const copyToClipboard = async (textareaRef) => {
if (textareaRef.current) {
textareaRef.current.select();
textareaRef.current.focus();
await navigator.clipboard.writeText(textareaRef.current.value);
}
};

View File

@@ -0,0 +1,44 @@
import * as React from 'react';
import { useTranslate } from '@portal/hooks';
import { clsx } from 'clsx'
import { type Wallet } from 'xrpl'
export function StatusSidebar({
balance,
sendingWallet,
connectionReady,
txHistory
}:
{
balance: number,
sendingWallet: Wallet | undefined,
connectionReady: boolean,
txHistory: React.JSX.Element[],
}) {
const { translate } = useTranslate();
return (<aside className="right-sidebar col-lg-6 order-lg-4">
<div id="connection-status" className="card">
<div className="card-header">
<h4>{translate("Status")}</h4>
</div>
<div className="card-body">
<ul className="list-group list-group-flush">
<li className="list-group-item" id="connection-status-label">{translate("XRP Testnet:")}</li>
<li className={clsx("list-group-item", (connectionReady ? 'active' : 'disabled'))} id="connection-status-item">{connectionReady ? translate("Connected") : translate("Not Connected")}</li>
<li className="list-group-item" id="sending-address-label">{translate("Sending Address:")}</li>
<li className="list-group-item disabled sending-address-item">{sendingWallet ? sendingWallet.address : translate("(None)")}</li>
<li className="list-group-item" id="balance-label">{translate("Testnet XRP Available:")}</li>
<li className="list-group-item disabled" id="balance-item">{balance ? translate(balance.toString()) : translate("(None)")}</li>
</ul>
<div id="tx-sender-history">
<h5 className="m-3">{translate("Transaction History")}</h5>
<ul className="list-group list-group-flush">
{txHistory}
</ul>
</div>
</div>
</div>
</aside>)
}

View 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>)
}

View File

@@ -0,0 +1,156 @@
import * as React from 'react';
import { useState } from 'react'
import { useTranslate } from '@portal/hooks';
import { clsx } from 'clsx'
import { type Transaction, type Wallet } from 'xrpl'
import { SubmitConstData, submitAndUpdateUI, canSendTransaction } from '../utils';
export interface TransactionButtonProps {
submitConstData: SubmitConstData,
connectionReady: boolean,
transaction: Transaction,
sendingWallet: Wallet | undefined
id: string, // Used to set all ids within component
content: {
buttonText: string,
units: string, // Displays after the input number
longerDescription: React.JSX.Element // JSX allows for embedding links within the longer description
buttonTitle?: string // Only used while loading bar is activated
},
inputSettings?: {
defaultValue: number, // Should NOT be a dynamic number
setInputValue: React.Dispatch<React.SetStateAction<number>>,
min: number,
max: number,
},
loadingBar?: {
id: string,
widthPercent: number,
description: string,
defaultOn: boolean,
},
checkBox?: {
setCheckValue: React.Dispatch<React.SetStateAction<boolean>>,
defaultValue: boolean,
description: string,
}
customOnClick?: Function
}
function shouldDisableButton(
connectionReady: boolean,
sendingWallet: Wallet | undefined,
waitingForTransaction: boolean,
loadingBar?: {
widthPercent: number
}
): boolean {
return !canSendTransaction(connectionReady, sendingWallet?.address)
|| waitingForTransaction
|| (!!(loadingBar?.widthPercent) && loadingBar.widthPercent < 100)
}
export function TransactionButton({
id,
submitConstData,
connectionReady,
transaction,
sendingWallet,
content,
inputSettings,
loadingBar,
checkBox,
customOnClick
}: TransactionButtonProps ) {
const { translate } = useTranslate()
const [waitingForTransaction, setWaitingForTransaction] = useState(false)
return (
<div>
<div className="form-group" id={id}>
{/* Optional loading bar - Used for Partial Payments setup and EscrowFinish wait time */}
{loadingBar?.id && <div className="progress mb-1" id={loadingBar?.id ?? ""}>
<div className={
clsx("progress-bar progress-bar-striped w-0",
(loadingBar?.widthPercent < 100 && loadingBar?.widthPercent > 0) && "progress-bar-animated")}
style={{width: (Math.min(loadingBar?.widthPercent + (loadingBar?.defaultOn ? 1 : 0), 100)).toString() + "%",
display: (loadingBar?.widthPercent >= 100) ? 'none' : ''}}>
&nbsp;
</div>
{(loadingBar?.widthPercent < 100 && loadingBar?.widthPercent > 0 || (loadingBar.defaultOn && loadingBar?.widthPercent === 0))
&& <small className="justify-content-center d-flex position-absolute w-100">
{translate(loadingBar?.description)}
</small>}
</div>}
<div className="input-group mb-3">
{/* Loading icon for when transaction is being submitted */}
<div className="input-group-prepend">
<span className="input-group-text loader" style={{display: waitingForTransaction ? '' : 'none'}}>
<img className="throbber" src="/img/xrp-loader-96.png" alt={translate("(loading)")} />
</span>
</div>
<button className={clsx("btn btn-primary form-control needs-connection",
(shouldDisableButton(connectionReady, sendingWallet, waitingForTransaction, loadingBar) && "disabled"))}
type="button" id={id + "_btn"}
disabled={shouldDisableButton(connectionReady, sendingWallet, waitingForTransaction, loadingBar)}
onClick={async () => {
setWaitingForTransaction(true)
customOnClick ? await customOnClick() : await submitAndUpdateUI(submitConstData, sendingWallet!, transaction)
setWaitingForTransaction(false)
}}
title={(loadingBar && (loadingBar.widthPercent > 0 && loadingBar.widthPercent < 100)) ? translate(content.buttonTitle) : ""}
>
{translate(content.buttonText)}
</button>
{inputSettings &&
<input id={id + "_amount"} className="form-control" type="number"
aria-describedby={id + "amount_help"}
defaultValue={inputSettings?.defaultValue}
min={inputSettings?.min}
max={inputSettings?.max}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
// Enforce min / max values
let { value, min, max } = event.target;
const newValue = Math.max(Number(min), Math.min(Number(max), Number(value)));
// Share the value so other logic can update based on it
inputSettings?.setInputValue(newValue)
}
} />}
{inputSettings && <div className="input-group-append">
<span className="input-group-text" id={id + "_help"}>
{translate(content.units)}
</span>
</div>
}
{/* Used for Escrow */}
{checkBox && <span className="input-group-text">
(
<input type="checkbox"
id={id + "_checkbox"}
defaultValue={checkBox.defaultValue ? 1 : 0}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => checkBox.setCheckValue(event.target.checked)} />
<label className="form-check-label" htmlFor={id + "_checkbox"}>
{translate(checkBox.description)}
</label>)
</span>}
</div>
<small className="form-text text-muted">
{content.longerDescription}
</small>
</div>
<hr />
</div>)
}

View File

@@ -0,0 +1,48 @@
import { useTranslate } from '@portal/hooks'
import { ReactElement, useState } from 'react';
import JsonView from 'react18-json-view'
interface RPCResponseGroupProps {
response: any
anchor: ReactElement
customExpanded?: number,
customExpandedText?: string
}
export const RPCResponseGroup = ({ response, anchor, customExpanded, customExpandedText }: RPCResponseGroupProps) => {
const [expanded, setExpanded] = useState<number | false>(1)
return <div className="group group-tx">
<h3>{anchor}</h3>
<RPCResponseGroupExpanders customExpanded={customExpanded} customExpandedText={customExpandedText} setExpanded={setExpanded} />
<JsonView
src={response}
collapsed={expanded}
collapseStringsAfterLength={100}
enableClipboard={false}
/>
<RPCResponseGroupExpanders customExpanded={customExpanded} customExpandedText={customExpandedText} setExpanded={setExpanded} />
</div>
}
const RPCResponseGroupExpanders = ({ customExpanded, customExpandedText, setExpanded }) => {
const { translate } = useTranslate();
return <ul className="nav nav-pills">
{customExpanded && customExpandedText && (
<li className="nav-item">
<a className="nav-link" onClick={() => setExpanded(customExpanded)}>
{customExpandedText}
</a>
</li>
)}
<li className="nav-item">
<a className="nav-link" onClick={() => setExpanded(false)}>{translate("expand all")}</a>
</li>
<li className="nav-item">
<a className="nav-link" onClick={() => setExpanded(1)}>
{translate("collapse all")}
</a>
</li>
</ul>
}

View File

@@ -0,0 +1,53 @@
import { useTranslate } from "@portal/hooks";
import { Connection } from './types';
import { ChangeEvent } from 'react';
import { Modal } from '../Modal';
interface ConnectionButtonProps {
selectedConnection: Connection;
setSelectedConnection: (value: Connection) => void;
connections: Connection[];
}
interface ConnectionProps extends ConnectionButtonProps {
closeConnectionModal: any;
}
export const ConnectionModal: React.FC<ConnectionProps> = ({
selectedConnection,
setSelectedConnection,
closeConnectionModal,
connections,
}) => {
const { translate } = useTranslate();
const handleConnectionChange = (event: ChangeEvent<HTMLInputElement>) => {
const selectedValue = event.target.value;
const foundConnection = connections.find(
(conn) => conn.id === selectedValue
);
setSelectedConnection(foundConnection);
};
return (
<Modal id="wstool-1-connection-settings" title={translate('Connection Settings')} onClose={closeConnectionModal}>
{connections.map((conn) => (
<div className="form-check" key={conn.id}>
<input
className="form-check-input"
type="radio"
name="wstool-1-connection"
id={conn.id}
value={conn.id}
checked={selectedConnection.id === conn.id}
onChange={handleConnectionChange}
/>
<label className="form-check-label" htmlFor={conn.id}>
<div dangerouslySetInnerHTML={{ __html: conn.longname }} />
</label>
</div>
))}
</Modal>
);
};

View File

@@ -0,0 +1,93 @@
import { useTranslate } from "@portal/hooks";
import { Connection } from './types';
import { useRef, useState } from 'react';
import { Modal, ModalClipboardBtn, ModalCloseBtn } from '../Modal';
interface CurlButtonProps {
currentBody: any;
selectedConnection: Connection;
}
interface CurlProps extends CurlButtonProps{
closeCurlModal: () => void;
}
const getCurl = function (currentBody, selectedConnection: Connection) {
let body;
try {
// change WS to JSON-RPC syntax
const params = JSON.parse(currentBody);
delete params.id;
const method = params.command;
delete params.command;
const body_json = { method: method, params: [params] };
body = JSON.stringify(body_json, null, null);
} catch (e) {
alert("Can't provide curl format of invalid JSON syntax");
return;
}
const server = selectedConnection.jsonrpc_url;
return `curl -H 'Content-Type: application/json' -d '${body}' ${server}`;
};
export const CurlModal: React.FC<CurlProps> = ({
currentBody,
selectedConnection,
}) => {
const curlRef = useRef(null);
const { translate } = useTranslate();
const footer = <>
<ModalClipboardBtn textareaRef={curlRef} />
<ModalCloseBtn onClick={() => {}} />
</>
return (
<Modal
id="wstool-1-curl"
title={translate("cURL Syntax")}
onClose={() => {}}
footer={footer}
>
<form>
<div className="form-group">
<label htmlFor="curl-box-1">
Use the following syntax to make the equivalent JSON-RPC
request using <a href="https://curl.se/">cURL</a> from a
commandline interface:
</label>
<textarea
id="curl-box-1"
className="form-control"
rows={8}
ref={curlRef}
>
{getCurl(currentBody, selectedConnection)}
</textarea>
</div>
</form>
</Modal>
);
};
export const CurlButton = ({selectedConnection, currentBody}: CurlButtonProps) => {
const [showCurlModal, setShowCurlModal] = useState(false);
return <>
<button
className="btn btn-outline-secondary curl"
data-toggle="modal"
data-target="#wstool-1-curl"
title="cURL syntax"
onClick={() => setShowCurlModal(true)}
>
<i className="fa fa-terminal"></i>
</button>
{showCurlModal && <CurlModal
closeCurlModal={() => setShowCurlModal(false)}
currentBody={currentBody}
selectedConnection={selectedConnection}
/>}
</>
}

View File

@@ -0,0 +1,649 @@
[
{
"group": "Account Methods",
"methods": [
{
"name": "account_channels",
"description": "Returns information about an account's <a href='payment-channels.html'>payment channels</a>.",
"link": "account_channels.html",
"body": {
"id": 1,
"command": "account_channels",
"account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"destination_account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"ledger_index": "validated"
}
},
{
"name": "account_currencies",
"description": "Retrieves a list of currencies that an account can send or receive, based on its trust lines.",
"link": "account_currencies.html",
"body": {
"command": "account_currencies",
"account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index": "validated"
}
},
{
"name": "account_info",
"description": "Retrieves information about an account, its activity, and its XRP balance.",
"link": "account_info.html",
"body": {
"id": 2,
"command": "account_info",
"account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index": "current",
"queue": true
}
},
{
"name": "account_lines",
"description": "Retrieves information about an account's trust lines, including balances for all non-XRP currencies and assets.",
"link": "account_lines.html",
"body": {
"id": 2,
"command": "account_lines",
"account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index": "validated"
}
},
{
"name": "account_nfts",
"description": "Retrieves NFTs owned by an account.",
"link": "account_nfts.html",
"body": {
"command": "account_nfts",
"account": "rsuHaTvJh1bDmDoxX9QcKP7HEBSBt4XsHx",
"ledger_index": "validated"
}
},
{
"name": "account_objects",
"description": "Returns the raw ledger format for all objects owned by an account.",
"link": "account_objects.html",
"body": {
"id": 1,
"command": "account_objects",
"account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index": "validated",
"type": "state",
"limit": 10
}
},
{
"name": "account_offers",
"description": "Retrieves a list of offers made by a given account that are outstanding as of a particular ledger version.",
"link": "account_offers.html",
"body": {
"id": 2,
"command": "account_offers",
"account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
}
},
{
"name": "account_tx",
"description": "Retrieves a list of transactions that affected the specified account.",
"link": "account_tx.html",
"body": {
"id": 2,
"command": "account_tx",
"account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index_min": -1,
"ledger_index_max": -1,
"binary": false,
"limit": 2,
"forward": false
}
},
{
"name": "gateway_balances",
"description": "Calculates the total balances issued by a given account, optionally excluding amounts held by operational addresses.",
"link": "gateway_balances.html",
"body": {
"id": "example_gateway_balances_1",
"command": "gateway_balances",
"account": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q",
"hotwallet": [
"rKm4uWpg9tfwbVSeATv4KxDe6mpE9yPkgJ",
"ra7JkEzrgeKHdzKgo4EUUVBnxggY4z37kt"
],
"ledger_index": "validated"
}
},
{
"name": "noripple_check",
"description": "Compares an account's Default Ripple and No Ripple flags to the recommended settings.",
"link": "noripple_check.html",
"body": {
"id": 0,
"command": "noripple_check",
"account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"role": "gateway",
"ledger_index": "current",
"limit": 2,
"transactions": true
}
}
]
},
{
"group": "Ledger Methods",
"methods": [
{
"name": "ledger",
"description": "Retrieves information about the public ledger.",
"link": "ledger.html",
"body": {
"id": 14,
"command": "ledger",
"ledger_index": "validated",
"full": false,
"accounts": false,
"transactions": false,
"expand": false,
"owner_funds": false
}
},
{
"name": "ledger_closed",
"description": "Returns the unique identifiers of the most recently closed ledger. (This ledger is not necessarily validated and immutable yet.)",
"link": "ledger_closed.html",
"body": {
"id": 2,
"command": "ledger_closed"
}
},
{
"name": "ledger_current",
"description": "Returns the unique identifiers of the current in-progress ledger.",
"link": "ledger_closed.html",
"body": {
"id": 2,
"command": "ledger_current"
}
},
{
"name": "ledger_data",
"description": "Retrieves contents of the specified ledger.",
"link": "ledger_data.html",
"body": {
"id": 2,
"ledger_hash": "842B57C1CC0613299A686D3E9F310EC0422C84D3911E5056389AA7E5808A93C8",
"command": "ledger_data",
"limit": 5,
"binary": true
}
},
{
"name": "ledger_entry - by object ID",
"description": "Returns an object by its unique ID.",
"link": "ledger_entry.html#get-ledger-object-by-id",
"body": {
"command": "ledger_entry",
"index": "7DB0788C020F02780A673DC74757F23823FA3014C1866E72CC4CD8B226CD6EF4",
"ledger_index": "validated"
}
},
{
"name": "ledger_entry - AccountRoot",
"description": "Returns a single account in its raw ledger format.",
"link": "ledger_entry.html#get-accountroot-object",
"body": {
"id": "example_get_accountroot",
"command": "ledger_entry",
"account_root": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_index": "validated"
}
},
{
"name": "ledger_entry - AMM",
"description": "Returns a single Automated Market Maker object in its raw ledger format.",
"link": "ledger_entry.html#get-amm-object",
"status": "not_enabled",
"body": {
"id": "example_get_amm",
"command": "ledger_entry",
"amm": {
"asset": {
"currency": "XRP"
},
"asset2": {
"currency": "TST",
"issuer": "rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd"
}
},
"ledger_index": "validated"
}
},
{
"name": "ledger_entry - DirectoryNode",
"description": "Returns a directory object in its raw ledger format.",
"link": "ledger_entry.html#get-directorynode-object",
"body": {
"id": "example_get_directorynode",
"command": "ledger_entry",
"directory": {
"owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"sub_index": 0
},
"ledger_index": "validated"
}
},
{
"name": "ledger_entry - NFT Page",
"description": "Returns an NFT Page object in its raw ledger format.",
"link": "ledger_entry.html#get-nft-page",
"body": {
"id": "example_get_nft_page",
"command": "ledger_entry",
"nft_page": "255DD86DDF59D778081A06D02701E9B2C9F4F01DFFFFFFFFFFFFFFFFFFFFFFFF",
"ledger_index": "validated"
}
},
{
"name": "ledger_entry - Offer",
"description": "Returns an Offer object in its raw ledger format.",
"link": "ledger_entry.html#get-offer-object",
"body": {
"id": "example_get_offer",
"command": "ledger_entry",
"offer": {
"account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"seq": 359
},
"ledger_index": "validated"
}
},
{
"name": "ledger_entry - RippleState",
"description": "Returns a RippleState object in its raw ledger format.",
"link": "ledger_entry.html#get-ripplestate-object",
"body": {
"id": "example_get_ripplestate",
"command": "ledger_entry",
"ripple_state": {
"accounts": [
"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"
],
"currency": "USD"
},
"ledger_index": "validated"
}
},
{
"name": "ledger_entry - Check",
"description": "Returns a Check object in its raw ledger format.",
"link": "ledger_entry.html#get-check-object",
"body": {
"id": "example_get_check",
"command": "ledger_entry",
"check": "C4A46CCD8F096E994C4B0DEAB6CE98E722FC17D7944C28B95127C2659C47CBEB",
"ledger_index": "validated"
}
},
{
"name": "ledger_entry - Escrow",
"description": "Returns an Escrow object in its raw ledger format.",
"link": "ledger_entry.html#get-escrow-object",
"body": {
"id": "example_get_escrow",
"command": "ledger_entry",
"escrow": {
"owner": "rL4fPHi2FWGwRGRQSH7gBcxkuo2b9NTjKK",
"seq": 126
},
"ledger_index": "validated"
}
},
{
"name": "ledger_entry - PayChannel",
"description": "Returns a PayChannel object in its raw ledger format.",
"link": "ledger_entry.html#get-paychannel-object",
"body": {
"id": "example_get_paychannel",
"command": "ledger_entry",
"payment_channel": "C7F634794B79DB40E87179A9D1BF05D05797AE7E92DF8E93FD6656E8C4BE3AE7",
"ledger_index": "validated"
}
},
{
"name": "ledger_entry - DepositPreauth",
"description": "Returns a DepositPreauth object in its raw ledger format.",
"link": "ledger_entry.html#get-depositpreauth-object",
"body": {
"id": "example_get_deposit_preauth",
"command": "ledger_entry",
"deposit_preauth": {
"owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"authorized": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX"
},
"ledger_index": "validated"
}
},
{
"name": "ledger_entry - Ticket",
"description": "Returns a Ticket object in its raw ledger format.",
"link": "ledger_entry.html#get-ticket-object",
"body": {
"id": "example_get_ticket",
"command": "ledger_entry",
"ticket": {
"account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ticket_seq": 389
},
"ledger_index": "validated"
}
}
]
},
{
"group": "Transaction Methods",
"methods": [
{
"name": "submit",
"description": "Submits a transaction to the network to be confirmed and included in future ledgers.",
"link": "submit.html",
"body": {
"id": "example_submit",
"command": "submit",
"tx_blob": "1200002280000000240000001E61D4838D7EA4C6800000000000000000000000000055534400000000004B4E9C06F24296074F7BC48F92A97916C6DC5EA968400000000000000B732103AB40A0490F9B7ED8DF29D246BF2D6269820A0EE7742ACDD457BEA7C7D0931EDB7447304502210095D23D8AF107DF50651F266259CC7139D0CD0C64ABBA3A958156352A0D95A21E02207FCF9B77D7510380E49FF250C21B57169E14E9B4ACFD314CEDC79DDD0A38B8A681144B4E9C06F24296074F7BC48F92A97916C6DC5EA983143E9D4A2B8AA0780F682D136F7A56D6724EF53754"
}
},
{
"name": "submit_multisigned",
"description": "Submits a multi-signed transaction to the network to be confirmed and included in future ledgers.",
"link": "submit_multisigned.html",
"body": {
"id": "submit_multisigned_example",
"command": "submit_multisigned",
"tx_json": {
"Account": "rEuLyBCvcw4CFmzv8RepSiAoNgF8tTGJQC",
"Fee": "30000",
"Flags": 262144,
"LimitAmount": {
"currency": "USD",
"issuer": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"value": "100"
},
"Sequence": 2,
"Signers": [
{
"Signer": {
"Account": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"SigningPubKey": "02B3EC4E5DD96029A647CFA20DA07FE1F85296505552CCAC114087E66B46BD77DF",
"TxnSignature": "30450221009C195DBBF7967E223D8626CA19CF02073667F2B22E206727BFE848FF42BEAC8A022048C323B0BED19A988BDBEFA974B6DE8AA9DCAE250AA82BBD1221787032A864E5"
}
},
{
"Signer": {
"Account": "rUpy3eEg8rqjqfUoLeBnZkscbKbFsKXC3v",
"SigningPubKey": "028FFB276505F9AC3F57E8D5242B386A597EF6C40A7999F37F1948636FD484E25B",
"TxnSignature": "30440220680BBD745004E9CFB6B13A137F505FB92298AD309071D16C7B982825188FD1AE022004200B1F7E4A6A84BB0E4FC09E1E3BA2B66EBD32F0E6D121A34BA3B04AD99BC1"
}
}
],
"SigningPubKey": "",
"TransactionType": "TrustSet",
"hash": "BD636194C48FD7A100DE4C972336534C8E710FD008C0F3CF7BC5BF34DAF3C3E6"
}
}
},
{
"name": "transaction_entry",
"description": "Retrieves information on a single transaction from a specific ledger version.",
"link": "transaction_entry.html",
"body": {
"id": 4,
"command": "transaction_entry",
"tx_hash": "E08D6E9754025BA2534A78707605E0601F03ACE063687A0CA1BDDACFCD1698C7",
"ledger_index": 348734
}
},
{
"name": "tx",
"description": "Retrieves information on a single transaction.",
"link": "tx.html",
"body": {
"id": 1,
"command": "tx",
"transaction": "E08D6E9754025BA2534A78707605E0601F03ACE063687A0CA1BDDACFCD1698C7",
"binary": false
}
}
]
},
{
"group": "Path and Order Book Methods",
"methods": [
{
"name": "book_offers",
"description": "Retrieves a list of offers, also known as the order book, between two currencies.",
"link": "book_offers.html",
"body": {
"id": 4,
"command": "book_offers",
"taker": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"taker_gets": {
"currency": "XRP"
},
"taker_pays": {
"currency": "USD",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"limit": 10
}
},
{
"name": "deposit_authorized",
"description": "Checks whether one account is authorized to send payments directly to another.",
"link": "deposit_authorized.html",
"body": {
"id": 1,
"command": "deposit_authorized",
"source_account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"destination_account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"ledger_index": "validated"
}
},
{
"name": "nft_buy_offers",
"description": "Retrieves offers to buy a given NFT.",
"link": "nft_buy_offers.html",
"body": {
"command": "nft_buy_offers",
"nft_id": "00090000D0B007439B080E9B05BF62403911301A7B1F0CFAA048C0A200000007",
"ledger_index": "validated"
}
},
{
"name": "nft_sell_offers",
"description": "Retrieves offers to sell a given NFT.",
"link": "nft_sell_offers.html",
"body": {
"command": "nft_sell_offers",
"nft_id": "00090000D0B007439B080E9B05BF62403911301A7B1F0CFAA048C0A200000007",
"ledger_index": "validated"
}
},
{
"name": "path_find",
"description": "Searches for a path along which a payment can possibly be made, and periodically sends updates when the path changes over time.",
"link": "path_find.html",
"ws_only": true,
"body": {
"id": 8,
"command": "path_find",
"subcommand": "create",
"source_account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"destination_account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"destination_amount": {
"value": "0.001",
"currency": "USD",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
}
}
},
{
"name": "ripple_path_find",
"description": "Searches one time for a payment path.",
"link": "ripple_path_find.html",
"body": {
"id": 8,
"command": "ripple_path_find",
"source_account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"source_currencies": [
{
"currency": "XRP"
},
{
"currency": "USD"
}
],
"destination_account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"destination_amount": {
"value": "0.001",
"currency": "USD",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
}
}
},
{
"name": "amm_info",
"description": "Looks up info on an Automated Market Maker instance.",
"link": "amm_info.html",
"status": "not_enabled",
"body": {
"command": "amm_info",
"asset": {
"currency": "XRP"
},
"asset2": {
"currency": "TST",
"issuer": "rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd"
}
}
}
]
},
{
"group": "Payment Channel Methods",
"methods": [
{
"name": "channel_authorize",
"description": "Creates a signature that can be used to redeem a specific amount of XRP from a payment channel.",
"link": "channel_authorize.html",
"body": {
"id": "channel_authorize_example_id1",
"command": "channel_authorize",
"channel_id": "5DB01B7FFED6B67E6B0414DED11E051D2EE2B7619CE0EAA6286D67A3A4D5BDB3",
"secret": "s████████████████████████████",
"amount": "1000000"
}
},
{
"name": "channel_verify",
"description": "Checks the validity of a signature that can be used to redeem a specific amount of XRP from a payment channel.",
"link": "channel_verify.html",
"body": {
"id": 1,
"command": "channel_verify",
"channel_id": "5DB01B7FFED6B67E6B0414DED11E051D2EE2B7619CE0EAA6286D67A3A4D5BDB3",
"signature": "304402204EF0AFB78AC23ED1C472E74F4299C0C21F1B21D07EFC0A3838A420F76D783A400220154FB11B6F54320666E4C36CA7F686C16A3A0456800BBC43746F34AF50290064",
"public_key": "aB44YfzW24VDEJQ2UuLPV2PvqcPCSoLnL7y5M1EzhdW4LnK5xMS3",
"amount": "1000000"
}
}
]
},
{
"group": "Subscription Methods",
"methods": [
{
"name": "subscribe",
"description": "Requests periodic notifications from the server when certain events happen.",
"link": "subscribe.html",
"body": {
"id": "Example watch one account and all new ledgers",
"command": "subscribe",
"streams": [
"ledger"
],
"accounts": [
"rrpNnNLKrartuEqfJGpqyDwPj1AFPg9vn1"
]
}
},
{
"name": "unsubscribe",
"description": "Tells the server to stop sending messages for a particular subscription or set of subscriptions.",
"link": "unsubscribe.html",
"body": {
"id": "Example stop watching one account and new ledgers",
"command": "unsubscribe",
"streams": [
"ledger"
],
"accounts": [
"rrpNnNLKrartuEqfJGpqyDwPj1AFPg9vn1"
]
}
}
]
},
{
"group": "Server Info Methods",
"methods": [
{
"name": "fee",
"description": "Reports the current state of the open-ledger requirements for the transaction cost.",
"link": "fee.html",
"body": {
"id": "fee_websocket_example",
"command": "fee"
}
},
{
"name": "server_info",
"description": "Reports a human-readable version of various information about the rippled server being queried.",
"link": "server_info.html",
"body": {
"id": 1,
"command": "server_info"
}
},
{
"name": "server_state",
"description": "Reports a machine-readable version of various information about the rippled server being queried.",
"link": "server_state.html",
"body": {
"id": 1,
"command": "server_state"
}
}
]
},
{
"group": "Utility Methods",
"methods": [
{
"name": "ping",
"description": "Checks that the connection is working.",
"link": "ping.html",
"body": {
"id": 1,
"command": "ping"
}
},
{
"name": "random",
"description": "Provides a random number, which may be a useful source of entropy for clients.",
"link": "random.html",
"body": {
"id": 1,
"command": "random"
}
}
]
}
]

View File

@@ -0,0 +1,45 @@
[
{
"id": "connection-s1",
"ws_url": "wss://s1.ripple.com/",
"jsonrpc_url": "https://s1.ripple.com:51234/",
"shortname": "Mainnet s1",
"longname": "s1.ripple.com (Mainnet Public Cluster)"
},
{
"id": "connection-xrplcluster",
"ws_url": "wss://xrplcluster.com/",
"jsonrpc_url": "https://xrplcluster.com/",
"shortname": "Mainnet xrplcluster",
"longname": "xrplcluster.com (Mainnet Full History Cluster)"
},
{
"id": "connection-s2",
"ws_url": "wss://s2.ripple.com/",
"jsonrpc_url": "https://s2.ripple.com:51234/",
"shortname": "Mainnet s2",
"longname": "s2.ripple.com (Mainnet Full History Cluster)"
},
{
"id": "connection-testnet",
"ws_url": "wss://s.altnet.rippletest.net:51233/",
"jsonrpc_url": "https://s.altnet.rippletest.net:51234/",
"shortname": "Testnet",
"longname": "s.altnet.rippletest.net (Testnet Public Cluster)"
},
{
"id": "connection-devnet",
"ws_url": "wss://s.devnet.rippletest.net:51233/",
"jsonrpc_url": "https://s.devnet.rippletest.net:51234/",
"shortname": "Devnet",
"longname": "s.devnet.rippletest.net (Devnet Public Cluster)"
},
{
"id": "connection-localhost",
"ws_url": "ws://localhost:6006/",
"jsonrpc_url": "http://localhost:5005/",
"shortname": "Local server",
"longname":
"localhost:6006 (Local <code>rippled</code> Server on port 6006) <br/>\n <small>(Requires that you <a href=\"install-rippled.html\">run <code>rippled</code></a> on this machine with default WebSocket settings)</small>"
}
]

View File

@@ -0,0 +1,94 @@
import React, { useRef, useState } from 'react';
import { useTranslate } from "@portal/hooks";
import { Connection } from './types';
import { Modal, ModalClipboardBtn, ModalCloseBtn } from '../Modal';
interface PermaLinkButtonProps {
currentBody: any;
selectedConnection: Connection;
}
interface PermaLinkProps extends PermaLinkButtonProps {
closePermalinkModal: any;
}
const PermalinkModal: React.FC<PermaLinkProps> = ({
closePermalinkModal,
currentBody,
selectedConnection
}) => {
const { translate } = useTranslate();
const permalinkRef = useRef(null);
const footer = <>
<ModalClipboardBtn textareaRef={permalinkRef} />
<ModalCloseBtn onClick={closePermalinkModal} />
</>
return (
<Modal
id="wstool-1-permalink"
title={translate("Permalink")}
footer={footer}
onClose={closePermalinkModal}
>
<form>
<div className="form-group">
<label htmlFor="permalink-box-1">
{translate(
"Share the following link to load this page with the currently-loaded inputs:"
)}
</label>
<textarea
id="permalink-box-1"
className="form-control"
ref={permalinkRef}
value={getPermalink(selectedConnection, currentBody)}
onChange={() => {}}
/>
</div>
</form>
</Modal>
);
};
export const PermalinkButton = ({currentBody, selectedConnection}: PermaLinkButtonProps) => {
const [isPermalinkModalVisible, setIsPermalinkModalVisible] = useState(false);
const openPermalinkModal = () => {
setIsPermalinkModalVisible(true);
};
const closePermalinkModal = () => {
setIsPermalinkModalVisible(false);
};
return <>
<button
className="btn btn-outline-secondary permalink"
data-toggle="modal"
data-target="#wstool-1-permalink"
title="Permalink"
onClick={openPermalinkModal}
>
<i className="fa fa-link"></i>
</button>
{isPermalinkModalVisible && (
<PermalinkModal
closePermalinkModal={closePermalinkModal}
currentBody={currentBody}
selectedConnection={selectedConnection}
/>
)}
</>
}
const getPermalink = (selectedConnection, currentBody) => {
const startHref = window.location.origin + window.location.pathname;
const encodedBody = encodeURIComponent(get_compressed_body(currentBody));
const encodedServer = encodeURIComponent(selectedConnection.ws_url);
return `${startHref}?server=${encodedServer}&req=${encodedBody}`;
};
function get_compressed_body(currentBody) {
return currentBody.replace("\n", "").trim();
}

View File

@@ -0,0 +1,54 @@
import React, { Fragment } from 'react';
import { useTranslate } from "@portal/hooks";
import { Link } from "@portal/Link";
import { slugify } from "./slugify";
import { CommandGroup, CommandMethod } from './types';
interface RightSideBarProps {
commandList: CommandGroup[];
currentMethod: CommandMethod;
setCurrentMethod: any;
}
export const RightSideBar: React.FC<RightSideBarProps> = ({
commandList,
currentMethod,
setCurrentMethod,
}) => {
const { translate } = useTranslate();
return (
<div className="command-list-wrapper">
<div className="toc-header">
<h4>{translate("API Methods")}</h4>
</div>
<ul className="command-list" id="command_list">
{commandList.map((list, index) => (
<Fragment key={index}>
<li className="separator">{list.group}</li>
{list.methods.map((method) => (
<li
className={`method${method === currentMethod ? " active" : ""}`}
key={method.name}
>
<Link
to={`resources/dev-tools/websocket-api-tool#${slugify(method.name)}`}
onClick={() => setCurrentMethod(method)}
>
{method.name}&nbsp;
{method.status === "not_enabled" && (
<span
className="status not_enabled"
title="This feature is not currently enabled on the production XRP Ledger."
>
<i className="fa fa-flask"></i>
</span>
)}
</Link>
</li>
))}
</Fragment>
))}
</ul>
</div>
);
};

View File

@@ -0,0 +1,18 @@
export const slugify = (str) => {
str = str.replace(/^\s+|\s+$/g, ""); // trim
str = str.toLowerCase();
// remove accents, swap ñ for n, etc
const from = "àáäâèéëêìíïîòóöôùúüûñç·/,:;";
const to = "aaaaeeeeiiiioooouuuunc-----";
for (let i = 0, l = from.length; i < l; i++) {
str = str.replace(new RegExp(from.charAt(i), "g"), to.charAt(i));
}
str = str
.replace(/[^a-z0-9 _-]/g, "") // remove invalid chars
.replace(/\s+/g, "-") // collapse whitespace and replace by -
.replace(/-+/g, "-"); // collapse dashes
return str;
};

View File

@@ -0,0 +1,21 @@
export interface CommandMethod {
name: string
description: string,
link: string
body: any
ws_only?: boolean,
status?: 'not_enabled'
}
export interface CommandGroup {
group: string
methods: CommandMethod[]
}
export interface Connection {
id: string
ws_url: string
jsonrpc_url: string
shortname: string
longname: string
}