mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2025-12-02 09:35:53 +00:00
Re-level non-docs content to top of repo and rename content→docs
This commit is contained in:
43
resources/dev-tools/components/AlertTemplate.tsx
Normal file
43
resources/dev-tools/components/AlertTemplate.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
56
resources/dev-tools/components/DestinationAddressInput.tsx
Normal file
56
resources/dev-tools/components/DestinationAddressInput.tsx
Normal 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>)
|
||||
}
|
||||
199
resources/dev-tools/components/InitButton.tsx
Normal file
199
resources/dev-tools/components/InitButton.tsx
Normal 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> <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>)
|
||||
}
|
||||
9
resources/dev-tools/components/Loader.tsx
Normal file
9
resources/dev-tools/components/Loader.tsx
Normal 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)")} />
|
||||
|
||||
}
|
||||
77
resources/dev-tools/components/LogEntry.tsx
Normal file
77
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>
|
||||
)
|
||||
}
|
||||
81
resources/dev-tools/components/Modal.tsx
Normal file
81
resources/dev-tools/components/Modal.tsx
Normal 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">×</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);
|
||||
}
|
||||
};
|
||||
44
resources/dev-tools/components/StatusSidebar.tsx
Normal file
44
resources/dev-tools/components/StatusSidebar.tsx
Normal 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>)
|
||||
}
|
||||
77
resources/dev-tools/components/TextLookupForm.tsx
Normal file
77
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>)
|
||||
}
|
||||
156
resources/dev-tools/components/TransactionButton.tsx
Normal file
156
resources/dev-tools/components/TransactionButton.tsx
Normal 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' : ''}}>
|
||||
|
||||
</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>)
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
93
resources/dev-tools/components/websocket-api/curl-modal.tsx
Normal file
93
resources/dev-tools/components/websocket-api/curl-modal.tsx
Normal 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}
|
||||
/>}
|
||||
</>
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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>"
|
||||
}
|
||||
]
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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}
|
||||
{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>
|
||||
);
|
||||
};
|
||||
18
resources/dev-tools/components/websocket-api/slugify.ts
Normal file
18
resources/dev-tools/components/websocket-api/slugify.ts
Normal 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;
|
||||
};
|
||||
21
resources/dev-tools/components/websocket-api/types.ts
Normal file
21
resources/dev-tools/components/websocket-api/types.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user