mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2025-11-05 04:15:50 +00:00
Migrate WebSocket Tool to Redocly
Recreate branch from base, add react-query-params, fix permalinks, fix sidebar use correct params library and upgrade redocly. Fix command text not working with permalink and move more modal logic out of main component. Moved more connection selection logic to connection modal Removed many `data-*` attributes previously used by bootstrap modal css Created a shared modal component which removed 38 lines. WS Tool: Fix Link import fix UL error toggle CurlModal to show/hide on button clicks resolve error: <div> cannot appear as a descendant of <p> remove <span> WS tool: sidebar fixes
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@ __pycache__
|
||||
out/
|
||||
yarn-error.log
|
||||
/.idea
|
||||
*.iml
|
||||
.venv/
|
||||
|
||||
# PHP
|
||||
|
||||
9
content/resources/dev-tools/components/Loader.tsx
Normal file
9
content/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)")} />
|
||||
|
||||
}
|
||||
81
content/resources/dev-tools/components/Modal.tsx
Normal file
81
content/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);
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
363
content/resources/dev-tools/websocket-api-tool.page.tsx
Normal file
363
content/resources/dev-tools/websocket-api-tool.page.tsx
Normal file
@@ -0,0 +1,363 @@
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useTranslate } from "@portal/hooks";
|
||||
import {
|
||||
JsonParam,
|
||||
StringParam,
|
||||
useQueryParams,
|
||||
withDefault,
|
||||
QueryParamProvider
|
||||
} from "use-query-params"
|
||||
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6';
|
||||
|
||||
import { PermalinkButton } from './components/websocket-api/permalink-modal';
|
||||
import { CurlButton } from './components/websocket-api/curl-modal';
|
||||
import { ConnectionModal } from "./components/websocket-api/connection-modal";
|
||||
|
||||
import { RightSideBar } from "./components/websocket-api/right-sidebar";
|
||||
import { slugify } from "./components/websocket-api/slugify";
|
||||
import { JsonEditor } from '../../shared/editor/json-editor';
|
||||
import { CommandGroup, CommandMethod } from './components/websocket-api/types';
|
||||
|
||||
import commandList from "./components/websocket-api/data/command-list.json";
|
||||
import connections from "./components/websocket-api/data/connections.json";
|
||||
import { Loader } from './components/Loader';
|
||||
|
||||
export function WebsocketApiTool() {
|
||||
|
||||
const [params, setParams] = useQueryParams({
|
||||
server: withDefault(StringParam, null),
|
||||
req: withDefault(JsonParam, null)
|
||||
})
|
||||
|
||||
const { hash: slug } = useLocation();
|
||||
const { translate } = useTranslate();
|
||||
const [isConnectionModalVisible, setIsConnectionModalVisible] =
|
||||
useState(false);
|
||||
const [selectedConnection, setSelectedConnection] = useState((params.server) ? connections.find((connection) => { return connection?.ws_url === params.server }) : connections[0]); const [connected, setConnected] = useState(false);
|
||||
const [connectionError, setConnectionError] = useState(false);
|
||||
const [keepLast, setKeepLast] = useState(50);
|
||||
const [streamPaused, setStreamPaused] = useState(false);
|
||||
const streamPausedRef = useRef(streamPaused);
|
||||
const [wsLoading, setWsLoading] = useState(false);
|
||||
const [sendLoading, setSendLoading] = useState(false);
|
||||
|
||||
const getInitialMethod = (): CommandMethod => {
|
||||
for (const group of (commandList as CommandGroup[])) {
|
||||
for (const method of group.methods) {
|
||||
if (slug.slice(1) === slugify(method.name) || params.req?.command == method.body.command) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
}
|
||||
return commandList[0].methods[0] as CommandMethod;
|
||||
};
|
||||
|
||||
const setMethod = (method: CommandMethod) => {
|
||||
setCurrentMethod(method)
|
||||
setCurrentBody(JSON.stringify(method.body, null, 2))
|
||||
}
|
||||
|
||||
const [currentMethod, setCurrentMethod] = useState<CommandMethod>(getInitialMethod);
|
||||
const [currentBody, setCurrentBody] = useState(
|
||||
JSON.stringify(params.req || currentMethod.body, null, 2)
|
||||
);
|
||||
streamPausedRef.current = streamPaused;
|
||||
|
||||
const handleCurrentBodyChange = (value: any) => {
|
||||
setCurrentBody(value);
|
||||
};
|
||||
|
||||
const handleKeepLastChange = (event) => {
|
||||
const newValue = event.target.value;
|
||||
setKeepLast(newValue);
|
||||
};
|
||||
|
||||
const openConnectionModal = () => {
|
||||
setIsConnectionModalVisible(true);
|
||||
};
|
||||
|
||||
const closeConnectionModal = () => {
|
||||
setIsConnectionModalVisible(false);
|
||||
};
|
||||
|
||||
const [ws, setWs] = useState(null);
|
||||
const [responses, setResponses] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ws && ws.readyState < 2) {
|
||||
ws.close();
|
||||
}
|
||||
const newWs = new WebSocket(selectedConnection.ws_url);
|
||||
setWs(newWs);
|
||||
setWsLoading(true);
|
||||
newWs.onopen = function handleOpen(event) {
|
||||
setConnected(true);
|
||||
setConnectionError(false);
|
||||
setWsLoading(false);
|
||||
};
|
||||
|
||||
newWs.onclose = function handleClose(event) {
|
||||
if (event.wasClean) {
|
||||
setConnected(false);
|
||||
setWsLoading(false);
|
||||
} else {
|
||||
console.debug(
|
||||
"socket close event discarded (new socket status already provided):",
|
||||
event
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
newWs.onerror = function handleError(event) {
|
||||
setConnectionError(true);
|
||||
setWsLoading(false);
|
||||
console.error("socket error:", event);
|
||||
};
|
||||
|
||||
newWs.onmessage = function handleMessage(event) {
|
||||
const message = event.data;
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(message);
|
||||
} catch (error) {
|
||||
console.error("Error parsing validation message", error);
|
||||
return;
|
||||
}
|
||||
if (data.type === "response") {
|
||||
setSendLoading(false);
|
||||
}
|
||||
if (data.type === "response" || !streamPausedRef.current) {
|
||||
setResponses((prevResponses) =>
|
||||
[JSON.stringify(data, null, 2)].concat(prevResponses)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
newWs.close();
|
||||
};
|
||||
}, [selectedConnection.ws_url]);
|
||||
|
||||
useEffect(() => {
|
||||
if (responses.length > keepLast) {
|
||||
setResponses(responses.slice(0, keepLast));
|
||||
}
|
||||
}, [responses, keepLast]);
|
||||
|
||||
const sendWebSocketMessage = (messageBody) => {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
alert("Can't send request: Must be connected first!");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JSON.parse(messageBody); // we only need the text version, but test JSON syntax
|
||||
} catch (e) {
|
||||
alert("Invalid request JSON");
|
||||
return;
|
||||
}
|
||||
|
||||
setSendLoading(true);
|
||||
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(messageBody);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container-fluid" role="document" id="main_content_wrapper">
|
||||
<div className="row">
|
||||
<aside
|
||||
className="right-sidebar col-lg-3 order-lg-4"
|
||||
role="complementary"
|
||||
>
|
||||
<RightSideBar
|
||||
commandList={commandList}
|
||||
currentMethod={currentMethod}
|
||||
setCurrentMethod={setMethod}
|
||||
/>
|
||||
</aside>
|
||||
<main
|
||||
className="main col-lg-9"
|
||||
role="main"
|
||||
id="main_content_body"
|
||||
>
|
||||
<section
|
||||
className="container-fluid pt-3 p-md-3 websocket-tool"
|
||||
id="wstool-1"
|
||||
>
|
||||
<h1>{translate("WebSocket Tool")}</h1>
|
||||
<div className="api-method-description-wrapper">
|
||||
<h3>
|
||||
<a
|
||||
href={`${currentMethod.name.split(" ")[0]}.html`}
|
||||
className="selected_command"
|
||||
>
|
||||
{currentMethod.name}
|
||||
</a>
|
||||
</h3>
|
||||
{currentMethod.description && (
|
||||
<p
|
||||
className="blurb"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: currentMethod.description,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{currentMethod.link && (
|
||||
<a
|
||||
className="btn btn-outline-secondary api-readmore"
|
||||
href={currentMethod.link}
|
||||
>
|
||||
{translate("Read more")}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="api-input-area pt-4">
|
||||
<h4>{translate("Request")}</h4>
|
||||
<div className="request-body">
|
||||
<JsonEditor
|
||||
value={currentBody}
|
||||
onChange={handleCurrentBodyChange}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="btn-toolbar justify-content-between pt-4"
|
||||
role="toolbar"
|
||||
>
|
||||
<div className="btn-group mr-3" role="group">
|
||||
<button
|
||||
className="btn btn-outline-secondary send-request"
|
||||
onClick={() => sendWebSocketMessage(currentBody)}
|
||||
>
|
||||
{translate("Send request")}
|
||||
</button>
|
||||
{sendLoading && (
|
||||
<div className="input-group loader send-loader">
|
||||
<span className="input-group-append">
|
||||
<Loader />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="btn-group request-options" role="group">
|
||||
<button
|
||||
className={`btn connection ${
|
||||
connected ? "btn-success" : "btn-outline-secondary"
|
||||
} ${connectionError ?? "btn-danger"}`}
|
||||
onClick={openConnectionModal}
|
||||
data-toggle="modal"
|
||||
data-target="#wstool-1-connection-settings"
|
||||
>
|
||||
{`${selectedConnection.shortname}${
|
||||
connected ? " (Connected)" : " (Not Connected)"
|
||||
}${connectionError ? " (Failed to Connect)" : ""}`}
|
||||
</button>
|
||||
{isConnectionModalVisible && (
|
||||
<ConnectionModal
|
||||
selectedConnection={selectedConnection}
|
||||
setSelectedConnection={setSelectedConnection}
|
||||
closeConnectionModal={closeConnectionModal}
|
||||
connections={connections}
|
||||
/>
|
||||
)}
|
||||
{wsLoading && (
|
||||
<div className="input-group loader connect-loader">
|
||||
<span className="input-group-append">
|
||||
<Loader />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<PermalinkButton
|
||||
currentBody={currentBody}
|
||||
selectedConnection={selectedConnection}
|
||||
/>
|
||||
{!currentMethod.ws_only &&
|
||||
(<CurlButton currentBody={currentBody} selectedConnection={selectedConnection}/>)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="api-response-area pt-4">
|
||||
<h4>{translate("Responses")}</h4>
|
||||
|
||||
<div
|
||||
className="btn-toolbar justify-content-between response-options"
|
||||
role="toolbar"
|
||||
>
|
||||
<div className="input-group">
|
||||
<div className="input-group-prepend">
|
||||
<div
|
||||
className="input-group-text"
|
||||
id="wstool-1-keep-last-label"
|
||||
>
|
||||
{translate("Keep last:")}
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
value={keepLast}
|
||||
min="1"
|
||||
aria-label="Number of responses to keep at once"
|
||||
aria-describedby="wstool-1-keep-last-label"
|
||||
className="form-control keep-last"
|
||||
onChange={handleKeepLastChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="btn-group" role="group">
|
||||
{!streamPaused && (
|
||||
<button
|
||||
className="btn btn-outline-secondary stream-pause"
|
||||
title="Pause Subscriptions"
|
||||
onClick={() => setStreamPaused(true)}
|
||||
>
|
||||
<i className="fa fa-pause"></i>
|
||||
</button>
|
||||
)}
|
||||
{streamPaused && (
|
||||
<button
|
||||
className="btn btn-outline-secondary stream-unpause"
|
||||
title="Unpause Subscriptions"
|
||||
onClick={() => setStreamPaused(false)}
|
||||
>
|
||||
<i className="fa fa-play"></i>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-outline-secondary wipe-responses"
|
||||
title="Delete All Responses"
|
||||
onClick={() => setResponses([])}
|
||||
>
|
||||
<i className="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="response-body-wrapper">
|
||||
{responses.map((response, i) => (
|
||||
<div className="response-metadata" key={response.id + '_' + i}>
|
||||
<span className="timestamp">
|
||||
{new Date().toISOString()}
|
||||
</span>
|
||||
<div className="response-json">
|
||||
<JsonEditor value={response} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return <QueryParamProvider adapter={ReactRouter6Adapter}>
|
||||
<WebsocketApiTool />
|
||||
</QueryParamProvider>
|
||||
}
|
||||
@@ -633,7 +633,7 @@
|
||||
- label: RPC Tool
|
||||
page: resources/dev-tools/rpc-tool.page.tsx
|
||||
- label: WebSocket API Tool
|
||||
- label: ripple.txt Validator
|
||||
page: resources/dev-tools/websocket-api-tool.page.tsx
|
||||
- label: xrp-ledger.toml Checker
|
||||
page: resources/dev-tools/xrp-ledger-toml-checker.page.tsx
|
||||
- label: Domain Verification Checker
|
||||
|
||||
File diff suppressed because one or more lines are too long
28
package-lock.json
generated
28
package-lock.json
generated
@@ -23,6 +23,7 @@
|
||||
"react-alert": "^7.0.3",
|
||||
"react18-json-view": "^0.2.6",
|
||||
"smol-toml": "^1.1.3",
|
||||
"use-query-params": "^2.2.1",
|
||||
"xrpl": "^3.0.0-beta.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -9536,6 +9537,11 @@
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/serialize-query-params": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/serialize-query-params/-/serialize-query-params-2.0.2.tgz",
|
||||
"integrity": "sha512-1chMo1dST4pFA9RDXAtF0Rbjaut4is7bzFbI1Z26IuMub68pNCILku85aYmeFhvnY//BXUPUhoRMjYcsT93J/Q=="
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
|
||||
@@ -10641,6 +10647,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-query-params": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/use-query-params/-/use-query-params-2.2.1.tgz",
|
||||
"integrity": "sha512-i6alcyLB8w9i3ZK3caNftdb+UnbfBRNPDnc89CNQWkGRmDrm/gfydHvMBfVsQJRq3NoHOM2dt/ceBWG2397v1Q==",
|
||||
"dependencies": {
|
||||
"serialize-query-params": "^2.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@reach/router": "^1.2.1",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
"react-router-dom": ">=5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@reach/router": {
|
||||
"optional": true
|
||||
},
|
||||
"react-router-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"react-alert": "^7.0.3",
|
||||
"react18-json-view": "^0.2.6",
|
||||
"smol-toml": "^1.1.3",
|
||||
"use-query-params": "^2.2.1",
|
||||
"xrpl": "^3.0.0-beta.1"
|
||||
},
|
||||
"overrides": {
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
|
||||
.response-metadata .timestamp {
|
||||
color: $gray-600;
|
||||
position: relative;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
.throbber {
|
||||
|
||||
Reference in New Issue
Block a user