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:
Caleb Kniffen
2023-12-19 17:46:00 -06:00
committed by mDuo13
parent e7978ae247
commit 5a9b40e8c8
17 changed files with 1512 additions and 4 deletions

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@ __pycache__
out/
yarn-error.log
/.idea
*.iml
.venv/
# PHP

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -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
View File

@@ -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",

View File

@@ -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": {

View File

@@ -7,8 +7,6 @@
.response-metadata .timestamp {
color: $gray-600;
position: relative;
top: 16px;
}
.throbber {