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

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

View File

@@ -0,0 +1,124 @@
import * as React from 'react';
import { usePageSharedData, useTranslate } from '@portal/hooks';
export const frontmatter = {
seo: {
title: 'Code Samples',
description: "Browse sample code for building common use cases on the XRP Ledger.",
}
};
const langIcons = {
cli: require('../static/img/logos/cli.svg'),
go: require('../static/img/logos/golang.svg'),
http: require('../static/img/logos/globe.svg'),
java: require('../static/img/logos/java.svg'),
js: require('../static/img/logos/javascript.svg'),
php: require('../static/img/logos/php.svg'),
py: require('../static/img/logos/python.svg'),
ts: require('../static/img/logos/typescript.svg'),
};
const target = {
github_forkurl: 'https://github.com/XRPLF/xrpl-dev-portal',
github_branch: 'master',
};
export default function CodeSamples() {
const { translate } = useTranslate();
const { codeSamples, langs } = usePageSharedData<any>('code-samples');
return (
<div className="landing page-community">
<div className="">
<section className="py-26">
<div className="col-lg-8 mx-auto text-lg-center">
<div className="d-flex flex-column-reverse">
<h1 className="mb-0">{translate('Start Building with Example Code')}</h1>
<h6 className="eyebrow mb-3">{translate('Code Samples')}</h6>
</div>
<a className="mt-12 btn btn-primary btn-arrow">Submit Code Samples</a>
</div>
</section>
<div className="position-relative d-none-sm">
<img
alt="default-alt-text"
src={require('../img/backgrounds/xrpl-overview-orange.svg')}
id="xrpl-overview-orange"
/>
</div>
<section className="container-new py-26">
<div className="d-flex flex-column col-sm-8 p-0">
<h3 className="h4 h2-sm">
{translate('Browse sample code for building common use cases on the XRP Ledger')}
</h3>
</div>
<div className="row col-12 card-deck mt-10" id="code-samples-deck">
<div className="row col-md-12 px-0" id="code_samples_list">
{codeSamples.map(card => (
<a
className={`card cardtest col-12 col-lg-5 ${card.langs.join(' ')}`}
href={target.github_forkurl + `/tree/${target.github_branch}/${card.href}`}
>
<div className="card-header">
{card.langs.map(lang => (
<span className="circled-logo">
<img alt={lang} src={langIcons[lang]} />
</span>
))}
</div>
<div className="card-body">
<h4 className="card-title h5">{card.title}</h4>
<p className="card-text">{card.description}</p>
</div>
<div className="card-footer">&nbsp;</div>
</a>
))}
</div>
</div>
</section>
<section className="container-new py-26">
<div>
<div className="d-flex flex-column">
<h3 className="h4 h2-sm pb-4">{translate('Contribute Code Samples')}</h3>
<h6 className="eyebrow mb-20">
{translate('Help the XRPL community by submitting your<br /> own code samples')}
</h6>
</div>
<div className="row pl-4">
<div className=" col-lg-3 pl-4 pl-lg-0 pr-4 contribute dot contribute_1">
<span className="dot" />
<h5 className="pb-4 pt-md-5">Fork and clone</h5>
<p className="pb-4">
Fork the <a href="https://github.com/XRPLF/xrpl-dev-portal">xrpl-dev-portal repo</a>. Using git, clone
the fork to your computer.
</p>
</div>
<div className=" col-lg-3 pl-4 pl-lg-0 pr-4 contribute dot contribute_2">
<span className="dot" />
<h5 className="pb-4 pt-md-5">Add to folder</h5>
<p className="pb-4">
Add your sample code to the <code>content/_code-samples/</code> folder. Be sure to include a{' '}
<code>README.md</code> that summarizes what it does and anything else people should know about it.
</p>
</div>
<div className=" col-lg-3 pl-4 pl-lg-0 pr-4 contribute dot contribute_3">
<span className="dot" />
<h5 className="pb-4 pt-md-5">Commit and push</h5>
<p className="pb-4">Commit your changes and push them to your fork on GitHub.</p>
</div>
<div className=" col-lg-3 pl-4 pl-lg-0 pr-2 contribute dot contribute_4 mb-4">
<span className="dot" />
<h5 className="pb-4 pt-md-5">Open a pull request</h5>
<p className="pb-0 mb-0">
Open a pull request to the original repo. Maintainers will review your submission and suggest changes
if necessary. If the code sample is helpful, it'll be merged and added to XRPL.org!
</p>
</div>
</div>
<a className="mt-12 btn btn-primary btn-arrow">Submit Code Samples</a>
</div>
</section>
</div>
</div>
);
}

View File

@@ -0,0 +1,95 @@
---
html: contribute-code.html
parent: resources.html
seo:
description: Learn how features can be coded into the XRP Ledger protocol.
labels:
- Blockchain
---
# Contribute Code
The software that powers the XRP Ledger is open source. Anyone can download, modify, extend, or explore it. If you want to contribute code, it's important to work with the community to define the specifications of your changes and test the code before it becomes a part of the XRP Ledger protocol and blockchain.
## Core Server Source
The software that powers the XRP Ledger is open-source, so anyone can download, modify, extend, or explore it. Community involvement makes it better. Look for "[Source]" links in the [documentation](/docs/) to jump directly into the related source code, or browse the source code on GitHub:
| XRP Ledger Source Code | |
|:-----------------------|:----------------------------------------------------|
| Repository | <https://github.com/XRPLF/rippled> |
| License | [Multiple; ISC (permissive)](https://github.com/XRPLF/rippled/blob/develop/LICENSE.md) |
| Programming Language | C++ |
If you're not sure where to start, Dev Null Productions provides a detailed and thorough [**Source Code Guide**](https://xrpintel.com/source) that describes the structure and functions of the core XRP Ledger server (`rippled`) implementation.
## XRP Ledger Standards
Changes to `rippled` are tracked by an XRP Ledger Standard (XLS), a document that identifies and details the specifications of a change. Before committing to development, you must start a discussion in the [XRPL-Standards repo](https://github.com/XRPLF/XRPL-Standards/discussions). This provides the community a chance to discuss and provide feedback about your change.
**Note:** Bug fixes don't require an XLS, but may require an amendment.
Creating an XLS has its own process, but can be summarized as:
1. Start a discussion and gather feedback.
2. Create an XLS draft in the standards repo.
3. Publishing the XLS draft as a Candidate Specification.
For details, see the [XLS contributing guide](https://github.com/XRPLF/XRPL-Standards/blob/master/CONTRIBUTING.md).
## Amendment Implementation
After you've created an XLS draft, you now need to determine if your change requires an amendment. Changes that affect **transaction processing** require amendments, specifically changes that:
- Modify ledger rules, resulting in different outcomes.
- Add or remove transactions.
- Affect consensus.
**Note:** If your change doesn't need an amendment, you can go straight to coding and deployment.
Implementing code as an amendment requires you to add the amendment to these files:
- **Feature.cpp**:
`Supported` parameter should be set to `no` until development is complete.
`DefaultVote` parameter should be set to `yes` for bug fixes; everything else defaults to `no`.
- **Feature.h**: Increment the `numFeatures` counter and declare an `extern uint256 const` variable.
## Coding and Deployment
The general development path breaks down as follows:
1. Create a fork or branch in the [`rippled` repository](https://github.com/XRPLF/rippled) to develop your code.
**Tip:** If you're not sure where to start, _Dev Null Productions_ provides a detailed and thorough [`rippled` Source Code Guide](https://xrpintel.com/source).
2. Run unit and integration tests. Running a server in _stand-alone mode_ is useful for testing your changes in an isolated environment, but you may want to stand up a private network for extensive changes.
3. Create a pull request on `XRPLF:develop`.
**Note for Amendments:** Update the `Supported` paramter to `yes` in **Feature.cpp**.
4. After the pull request is approved by XRP Ledger maintainers, your code is merged into `develop` and additional testing can be done on Devnet.
**Note for Amendments:**
- The `DefaultVote` parameter is now locked.
- If problems are found with the amendment, you must restart the process of making fixes and submitting a new PR. You can change the default vote in the new PR.
5. On a quarterly basis, a release candidate is built from approved PRs on `develop`. The package is deployed to Testnet and a few nodes on Mainnet. If no issues are found with the release candidate, the code is merged into `master` and nodes on Mainnet can upgrade to this build.
6. New amendments go through the consensus process and validators vote on whether to enable them.
## Code Flowchart
![Code Flowchart](/img/Contribute Code Flowchart.png)
## See Also
- **Concepts:**
- [Amendments](../../concepts/networks-and-servers/amendments.md)

View File

@@ -0,0 +1,392 @@
---
html: create-custom-transactors.html
parent: contribute-code.html
seo:
description: Create custom transactors to interact with the XRP Ledger.
labels:
- Development
- Blockchain
---
# Create Custom Transactors
A _transactor_ is code that processes a transaction and modifies the XRP Ledger. Creating custom transactors enables you to add new functionality to `rippled`. This tutorial walks through coding transactors, but you'll have to go through the amendment process to add it to XRPL. See: [Contribute Code to the XRP Ledger](contribute-code.md).
Transactors follow a basic order of operations:
1. Access a _view_ into a serialized type ledger entry (SLE).
2. Update, erase, or insert values in the _view_.
3. Apply the finalized changes from the _view_ to the ledger.
**Note:** _Views_ are sandboxes into ledgers. Transactors make all necessary error checks and changes in sandboxes, not directly on the ledger. After values are finalized, changes are applied atomically to the ledger.
This tutorial uses the existing `CreateCheck` transactor as an example. You can view the source files here:
- [Header File](https://github.com/XRPLF/rippled/blob/master/src/ripple/app/tx/impl/CreateCheck.h)
- [CPP File](https://github.com/XRPLF/rippled/blob/master/src/ripple/app/tx/impl/CreateCheck.cpp)
## Header File
Create a header file in this format:
```c++
namespace ripple {
class CreateCheck : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit CreateCheck(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace ripple
```
Initializing the transactor with `ApplyContext` gives it access to:
- The transaction that triggered the transactor.
- A view of the SLE.
- A journal to log errors.
## CPP File
### 1. Add a `preflight` function.
The `preflight` function checks for errors in the transaction itself before accessing the ledger. It should reject invalid and incorrectly formed transactions.
- `PreflightContext` doesn't have a view of the ledger.
- Use bracket notation to retrieve fields from ledgers and transactions:
```
auto const curExpiration = (*sle*)[~sfExpiration];
(*sle)[sfBalance] = (*sle)[sfBalance] + reqDelta;
```
**Note:** The `~` symbol returns an optional type.
- You can view ledger and transaction schemas here:
- [`LedgerFormats.cpp`](https://github.com/XRPLF/rippled/blob/master/src/ripple/protocol/impl/LedgerFormats.cpp)
- [`TxFormats.cpp`](https://github.com/XRPLF/rippled/blob/master/src/ripple/protocol/impl/TxFormats.cpp)
- `rippled` summarizes transaction results with result codes. See: [Transaction Results](../../references/protocol/transactions/transaction-results/transaction-results.md)
```c++
CreateCheck::preflight(PreflightContext const& ctx)
{
// Check if this amendment functionality is enabled on the network.
if (!ctx.rules.enabled(featureChecks))
return temDISABLED;
NotTEC const ret{preflight1(ctx)};
if (!isTesSuccess(ret))
return ret;
if (ctx.tx.getFlags() & tfUniversalMask)
{
// There are no flags (other than universal) for CreateCheck yet.
JLOG(ctx.j.warn()) << "Malformed transaction: Invalid flags set.";
return temINVALID_FLAG;
}
if (ctx.tx[sfAccount] == ctx.tx[sfDestination])
{
// They wrote a check to themselves.
JLOG(ctx.j.warn()) << "Malformed transaction: Check to self.";
return temREDUNDANT;
}
{
STAmount const sendMax{ctx.tx.getFieldAmount(sfSendMax)};
if (!isLegalNet(sendMax) || sendMax.signum() <= 0)
{
JLOG(ctx.j.warn()) << "Malformed transaction: bad sendMax amount: "
<< sendMax.getFullText();
return temBAD_AMOUNT;
}
if (badCurrency() == sendMax.getCurrency())
{
JLOG(ctx.j.warn()) << "Malformed transaction: Bad currency.";
return temBAD_CURRENCY;
}
}
if (auto const optExpiry = ctx.tx[~sfExpiration])
{
if (*optExpiry == 0)
{
JLOG(ctx.j.warn()) << "Malformed transaction: bad expiration";
return temBAD_EXPIRATION;
}
}
return preflight2(ctx);
}
```
### 2. Add a `preclaim` function.
The `preclaim` function checks for errors that require viewing information on the current ledger.
- If this step returns a result code of `tesSUCCESS` or any `tec` result, the transaction will be queued and broadcast to peers.
```c++
CreateCheck::preclaim(PreclaimContext const& ctx)
{
AccountID const dstId{ctx.tx[sfDestination]};
// Use the `keylet` function to get the key of the SLE. Views have either `read` or `peek` access.
// `peek` access allows the developer to modify the SLE returned.
auto const sleDst = ctx.view.read(keylet::account(dstId));
if (!sleDst)
{
JLOG(ctx.j.warn()) << "Destination account does not exist.";
return tecNO_DST;
}
auto const flags = sleDst->getFlags();
// Check if the destination has disallowed incoming checks
if (ctx.view.rules().enabled(featureDisallowIncoming) &&
(flags & lsfDisallowIncomingCheck))
return tecNO_PERMISSION;
if ((flags & lsfRequireDestTag) && !ctx.tx.isFieldPresent(sfDestinationTag))
{
// The tag is basically account-specific information we don't
// understand, but we can require someone to fill it in.
JLOG(ctx.j.warn()) << "Malformed transaction: DestinationTag required.";
return tecDST_TAG_NEEDED;
}
{
STAmount const sendMax{ctx.tx[sfSendMax]};
if (!sendMax.native())
{
// The currency may not be globally frozen
AccountID const& issuerId{sendMax.getIssuer()};
if (isGlobalFrozen(ctx.view, issuerId))
{
JLOG(ctx.j.warn()) << "Creating a check for frozen asset";
return tecFROZEN;
}
// If this account has a trustline for the currency, that
// trustline may not be frozen.
//
// Note that we DO allow create check for a currency that the
// account does not yet have a trustline to.
AccountID const srcId{ctx.tx.getAccountID(sfAccount)};
if (issuerId != srcId)
{
// Check if the issuer froze the line
auto const sleTrust = ctx.view.read(
keylet::line(srcId, issuerId, sendMax.getCurrency()));
if (sleTrust &&
sleTrust->isFlag(
(issuerId > srcId) ? lsfHighFreeze : lsfLowFreeze))
{
JLOG(ctx.j.warn())
<< "Creating a check for frozen trustline.";
return tecFROZEN;
}
}
if (issuerId != dstId)
{
// Check if dst froze the line.
auto const sleTrust = ctx.view.read(
keylet::line(issuerId, dstId, sendMax.getCurrency()));
if (sleTrust &&
sleTrust->isFlag(
(dstId > issuerId) ? lsfHighFreeze : lsfLowFreeze))
{
JLOG(ctx.j.warn())
<< "Creating a check for destination frozen trustline.";
return tecFROZEN;
}
}
}
}
if (hasExpired(ctx.view, ctx.tx[~sfExpiration]))
{
JLOG(ctx.j.warn()) << "Creating a check that has already expired.";
return tecEXPIRED;
}
return tesSUCCESS;
}
```
### 3. Add a `doApply()` function.
The `doApply()` function has read/write access, enabling you to modify the ledger.
```c++
CreateCheck::doApply()
{
auto const sle = view().peek(keylet::account(account_));
if (!sle)
return tefINTERNAL;
// A check counts against the reserve of the issuing account, but we
// check the starting balance because we want to allow dipping into the
// reserve to pay fees.
{
STAmount const reserve{
view().fees().accountReserve(sle->getFieldU32(sfOwnerCount) + 1)};
if (mPriorBalance < reserve)
return tecINSUFFICIENT_RESERVE;
}
// Note that we use the value from the sequence or ticket as the
// Check sequence. For more explanation see comments in SeqProxy.h.
std::uint32_t const seq = ctx_.tx.getSeqProxy().value();
Keylet const checkKeylet = keylet::check(account_, seq);
auto sleCheck = std::make_shared<SLE>(checkKeylet);
sleCheck->setAccountID(sfAccount, account_);
AccountID const dstAccountId = ctx_.tx[sfDestination];
sleCheck->setAccountID(sfDestination, dstAccountId);
sleCheck->setFieldU32(sfSequence, seq);
sleCheck->setFieldAmount(sfSendMax, ctx_.tx[sfSendMax]);
if (auto const srcTag = ctx_.tx[~sfSourceTag])
sleCheck->setFieldU32(sfSourceTag, *srcTag);
if (auto const dstTag = ctx_.tx[~sfDestinationTag])
sleCheck->setFieldU32(sfDestinationTag, *dstTag);
if (auto const invoiceId = ctx_.tx[~sfInvoiceID])
sleCheck->setFieldH256(sfInvoiceID, *invoiceId);
if (auto const expiry = ctx_.tx[~sfExpiration])
sleCheck->setFieldU32(sfExpiration, *expiry);
view().insert(sleCheck);
auto viewJ = ctx_.app.journal("View");
// If it's not a self-send (and it shouldn't be), add Check to the
// destination's owner directory.
if (dstAccountId != account_)
{
auto const page = view().dirInsert(
keylet::ownerDir(dstAccountId),
checkKeylet,
describeOwnerDir(dstAccountId));
JLOG(j_.trace()) << "Adding Check to destination directory "
<< to_string(checkKeylet.key) << ": "
<< (page ? "success" : "failure");
if (!page)
return tecDIR_FULL;
sleCheck->setFieldU64(sfDestinationNode, *page);
}
{
auto const page = view().dirInsert(
keylet::ownerDir(account_),
checkKeylet,
describeOwnerDir(account_));
JLOG(j_.trace()) << "Adding Check to owner directory "
<< to_string(checkKeylet.key) << ": "
<< (page ? "success" : "failure");
if (!page)
return tecDIR_FULL;
sleCheck->setFieldU64(sfOwnerNode, *page);
}
// If we succeeded, the new entry counts against the creator's reserve.
adjustOwnerCount(view(), sle, 1, viewJ);
return tesSUCCESS;
}
```
## Additional Functions
You can add more helper functions to your custom transactor as necessary. There are a few special functions that are relevant in special cases.
### `calculateBaseFee`
Most transactions inherit the default [reference transaction cost](../../concepts/transactions/transaction-cost.md). However, if your transactor needs to define a non-standard transaction cost, you can replace the transactor's `calculateBaseFee` method with a custom one.
The following example shows how `EscrowFinish` transactions charge an additional cost on conditional escrows based on the size of the fulfillment:
```c++
XRPAmount
EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx)
{
XRPAmount extraFee{0};
if (auto const fb = tx[~sfFulfillment])
{
extraFee += view.fees().base * (32 + (fb->size() / 16));
}
return Transactor::calculateBaseFee(view, tx) + extraFee;
}
```
### `makeTxConsequences`
`rippled` uses a [`TxConsequences`](https://github.com/XRPLF/rippled/blob/master/src/ripple/app/tx/applySteps.h#L41-L44) class to describe the outcome to an account when applying a transaction. It tracks the fee, maximum possible XRP spent, and how many sequence numbers are consumed by the transaction. There are three types of consequences:
- **Normal:** The transactor doesn't affect transaction signing and _only_ consumes an XRP fee. Transactions that spend XRP beyond the fee aren't considered normal.
- **Blocker:** The transactor affects transaction signing, preventing valid transactions from queueing behind it.
- **Custom:** The transactor needs to do additional work to determination consequences.
The `makeTxConsequences` function enables you to create custom consequences for situations such as:
- Payments sending XRP.
- Tickets consuming more than one sequence number.
- Transactions that are normal or blockers, depending on flags or fields set.
**Note:** `TxConsequences` only affects the [transaction queue](../../concepts/transactions/transaction-queue.md). If a transaction is likely to claim a fee when applied to the ledger, it will be broadcast to peers. If it's not likely to claim a fee, or that can't be determined, it won't be broadcast.
```c++
SetAccount::makeTxConsequences(PreflightContext const& ctx)
{
// The SetAccount may be a blocker, but only if it sets or clears
// specific account flags.
auto getTxConsequencesCategory = [](STTx const& tx) {
if (std::uint32_t const uTxFlags = tx.getFlags();
uTxFlags & (tfRequireAuth | tfOptionalAuth))
return TxConsequences::blocker;
if (auto const uSetFlag = tx[~sfSetFlag]; uSetFlag &&
(*uSetFlag == asfRequireAuth || *uSetFlag == asfDisableMaster ||
*uSetFlag == asfAccountTxnID))
return TxConsequences::blocker;
if (auto const uClearFlag = tx[~sfClearFlag]; uClearFlag &&
(*uClearFlag == asfRequireAuth || *uClearFlag == asfDisableMaster ||
*uClearFlag == asfAccountTxnID))
return TxConsequences::blocker;
return TxConsequences::normal;
};
return TxConsequences{ctx.tx, getTxConsequencesCategory(ctx.tx)};
}
```
## Next Steps
Re-compile the server with your new transactor and test it in [stand-alone mode](../../infrastructure/testing-and-auditing/index.md). If you coded the transactor behind an amendment, you can [force-enable](../../infrastructure/testing-and-auditing/test-amendments.md) the feature using the config file.

View File

@@ -0,0 +1,36 @@
---
html: creating-diagrams.html
parent: contribute-documentation.html
seo:
description: Create diagrams that interact properly with light and dark mode settings.
---
# Creating Diagrams
The site contains code to automatically recolor SVG diagrams for light and dark mode. This is more than just inverting images. The recoloring keeps gradients going the same direction (so that things don't look bottom-lit) and replaces colors with equivalents that fit with the theme rather than their inverse. For example, "Ripple blue" gets recolored to XRPL green, not its inverse orange. Example:
![Comparison of invert and theme-aware recoloring](/img/theme-aware-recolor.png)
Theme-aware recoloring uses a single source file in SVG format for diagrams, and produces diagrams that are recolored to match the current theme (light/dark) using CSS. If the user changes their theme, the diagrams immediately change to match it.
To include a theme-aware diagram in a document, use the `include_svg` filter with syntax such as the following:
```jinja
[{% inline-svg file="/img/anatomy-of-a-ledger-complete.svg" /%}](/img/anatomy-of-a-ledger-complete.svg "Figure 1: XRP Ledger Elements")
```
Leave empty lines before and after this syntax. The SVG file in question should be in the [`img/`](https://github.com/XRPLF/xrpl-dev-portal/tree/master/img) folder at the top level of the repo, or a subfolder of it. The second argument is _title text_, which appears when the user hovers their mouse over the diagram, and can also be used by other software (such as screen readers) to caption the diagram.
The resulting SVG file is inlined directly into the Markdown file. One limitation is that you can't use it inside other Markdown structures such as bulleted lists or tables.
{% admonition type="info" name="Note" %}
The filter source code is [`tool/filter_include_svg.py`](https://github.com/XRPLF/xrpl-dev-portal/blob/master/tool/filter_include_svg.py). This is also the reason that `lxml` is one of the dependencies for building the site.
{% /admonition %}
## Making Diagrams
You have to take care when creating diagrams so that the recoloring applies correctly; otherwise, some elements might be invisible (white-on-white or black-on-black, for example) when recolored for the other theme. The theme-aware diagrams code supports diagrams created using either [Umlet](https://www.umlet.com/) or [Google Draw](https://docs.google.com/drawings/) and exported as SVG. Additionally, you should follow these guidelines when making diagrams:
- Create diagrams for light mode by default. Use a transparent background color.
- Only use colors that the theme-aware diagrams code has mappings for. The code for this, including the full list of colors, is in [`styles/_diagrams.scss`](https://github.com/XRPLF/xrpl-dev-portal/blob/master/styles/_diagrams.scss). If needed, you can add new colors by extending the SCSS code. (Don't forget to re-export the CSS when you're done. See the [styles README](https://github.com/XRPLF/xrpl-dev-portal/blob/master/styles/README.md).)
- Use actual vector shapes instead of embedded icons/images whenever possible. If you need to put text on top of an image, add a solid background to the text element and use one of the colors the theme has mappings for.
- Don't layer transparent elements containing text on top of elements with different background colors. Apply a background color directly to the element that contains the text.

View File

@@ -0,0 +1,81 @@
---
html: documentation-translations.html
parent: contribute-documentation.html
seo:
description: Learn how to contribute and maintain translations of the documentation on this website.
---
# Translations
The XRP Ledger Dev Portal is mostly written in English, so the English version is generally the most up-to-date and accurate version. However, to broaden the reach of the XRP Ledger software and community, this repository also contains translated versions of the documentation. We strongly encourage members of the community who understand other languages to contribute translations of the dev portal contents in their native languages.
The `dactyl-config.yml` contains a "target" entry for each available language. (Currently, the available languages are English and Japanese.) This entry includes a dictionary of strings used in the template files. For example:
```yaml
- name: en
lang: en
display_name: XRP Ledger Dev Portal
# These github_ fields are used by the template's "Edit on GitHub" link.
# Override them with --vars to change which fork/branch to edit.
github_forkurl: https://github.com/XRPLF/xrpl-dev-portal
github_branch: master
strings:
blog: "Blog"
search: "Search site with Google..."
bc_home: "Home"
# ...
```
There is also a top-level `languages` listing that defines some properties for each supported language. The short code for each language should be short code according to [IETF BCP47](https://tools.ietf.org/html/bcp47). For example, "en" for English, "es" for Spanish, "ja" for Japanese, "zh-CN" for Simplified Chinese, "zh-TW" for Traditional Chinese (as used in Taiwan), and so on. The `display_name` field defines the language's name as written natively in that language. The `prefix` field defines a prefix to be used in hyperlinks to that language's version of the site. Example `languages` definition:
```yaml
languages:
- code: en
display_name: English
prefix: "/"
- code: ja
display_name: 日本語
prefix: "/ja/"
```
The same `dactyl-config.yml` file contains an entry for each content page in the XRP Ledger Dev Portal. If a page has been translated, there is a separate entry for each translation, linked to the "target" for that translation. If a page has not yet been translated, the English version is used across all targets. (If a new page is added only to English and not other languages, the link checker reports that as a broken link.)
Translating a page means separating out the entry for that page in the other language. Here are some tips for translating the page's metadata, which can be in either the `dactyl-config.yml` file or the frontmatter at the top of the page's Markdown file:
| Field | Notes |
|-------|-------|
| `html` | The HTML file name of the page. By convention, this should be the same across all language versions. |
| `md` | The Markdown source file for the page. Translated Markdown source files should use the same filename as the English-language version except that the file extension should be `.{language code}.md` instead of only `.md` for English. For example, Japanese translated files end in `.ja.md`. |
| `blurb` | A short summary of the page. This should be translated. This text is used in metadata for search engine optimization and also on automatically-generated landing pages. |
Example of English and Japanese entries for the `server_info` method page:
```yaml
- md: references/http-websocket-apis/public-api-methods/server-info-methods/server_info.md
targets:
- en
- md: references/http-websocket-apis/public-api-methods/server-info-methods/server_info.ja.md
targets:
- ja
```
Example entry for a page that isn't translated:
```yaml
- md: concepts/payment-system-basics/transaction-basics/source-and-destination-tags.md
targets:
- en
- ja
```
## Where to Start
If you want to translate the XRP Ledger Dev Portal into your native language of choice, start with the {% repo-link path="content/concepts/introduction/what-is-the-xrp-ledger.md" %}"What is the XRP Ledger?" page{% /repo-link %}, which introduces the core concepts behind the XRP Ledger.
Save the file as `what-is-the-xrp-ledger.{language code}.md`, where `{language code}` is the [IETF BCP47](https://tools.ietf.org/html/bcp47) language code. (For example, "es" for Spanish, "ja" for Japanese, "zh-CN" for Simplified Chinese, "zh-TW" for Traditional Chinese as used in Taiwan, and so on.) Then open a [pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) adding your file to this repository. One of the repository's maintainers can help with the other necessary setup to add the language to the site.
For the Markdown content files, please use the following conventions:
- Line-feed newline characters (`\n`) only (Unix style). Do not use carriage return (`\r`) characters (Windows style).
- Use UTF-8 encoding. Avoid the use of Byte-order marks.

View File

@@ -0,0 +1,146 @@
---
html: contribute-documentation.html
parent: resources.html
seo:
description: Contribution guides for XRP Ledger documentation.
---
# Contribute Documentation
Thanks for considering a contribution to the XRP Ledger Developer Portal!
We're thrilled you're interested and your help is greatly appreciated. Contributing is a great way to learn about the XRP Ledger (XRPL).
We are happy to review your pull requests. To make the process as smooth as possible, please read this document and follow the stated guidelines.
## About This Site
The XRPL Dev Portal provides comprehensive documentation of the the XRP Ledger, including sample code and other information for developers to start building.
The official source repository for the site is at <https://github.com/XRPLF/xrpl-dev-portal>. Contributions are copyright their respective contributors, but must be provided under the MIT [LICENSE](https://github.com/XRPLF/xrpl-dev-portal/blob/master/LICENSE).
## Repository Layout
***Note: The repository layout is currently in flux as we transition to Redocly. This section is not fully updated.***
- [static/](https://github.com/XRPLF/xrpl-dev-portal/tree/master/assets) - Static files used by the site's templates and theme.
- [content/](https://github.com/XRPLF/xrpl-dev-portal/tree/master/content) - Source files used to build the documentation. Mostly in Markdown.
- [content/\_code-samples/](https://github.com/XRPLF/xrpl-dev-portal/tree/master/content/_code-samples) - Code samples used or referenced by the documentation. Where possible, these are fully functional / executable scripts.
- [content/\_img-sources/](https://github.com/XRPLF/xrpl-dev-portal/tree/master/content/_img-sources) - Source files for images used in the documentation. Any `.uxf` files are diagrams made with [Umlet](https://www.umlet.com/).
- [content/\_snippets/](https://github.com/XRPLF/xrpl-dev-portal/tree/master/content/_snippets) - Reusable chunks of Markdown text that are included in other content files, using the Dactyl preprocessor.
- [img/](https://github.com/XRPLF/xrpl-dev-portal/tree/master/img) - Images used by the documentation contents.
- [template/](https://github.com/XRPLF/xrpl-dev-portal/tree/master/template) - Template files for building the HTML outputs. (Deprecated)
- [tool/](https://github.com/XRPLF/xrpl-dev-portal/tree/master/tool) - Filters, style-checker rules, and other scripts.
- [styles/](https://github.com/XRPLF/xrpl-dev-portal/tree/master/styles) - Source files (SCSS) to generate the CSS files in the assets folder.
- [`dactyl-config.yml`](https://github.com/XRPLF/xrpl-dev-portal/blob/master/dactyl-config.yml) - Old config file, which contains the metadata for the site.
## Requirements for a Successful Pull Request
Before being considered for review or merging, each pull request must:
- Pass continuous integration tests.
- Be [marked as drafts](https://github.blog/2019-02-14-introducing-draft-pull-requests/) until they are ready for review.
- Adhere to the [code of conduct](https://github.com/XRPLF/xrpl-dev-portal/blob/master/CODE_OF_CONDUCT.md) for this repository.
## Redocly Setup
The portal is built using Redocly Realm, which is currently in closed beta. Installing it for local development requires Node.js (version 20 recommended) and NPM.
You can install Realm and other necessary dependencies using NPM from the repository top:
```sh
npm i
```
## Building the Site
After you've installed dependencies, you can start a local dev server with:
```sh
npm run start
```
You can view the preview in a web browser, probably at http://localhost:4000/
## Config Formatting
Realm uses YAML config files to generate navigation elements in the site, including header, footer, sidebars, and breadcrumbs.
If you add a new page, you should add it to the appropriate `sidebars.yaml` file. There is one sidebars file for the documentation and one for the blog. Here's an example of an entry for a page with no nested children:
```yaml
- page: concepts/consensus-protocol/index.md
```
The Markdown file for a page should start with a [frontmatter stanza](#frontmatter-fields).
## Conventions
Use the following conventions when creating a page:
- Filenames (aside from `index.md`) should generally match the title of the page, including words like "and" and "the", but should be in all lowercase with hyphens instead of spaces and punctuation. For example, `cash-a-check-for-an-exact-amount.md`. If you change the title of a page, change the filename too. (If it has already been published at another URL, leave a redirect from the old URL.)
- The page within a category should be in a subfolder named for that category but can be less verbose (especially if the page title includes words also in the parent directories), should have the filename `index.md`, and a title that is similar to the folder name. For example, the "Protocol Reference" index page is at `references/protocol/index.md`.
- Always start a page with a h1 header.
- Don't link to the top h1 anchor of a page, link to the page itself without an anchor. This helps prevent broken links in translation. It's OK to link to later headers.
- Don't use any formatting (like _italics_ or `code font`) in the title of the page.
- Don't hard-wrap text in Markdown files.
- For code samples, try to keep lines no longer than 80 columns wide.
- When in doubt, follow [Ciro Santilli's Markdown Style Guide (Writability Profile)](https://cirosantilli.com/markdown-style-guide/).
- Don't use tab characters for indentation in Markdown or code samples. Use 4 spaces per indent, except in **JavaScript** code samples, which should use 2 spaces per indent.
- Make sure text files end in a newline character. (Some text editors handle this automatically.) Encode files in UTF-8 with no byte-order mark.
### Terminology
Use the following words and phrases as described:
| Term | Terms to Avoid | Notes |
|-------------------|----------------|-------|
| API, APIs | API's, RPC | Application Programming Interface, a set of functions and definitions for software to connect to other software. |
| core server, core XRP Ledger server | `rippled` | The `rippled` name is probably going to be retired in the near future, so it's better to refer to it by the more generic name. When necessary, refer to `rippled` in all lowercase and code font. (It's pronounced "ripple dee", and the "d" stands for "daemon" per UNIX tradition.)
| financial institution | bank, FI, PSP (payment services provider) | This term encompasses a wider range of businesses than just _bank_ or other terms and does not rely on an understanding of industry jargon. |
| ledger entry | ledger object, node | A single object inside the state data of the XRP Ledger. The term _ledger object_ could refer to one of these or to the whole ledger. The term _node_ was sometimes used for this case because the ledger's state data can be envisioned as a graph, but this is confusing because _node_ has other uses. |
| liquidity provider | market maker | A business or individual who offers to exchange between two currencies or assets, often deriving income from the price differential between trades. The term _market maker_ has a specific legal definition in some jurisdictions and may not apply in all the same circumstances. |
| malicious actor | hacker | A person, organization, or even an automated tool which might attempt to acquire secrets, break encryption, deny service, or otherwise attack a secure resource. |
| a NFT | an NFT | A NFT object in the XRP Ledger tracks or represents a non-fungible token. Pronounced "nifty" and written _a NFT_ rather than _an NFT_. |
| PostgreSQL | Postgres | A specific brand of relational database software. Always use the full name, not an informal short version. |
| order book | orderbook, offer book | A collection of trade orders waiting to be matched and executed, typically sorted by exchange rate. Use two words. |
| server | node | A server is software and/or hardware, especially the ones that connect to the XRP Ledger peer-to-peer network. The term _node_ is sometimes used for this purpose but is also overloaded with other meanings including entries in a graph and Node.js, a JavaScript interpreter. |
| stablecoin issuer | gateway | An issuer is the organization that issues a token in the XRP Ledger. A stablecoin is a token where the issuer promises that it is fully backed by some outside asset (such as fiat currency), with the stablecoin issuer providing deposit and withdraw operations to convert between the two (possibly for a fee). Previously the term _gateway_ was used (especially by Ripple, the company) to describe this use case, but the rest of the industry adopted _stablecoin issuer_ instead. |
| transaction cost | transaction fee | The amount of XRP burnt to send a transaction in the XRP Ledger. Even though this is specified in the `Fee` field of transactions, the term _fee_ implies that the money is paid to someone, so _cost_ is preferable. |_
| trust line | trustline | Use two words. A trust line is a relationship between two accounts in the XRP Ledger that tracks a balance of tokens between those accounts. |
| tokens | IOUs, issuances, issues, issued currencies | A token in the XRP ledger may not represent money owed outside of the ledger as the name _IOU_ implies. Specify _fungible tokens_ if necessary to distinguish from non-fungible tokens. |
| wallet | wallet | Depending on the context, _wallet_ could refer to hardware, software, a cryptographic key pair, or an online service. Be sure to provide enough context that the meaning is clear, or use an alternative such as _key pair_ or _client application_. |
| WebSocket | web socket, Websockets | A two way protocol for communication on the web. Always singular and in CamelCase. |
| XRP | XRPs, ripples | The native digital asset, or cryptocurrency, of the XRP Ledger. XRP is not a token. |
| the XRP Ledger | XRP Ledger (no the), Ripple, Ripple Network, RCL | The XRP Ledger was called _the Ripple network_ and the _Ripple Consensus Ledger_ or _RCL_ at various times in the past. These names were confusing and have been retired because of their similarity to the name of the company, Ripple (formerly Ripple Labs) which develops the reference implementation of the core server. |
| XRPL | XRPL | Short for _XRP Ledger_. As much as possible, spell out _XRP Ledger_ instead; _XRPL_ is cryptic and looks like it could be a typo for _XRP_. |
## Frontmatter Fields
***Note: The details of Realm's frontmatter specification are not fully documented. Update this with a link when Realm exits closed beta.***
Frontmatter for Markdown files can include details such as the following:
```yaml
---
metadata:
indexPage: true # Add this if you want the page to contain an auto-generated list of its child pages.
seo:
description: rippled is the core peer-to-peer server that manages the XRP Ledger. This section covers concepts that help you learn the "what" and "why" behind fundamental aspects of the rippled server.
---
```
Some pages in the site have leftover metadata from the previous (Dactyl) toolchain, such as `html`, `parent`, or `targets` fields. These fields have no effect and can be omitted from new pages.
## Markdoc Components
The files are processed with [Markdoc](https://markdoc.dev/), which means they can contain special tags in `{% ... %}` syntax. In addition to Redocly's built-in tags, this repository has some custom tags defined in `content/@theme/markdoc/`.
## Common Links
To make it easier to link to pages that are commonly cited, you can add a `{% raw-partial file="/_snippets/common-links.md /%}` tag to a Markdown file, and then use centrally-defined reference-style links such as `[account_info method][]` or `[Payment transaction][]`. The contents of the common-links file are in alphabetical order. (They have been generated by script before, but are currently manually maintained.)
{% child-pages /%}

View File

@@ -0,0 +1,84 @@
---
html: tutorial-guidelines.html
parent: contribute-documentation.html
seo:
description: Learn how this site's tutorials are structured and guidelines for contributing quality tutorials.
---
# Tutorial Guidelines
We are creating a modular tutorial framework that allows developers to learn how transactions and requests work on the XRP Ledger. Developers can review the modules to learn about business solutions, and potentially repurpose the scripts in their own applications.
# Rationale
What a developer wants comes down to two things:
1. Sample code snippets they can copy and paste into their own applications.
2. Complete API reference documentation.
Keep the conceptual information to a minimum only the information necessary to complete the tutorial. For background or deeper understanding, provide links to the conceptual topics at the end of the tutorial, if needed.
Modular tutorials follow Malcolm Knowles six assumptions for designing adult learning:
1. Adults need to know why they need to learn something.
2. Adults need to build on their experience.
3. Adults have a need to feel responsible for their learning.
4. Adults are ready to learn if training solves an immediate problem.
5. Adults want their training to be problem focused.
6. Adults learn best when motivation comes intrinsically.
Add into that Ralph Smedleys quote, “We learn best in moments of enjoyment.” Taking a lighter touch helps to relax the learner so that the material flows into their brain with less resistance.
# Sample Code vs. Tasks vs. Concepts vs. Tutorials
To date, there have been some blurred lines where different types of documentation show up as _Tutorials_. Here are some comparisons that help define the distinction.
## Sample Code
Sample code is well commented snippets or applications that illustrate best practices for implementing a feature of the API. Sample code is modular and reusable with little customization required.
Sample code is desirable, because advanced users can typically scan the example and use it immediately without a formal tutorial. It can also be used by others as a basis for tutorials. Sample code developers can focus on what they do well, while technical writers and support personnel can use the samples to create quality training materials.
## Tasks
Tasks are step-by-step instructions for how to accomplish a specific result. For example, “Installing rippled on a Red Hat Linux Server.” Task documentation is not intended to be particularly educational. It frequently describes tasks that are only performed one time per implementation, or maintenance tasks that always follow a familiar pattern. Tasks provide troubleshooting guidance, since there are likely variables that the user must adjust based on the specifics of their use case.
## Concepts
Conceptual information describes elements of the API, how they work, and when to use them. If a tutorial requires lengthy explanations before or during the programming tasks, consider how you might separate the exposition into a new topic, or link to existing topics that set the proper context.
For example, three paragraphs of context and a single line of code would be a concept, not a tutorial.
## Tutorials
Tutorials begin with sample code that illustrates best practices for implementing a feature. They take the developer step-by-step through the development process, explaining the purpose of each block of code.
Tutorials further combine a number of features to work together to solve a business problem. They describe the straightforward sunny day path to complete a task. Then, the tutorial might suggest modifications that let the developer try several different scenarios. Due to their focus on a certain limited scope of behavior, tutorials should not require extensive troubleshooting information.
## Use Cases
Use cases describe how to pull together multiple features to create a practical application that solves a business problem. They provide context and assistance with the decision making process, then provide links to the appropriate topics for each step of implementation.
# Tutorial Components
This section describes the elements of the modular tutorials used on XRPL.org.
## Sample Application
XRPL tutorial code samples are modular in nature. For example, Script 1 demonstrates how to create a test account, access the XRP Ledger, and transfer XRP between accounts. Any further samples can reuse the functions in Script 1.
Create a new script with the specific, minimal function code required to demonstrate the practical solution to a business problem. The examples should be incremental, with just enough behaviors to illustrate a business process.
For example, the first NFT tutorial shows how to mint, retrieve, and burn an NFT. The next tutorial shows how to create and accept a sell offer, and create and accept a buy offer.
Dont focus too much on the UX of the application, unless the look and feel is pertinent to the topic. Use the standard CSS file with the look and feel for all of the tutorials.
Reuse the code from other modules when possible. There might be situations where you need to modify the behavior from an earlier module. You can either overload the function name or modify the module and save it with a different name.

View File

@@ -0,0 +1,55 @@
---
html: tutorial-structure.html
parent: contribute-documentation.html
seo:
description: A summary of the parts of a standard tutorial.
---
# Tutorial Structure
Each XRP Ledger tutorial follows the same format.
1. A brief description of the features illustrated in the tutorial.
2. Prerequisites for running the code, if needed, or links to the sample code.
3. Usage examples of the features in the tutorial.
4. A code walkthrough of the sample application, highlighting unique elements in the scripts.
5. See Also, with links to conceptual information or good tutorials to try as a next step.
Separate setup (prerequisites) from usage from code development. These are each different activities that engage different areas of the brain. Trying to think of all three elements at once leads to confusion and headaches.
## Description
![Description](/img/tut-struct1.png)
List what the sample demonstrates. If possible, each example should describe the steps to accomplish specific related tasks. (For example, create a NFT Sell Offer, Accept a Sell Offer, Delete a Sell Offer.) There should be enough conceptual information to understand what the tutorial illustrates, with links to additional conceptual information, if needed.
## Prerequisites
![Prerequisites](/img/tut-struct2.png)
Provide links to any required software and to all of the example code needed to run the tutorial. If necessary, give simple instructions for using third-party tools, but provide a link to the source website for the customer to do a deeper dive at their leisure.
## Usage Example
![Usage](/img/tut-struct3.png)
Start by providing a finished, working example of the tutorial application. This is an opportunity for immediate success working with the software to solve a problem.
Use screenshots for each step of the tutorial these allow the user to understand the tutorial without having to run the code themselves. Of course we _want_ them to run the code, but this gives them a choice.
Describe the sunny day scenario. The application should run without problems if there is an uninterrupted connection to the internet. Dont provide a lot of troubleshooting information, unless its pertinent to the tutorial.
## Code Walkthrough
![Code Walkthrough](/img/tut-struct4.png)
Walk through the code, one chunk at a time. Dont belabor topics that have been discussed in earlier examples. Provide sample code, but dont provide exhaustive explanations for how to program underlying platforms like HTML syntax unless there is something unique to the implementation.
An important thing to emphasize is that every interaction with the XRPL is either a transaction or a request, and that all transactions and requests are essentially the same. The sample code we provide shows how to prepare the transaction or request, and how to process the returned results. Knowing how to submit and respond to one transaction or request gives a pretty good idea for how to submit and respond to any transaction or request.
(Technically there is third category, similar to a request: a notification from a subscription stream. See [Subscription Methods](../../references/http-websocket-apis/public-api-methods/subscription-methods/index.md).)
## See Also
![See Also](/img/tut-struct5.png)
At the end of the tutorial, provide links to additional resources, conceptual information, and any tutorials that would be a sensible next step in the learning journey.

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,77 @@
import * as React from 'react';
import { useTranslate } from '@portal/hooks';
import { clsx } from 'clsx'
export const CLASS_GOOD = "badge badge-success"
export const CLASS_BAD = "badge badge-danger"
export interface LogEntryStatus {
icon?: {
label: string,
type: "SUCCESS" | "ERROR"
check?: boolean
}
followUpMessage?: JSX.Element
}
export interface LogEntryItem {
message: string
id: string
status?: LogEntryStatus
}
/**
* Add entry to the end of the value that setLogEntries modifies.
*
* @param setLogEntries - A setter to modify a list of LogEntries
* @param entry - Data for a new LogEntry
*/
export function addNewLogEntry(
setLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,
entry: LogEntryItem)
{
setLogEntries((prev) => {
return [...prev, entry]
})
}
/**
* Looks up an existing log entry from the previous value within setLogEntries which has
* the same id as entry.id. Then it updates that value to equal entry.
*
* Primarily used to update the "status" after verifying a field.
*
* @param setLogEntries - A setter to modify a list of LogEntries.
* @param entryToUpdate - Updated data for an existing LogEntry.
*/
export function updateLogEntry(
setLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,
entryToUpdate: LogEntryItem) {
setLogEntries((prev) => {
const index = prev.findIndex((entry)=> entryToUpdate.id === entry.id)
prev.splice(index, 1, entryToUpdate)
return [...prev]
})
}
export function LogEntry({
message,
id,
status
}: LogEntryItem)
{
const {translate} = useTranslate()
let icon = undefined
if(!!(status?.icon)) {
icon = <span className={
clsx(status.icon?.type === "SUCCESS" && CLASS_GOOD,
status.icon?.type === "ERROR" && CLASS_BAD)}>
{status.icon?.label}
{status.icon?.check && <i className="fa fa-check-circle"/>}
</span>
}
return (
<li id={id}>{translate(`${message} `)}{icon}{status?.followUpMessage}</li>
)
}

View File

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

View File

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

View File

@@ -0,0 +1,77 @@
import * as React from 'react';
import { useState } from 'react'
import { useTranslate } from '@portal/hooks';
import { LogEntry, LogEntryItem } from './LogEntry';
/**
* A button that allows a single field to be submitted & logs displayed underneath.
*/
export interface TextLookupFormProps {
/**
* The big header above the button.
*/
title: string
/**
* Main description for what the button will do. Usually wrapped in <p> with <a>'s inside.
* All translation must be done before passing in the description.
*/
description: React.JSX.Element,
/**
* 2-3 words that appear on the button itself.
*/
buttonDescription: string
/*
* Triggered when users click the button to submit the form.
* setLogEntries is internally used to display logs to the user as handleSubmit executes.
* fieldValue represents the value they submitted with the form.
*/
handleSubmit: (
setLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,
event: React.FormEvent<HTMLFormElement>,
fieldValue: string) => void
/**
* Optionally include this as an example in the form to hint to users what they should type in.
*/
formPlaceholder?: string
}
/**
* A form to look up a single text field and display logs to the user.
*
* @param props Text fields for the form / button and a handler when the button is clicked.
* @returns A single-entry form which displays logs after submitting.
*/
export function TextLookupForm(props: TextLookupFormProps) {
const { translate } = useTranslate()
const { title, description, buttonDescription, formPlaceholder, handleSubmit } = props
const [logEntries, setLogEntries] = useState<LogEntryItem[]>([])
const [fieldValue, setFieldValue] = useState("")
return (
<div className="p-3 pb-5">
<form onSubmit={(event) => handleSubmit(setLogEntries, event, fieldValue)}>
<h4>{translate(title)}</h4>
{description}
<div className="input-group">
<input type="text" className="form-control" required
placeholder={translate(formPlaceholder)}
onChange={(event) => setFieldValue(event.target.value)}
/>
<br />
<button className="btn btn-primary form-control">{translate(buttonDescription)}</button>
</div>
</form>
<br/>
<br/>
{logEntries?.length > 0 && <div>
<h5 className="result-title">{translate(`Result`)}</h5>
<ul id="log">
{logEntries.map((log) => {
return <LogEntry message={log.message} id={log.id} key={log.id} status={log.status} />
})}
</ul>
</div>}
</div>)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,264 @@
import * as React from "react";
import { useTranslate } from "@portal/hooks";
import { decode } from "ripple-binary-codec";
import addressCodec, { encodeNodePublic } from "ripple-address-codec";
import { verify as keyCodecVerify } from "ripple-keypairs";
import { parse } from "smol-toml";
import { TextLookupForm } from "./components/TextLookupForm";
import { addNewLogEntry, LogEntryItem, updateLogEntry } from './components/LogEntry'
import { hexToBytes, hexToString, stringToHex } from "@xrplf/isomorphic/utils"
export const frontmatter = {
seo: {
title: 'Domain Verifier',
description: "Use this tool to confirm that your rippled validator has domain verification set up correctly",
}
};
const TIPS =
<p>Check if the xrp-ledger.toml file is actually hosted in the /.well-known/ location at the domain in your manifest. Check your server\'s HTTPS settings and certificate, and make sure your server provides the required <a href="xrp-ledger-toml.html#cors-setup">CORS header.</a></p>;
const DomainVerificationPage = () => {
const { translate } = useTranslate();
const TOML_PATH = "/.well-known/xrp-ledger.toml";
let query_param = 0;
const parse_xrpl_toml = async (setLogEntries, data, public_key_hex, public_key, message) => {
const parsingTomlBase = {
message: translate('Parsing TOML data...'),
id: 'parsing-toml'
}
let parsed;
try {
addNewLogEntry(setLogEntries, parsingTomlBase);
parsed = parse(data);
updateLogEntry(setLogEntries, {
...parsingTomlBase,
status: {
icon: {
label: translate('Success'),
type: 'SUCCESS'
}
}
});
} catch(e) {
updateLogEntry(setLogEntries, {
...parsingTomlBase,
status: {
icon: {
label: e.message,
type: 'SUCCESS'
}
}
});
}
const validator_entries = parsed.VALIDATORS;
if (validator_entries) {
if (!Array.isArray(validator_entries)) {
addNewLogEntry(setLogEntries, {
id: 'validators',
message: translate('Validators'),
status: {
icon: {
label: translate('Wrong type - should be table-array'),
type: 'SUCCESS'
}
}
})
return;
}
let validator_found = false;
for (let i = 0; i < validator_entries.length; i++) {
const pk = validator_entries[i]["public_key"];
if (pk === public_key) {
validator_found = true;
const attestation = validator_entries[i]["attestation"];
const verify = keyCodecVerify(
stringToHex(message),
attestation,
public_key_hex
);
if (verify) {
addNewLogEntry(setLogEntries, {
message: translate("Domain Verification Succeeded"),
id: "domain-verification-success",
});
} else {
addNewLogEntry(setLogEntries, {
message: translate("Domain Verification Failed"),
id: "domain-verification-fail",
});
}
break;
}
}
if (!validator_found) {
addNewLogEntry(setLogEntries, {
message: translate(
"The validator key for this manifest was not found in the TOML file"
),
id: "validator-key-not-found",
});
}
} else {
addNewLogEntry(setLogEntries, {
message: translate("No Validators Found"),
id: "no-validators",
});
}
};
function displayManifest(setLogEntries, manifest) {
for(const key in manifest) {
addNewLogEntry(setLogEntries,{
message: `${key}: ${manifest[key]}`,
id: `manifest-${key}`
})
}
}
const parseAndVerifyManifest = async (setLogEntries, manifest) => {
let decodedManifest: any
try {
decodedManifest = decode(manifest.toUpperCase());
} catch(e) {
addNewLogEntry(setLogEntries, {
message: translate(`Error decoding manifest:`),
id: "error-decoding-manifest",
status: {
icon: {
label: e.message,
type: 'ERROR'
}
}
});
return
}
const publicKeyHex = decodedManifest.PublicKey as string;
const publicKey = encodeNodePublic(hexToBytes(publicKeyHex));
const seq = decodedManifest['Sequence']
const ephemeralPublicKeyHex = decodedManifest["SigningPubKey"];
const ephemeralPublicKey = addressCodec.encodeNodePublic(hexToBytes(ephemeralPublicKeyHex));
let domain: string;
try {
domain = hexToString(decodedManifest.Domain as string);
} catch {
addNewLogEntry(setLogEntries, {
message: translate(`"Domain not found in manifest"`),
id: "no-domain",
});
displayManifest(setLogEntries, {
"Sequence": seq,
"Master Public Key": publicKey,
"Ephemeral Public Key": ephemeralPublicKey
});
return
}
displayManifest(setLogEntries, {"Sequence":seq,
"Domain":domain,
"Master Public Key": publicKey,
"Ephemeral Public Key":ephemeralPublicKey})
const message = `[domain-attestation-blob:${domain}:${publicKey}]`;
const url = `https://${domain}${TOML_PATH}?v=${query_param++}`;
const baseCheckingToml = {
id: 'checking-toml',
message: translate(`${translate('Checking ')} ${url}`)
}
addNewLogEntry(setLogEntries, baseCheckingToml)
try {
await fetch(url)
.then((response) => response.text())
.then((data) => {
updateLogEntry(setLogEntries, {
...baseCheckingToml,
status: {
icon: {
label: translate('Found'),
type: 'SUCCESS'
}
}
})
parse_xrpl_toml(setLogEntries, data, publicKeyHex, publicKey, message)
})
.catch((error) => {
updateLogEntry(setLogEntries, {
...baseCheckingToml,
status: {
icon: {
label: error.message,
type: 'ERROR'
}
}
})
});
} catch (e) {
addNewLogEntry(setLogEntries, {
message: translate(`Error decoding manifest:`),
id: "error-decoding-manifest",
status: {
followUpMessage: TIPS,
icon: {
label: e.message,
type: 'ERROR',
},
}
});
}
};
const handleSubmit = (
setLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,
event: React.FormEvent<HTMLFormElement>,
fieldValue: string
) => {
event.preventDefault();
setLogEntries([]);
parseAndVerifyManifest(setLogEntries, fieldValue);
};
const formProps = {
title: translate("Domain Verification Checker"),
description: (
<div>
<p>
{translate(
"This tool allows you to verify that domain verification is properly configured."
)}
</p>
<p>
{translate(
"Enter the manifest found in your validator-keys.json file. Do not confuse this with your validator's secret key."
)}
</p>
<p>
{translate(
"To do this with the validator-keys-tool use the following command:"
)}
</p>
<pre>
<code>$ validator-keys show_manifest hex</code>
</pre>
</div>
),
buttonDescription: translate("Verify"),
formPlaceholder: translate("Your Manifest Here"),
handleSubmit,
};
return <TextLookupForm {...formProps} />;
};
export default DomainVerificationPage;

View File

@@ -0,0 +1,28 @@
{
"knownFaucets": [
{
"id": "faucet-select-testnet",
"wsUrl": "wss://s.altnet.rippletest.net:51233/",
"jsonRpcUrl": "https://s.altnet.rippletest.net:51234/",
"faucetUrl": "faucet.altnet.rippletest.net",
"shortName": "Testnet",
"desc": "Mainnet-like network for testing applications."
},
{
"id": "faucet-select-devnet",
"wsUrl": "wss://s.devnet.rippletest.net:51233/",
"jsonRpcUrl": "https://s.devnet.rippletest.net:51234/",
"faucetUrl": "faucet.devnet.rippletest.net",
"shortName": "Devnet",
"desc": "Preview of upcoming amendments."
},
{
"id": "faucet-select-xahau",
"wsUrl": "wss://xahau-test.net/",
"jsonRpcUrl": "https://xahau-test.net/",
"faucetUrl": "xahau-test.net",
"shortName": "Xahau-Testnet",
"desc": "Hooks (L1 smart contracts) enabled Xahau testnet."
}
]
}

View File

@@ -0,0 +1,310 @@
import * as React from "react";
import { useTranslate } from "@portal/hooks";
export const frontmatter = {
seo: {
title: 'Dev Tools',
description: "Use these tools to explore, build, and test XRP Ledger technology.",
}
};
const explorers_tools = [
{
id: "xrp-explorer",
title: "XRPL Explorer",
description:
"View validations of new ledger versions in real-time, or chart the location of servers in the XRP Ledger.",
href: "https://livenet.xrpl.org",
img: require("../../img/dev-tools/explorer.png"),
},
{
id: "bithomp-explorer",
title: "Bithomp Explorer",
description:
"Explore public ledger data including accounts' transaction history and known names.",
href: "https://bithomp.com/",
img: require("../../img/dev-tools/bithomp.png"),
},
{
id: "xrpscan",
title: "XRPScan",
description:
"Explore ledger activity, view amendment voting in real-time, and get account information. API access is also available.",
href: "https://xrpscan.com/",
img: require("../../img/dev-tools/xrpscan.png"),
},
{
id: "token-list",
title: "Token List",
description:
"See all tokens issued in the XRP Ledger and use preset tools to issue custom tokens at the click of a button.",
href: "https://xumm.community/tokens",
img: require("../../img/dev-tools/tokenlist.png"),
},
];
const api_access_tools = [
{
id: "websocket",
title: "WebSocket Tool",
description:
"Send sample requests and get responses from the rippled API. ",
href: "websocket-api-tool",
img: require("../../img/dev-tools/websocket-tool.png"),
},
{
id: "rpc",
title: "RPC Tool",
description:
"Print raw information about an XRP Ledger account, transaction, or ledger.",
href: "xrp-ledger-rpc-tool",
img: require("../../img/dev-tools/rpc-tool.png"),
},
{
id: "technical-explorer",
title: "Technical Explorer",
description: "Browse API objects from the ledger with real-time updates. ",
href: "https://explorer.xrplf.org/",
img: require("../../img/dev-tools/technical-explorer.png"),
},
{
id: "faucets",
title: "Faucets",
description:
"Get credentials and test-XRP for XRP Ledger Testnet or Devnet.",
href: "xrp-testnet-faucet",
img: require("../../img/dev-tools/faucets.png"),
},
{
id: "trasaction-sender",
title: "Transaction Sender",
description:
"Test how your code handles various XRP Ledger transactions by sending them over the Testnet to the address.",
href: "tx-sender",
img: require("../../img/dev-tools/transaction-sender.png"),
},
];
const other = [
{
id: "domain",
title: "Domain Verification Checker",
description: "Verify your validator's domain.",
href: "validator-domain-verifier",
img: require("../../img/dev-tools/domain-checker.png"),
},
{
id: "xrp-ledger",
title: "xrp-ledger.toml Checker",
description: "Verify that your xrp-ledger.toml file is set up properly.",
href: "xrp-ledger-toml-checker",
img: require("../../img/dev-tools/toml-checker.png"),
},
{
id: "binary-visualizer",
title: "Binary Visualizer",
description:
"Parse the XRP Ledger's native binary format with a visual representation breaking down the raw structure into its parts.",
href: "https://richardah.github.io/xrpl-binary-visualizer/",
img: require("../../img/dev-tools/binary-visualizer.png"),
},
{
id: "token-metadata-lookup",
title: "Token Metadata Lookup",
description:
"Query known information about any token issued on the XRP Ledger.",
href: "https://xrplmeta.org/",
img: require("../../img/dev-tools/token-metadata.png"),
},
];
export default function DevTools() {
const { translate } = useTranslate();
return (
<article className="page-dev-tools pt-3 p-md-3">
<div>
<section className="py-20">
<div className="mx-auto text-lg-left">
<div className="d-flex flex-column-reverse">
<p className="mb-0">
{translate(
"Use the developer tools to test, explore, and validate XRP Ledger\n API requests and behavior."
)}
</p>
<h3 className="eyebrow mb-3"> {translate("Dev Tools")}</h3>
</div>
</div>
</section>
<section className="py-10">
<div className="mx-auto text-lg-left">
<div className="sticky">
<ul className="nav nav-tabs pb-15" role="tablist">
<li className="nav-item" role="presentation">
<button
className="nav-link active dev-tools-tab"
id="explorers-tab"
data-toggle="tab"
data-target="#explorers"
role="tab"
aria-controls="explorers"
aria-selected="true"
>
{translate("Explorers")}
</button>
</li>
<li className="nav-item" role="presentation">
<button
className="nav-link dev-tools-tab"
id="api-access-tab"
data-toggle="tab"
data-target="#api-access"
role="tab"
aria-controls="api-access"
aria-selected="false"
>
{translate("API Access")}
</button>
</li>
<li className="nav-item" role="presentation">
<button
className="nav-link dev-tools-tab"
id="other-tab"
data-toggle="tab"
data-target="#other"
role="tab"
aria-controls="other"
aria-selected="false"
>
{translate("Other")}
</button>
</li>
</ul>
</div>
<div className="tab-content pt-20">
<div
className="tab-pane show active"
id="explorers"
role="tabpanel"
aria-labelledby="explorers-tab"
>
<h4> {translate("Explorers")}</h4>
<div className="row row-cols-1 row-cols-lg-3 card-deck">
{explorers_tools.map((card) => (
<a
className="card"
href={card.href}
target="_blank"
rel="noopener noreferrer"
id={card.id}
key={card.id}
>
{card.img && (
<img src={card.img} alt={`${card.title} Screenshot`} />
)}
<div className="card-body">
<h4 className="card-title h5">{card.title}</h4>
<p className="card-text">{card.description}</p>
</div>
<div className="card-footer">&nbsp;</div>
</a>
))}
</div>
</div>
<div
className="tab-pane"
id="api-access"
role="tabpanel"
aria-labelledby="api-access-tab"
>
<h4> {translate("API Access")}</h4>
<div className="row row-cols-1 row-cols-lg-3 card-deck">
{api_access_tools.map((card) => (
<a
className="card"
href={card.href}
target="_blank"
id={card.id}
key={card.id}
>
{
card.img && (
<img src={card.img} alt={card.title + " Screenshot"} />
)
}
<div className="card-body">
<h4 className="card-title h5">{card.title}</h4>
<p className="card-text">{card.description}</p>
</div>
<div className="card-footer">&nbsp;</div>
</a>
))}
</div>
</div>
<div
className="tab-pane"
id="other"
role="tabpanel"
aria-labelledby="other-tab"
>
<h4> {translate("Other")}</h4>
<div className="row row-cols-1 row-cols-lg-3 card-deck">
{other.map((card) => (
<a
className="card"
href={card.href}
target="_blank"
id={card.id}
key={card.id}
>
{
card.img && (
<img src={card.img} alt={card.title + " Screenshot"} />
)
}
<div className="card-body">
<h4 className="card-title h5">{card.title}</h4>
<p className="card-text">{card.description}</p>
</div>
<div className="card-footer">&nbsp;</div>
</a>
))}
</div>
</div>
</div>
</div>
</section>
<section className="container-new py-10 px-0">
<div className="col-lg-12 p-6-sm p-10-until-sm br-8 cta-card">
<img
alt="default-alt-text"
src={require("../../img/backgrounds/cta-home-purple.svg")}
className="d-none-sm cta cta-top-left"
/>
<img
alt="default-alt-text"
src={require("../../img/backgrounds/cta-home-green.svg")}
className="cta cta-bottom-right"
/>
<div className="z-index-1 position-relative">
<h2 className="h4 mb-8-sm mb-10-until-sm">
{translate("Have an Idea For a Tool?")}
</h2>
<p className="mb-10">
{translate(
"Contribute to the XRP Ledger community by submitting your idea for a tool or open a pull request if you've developed a tool."
)}
</p>
<a
className="btn btn-primary btn-arrow-out"
href="https://github.com/XRPLF/xrpl-dev-portal/"
>
{translate("Open a pull Request")}
</a>
</div>
</div>
</section>
</div>
</article>
);
}

View File

@@ -0,0 +1,327 @@
import { useTranslate } from "@portal/hooks";
import { Link } from '@portal/Link';
import { useState, useEffect } from "react";
import { useLocation } from "react-router-dom";
import JsonView from 'react18-json-view'
import { Client, isValidAddress } from 'xrpl'
import { RPCResponseGroup } from './components/rpc-tool/rpc-response-group';
import { clsx } from 'clsx';
export const frontmatter = {
seo: {
title: 'RPC Tool',
description: "Quickly query several key details about an XRP Ledger account or transaction",
}
};
export default function RpcTool() {
const { hash: slug } = useLocation();
const [accountInfoResponse, setAccountInfoResponse] = useState(null);
const [accountLinesResponse, setAccountLinesResponse] = useState(null);
const [accountTxResponse, setAccountTxResponse] = useState(null);
const [accountObjectsResponse, setAccountObjectsResponse] = useState(null);
const [txResponse, setTxResponse] = useState(null);
const [ledgerResponse, setLedgerResponse] = useState(null);
const [inputText, setInputText] = useState(slug ? slug.slice(1) : "");
const [errorText, setErrorText] = useState(null);
const { translate } = useTranslate();
const [inputType, setInputType] = useState("");
const [showResult, setShowResult] = useState(false);
const [progressBarWidth, setProgressBarWidth] = useState("0%");
const FULL_HISTORY_SERVER = "wss://s2.ripple.com";
const reTxId = /^[0-9A-Fa-f]{64}$/;
const reLedgerSeq = /^[0-9]+$/;
/*
XRPL requests
* account_info
* account_lines
* account_tx
* account_objects
* tx
* ledger
*/
const accountInfo = async (
client: Client,
address: string
): Promise<void> => {
const response = await client.request({
command: "account_info",
account: address,
});
setProgressBarWidth("20%");
setAccountInfoResponse(response);
};
const accountLines = async (
client: Client,
address: string
): Promise<void> => {
const response = await client.request({
command: "account_lines",
account: address,
});
setProgressBarWidth("40%");
setAccountLinesResponse(response);
};
const accountTx = async (
client: Client,
address: string
): Promise<void> => {
const response = await client.request({
command: "account_tx",
account: address,
ledger_index_min: -1,
ledger_index_max: -1,
binary: false,
limit: 20,
forward: false,
});
setProgressBarWidth("60%");
setAccountTxResponse(response);
};
const accountObjects = async (
client: Client,
address: string
): Promise<void> => {
const response = await client.request({
command: "account_objects",
account: address,
});
setProgressBarWidth("80%");
setAccountObjectsResponse(response);
};
const tx = async (
client: Client,
transactionId: string
): Promise<void> => {
const response = await client.request({
command: "tx",
transaction: transactionId,
binary: false,
});
setProgressBarWidth("100%");
setTxResponse(response);
};
const ledger = async (
client: Client,
ledgerSequence: string
): Promise<void> => {
const response = await client.request({
command: "ledger",
ledger_index: parseInt(ledgerSequence),
transactions: true,
expand: true,
});
setProgressBarWidth("100%");
setLedgerResponse(response);
};
useEffect(() => {
if (slug && slug !== "") {
getInfo();
}
}, []);
const getInfo = async (): Promise<void> => {
setAccountInfoResponse(null);
setAccountLinesResponse(null);
setAccountTxResponse(null);
setAccountObjectsResponse(null);
setTxResponse(null);
setLedgerResponse(null);
setErrorText(null);
setShowResult(true);
setProgressBarWidth("0%");
const client = new Client(FULL_HISTORY_SERVER);
await client.connect();
if (isValidAddress(inputText)) {
// Example input: rh3VLyj1GbQjX7eA15BwUagEhSrPHmLkSR
setInputType("accounts");
setErrorText("");
setProgressBarWidth("10%");
// account_info
await accountInfo(client, inputText);
// account_lines
await accountLines(client, inputText);
// account_tx
await accountTx(client, inputText);
// account_objects
await accountObjects(client, inputText);
setProgressBarWidth("100%");
} else if (reTxId.test(inputText)) {
// Example input: A25795C88E176FFF85B8D595D1960229F4ACC825BAE634ADF38F6AE38E0D24D8
setInputType("transactions");
setErrorText("");
setProgressBarWidth("10%");
// tx
await tx(client, inputText);
} else if (reLedgerSeq.test(inputText)) {
// Example input: 131524184
setInputType("ledgers");
setErrorText("");
setProgressBarWidth("10%");
// ledger
await ledger(client, inputText);
} else {
setProgressBarWidth("100%");
setErrorText("Input is not a valid address or transaction hash");
}
await client.disconnect();
};
return (
<div className="container-fluid rpc-tool" role="document" id="main_content_wrapper">
<div className="row">
<main
className="main order-md-3"
role="main"
id="main_content_body"
>
<section className="container-fluid pt-3 p-md-3">
<h1>RPC Tool</h1>
<div className="content">
<p>
{translate(
"This is a debug tool for printing raw information about an account (by classic address), a transaction (by hash) or a ledger (by sequence number)."
)}
</p>
<input
onChange={(event) => setInputText(event.target.value)}
value={inputText}
id="target"
className="form-control"
required
type="text"
placeholder={translate(
"XRP Ledger address, transaction ID, or ledger index"
)}
/>
<span className="help-block">
<small>
{translate("Try an account like ")}
<em>rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn</em>.
</small>
</span>&nbsp;
<button className="btn btn-primary" onClick={getInfo}>
{translate("Get info")}
</button>
{showResult && (
<div id="result">
<h2>{translate("Result")}</h2>
<div
id="progress"
className="progress"
style={
progressBarWidth === "100%" ?
{ transition: 'opacity 0.5s ease-out', opacity: 0, } :
{ opacity: 1 }
}
>
<div
className="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar"
style={{
width: progressBarWidth,
}}
/>
</div>
{errorText && (
<div id="error" className="devportal-callout warning">
{translate(errorText)}
</div>
)}
{errorText === "" && (
<ul id="links" className="nav nav-pills">
<li className="nav-link">
<Link
id="permalink"
to={`/resources/dev-tools/rpc-tool#${inputText}`}
target="_blank"
>
{translate("Permalink")}
</Link>
</li>
<li className="nav-link">
<Link
id="explorerlink"
to={`https://livenet.xrpl.org/${inputType}/${inputText}`}
target="_blank"
>
{translate("Explorer")}
</Link>
</li>
</ul>
)}
{txResponse && (
<RPCResponseGroup response={txResponse}
anchor={<a href="tx.html">tx</a>}
customExpanded={3}
customExpandedText={translate("expand tx")}
/>
)}
<div className="group group-account">
{accountInfoResponse && (
<>
<h3>
<a href="account_info.html">account_info</a>
</h3>
<JsonView
src={accountInfoResponse}
collapsed={1}
collapseStringsAfterLength={100}
enableClipboard={false}
/>
</>
)}
{accountLinesResponse && (
<RPCResponseGroup response={accountLinesResponse}
anchor={<a href="account_lines.html">account_lines</a>}
/>)
}
{accountTxResponse && (
<RPCResponseGroup response={accountTxResponse}
anchor={<><a href="account_tx.html">account_tx</a>{" "} {translate("(last 20)")}</>}
customExpanded={3}
customExpandedText={translate("expand tx")}
/>)
}
{accountObjectsResponse && (
<RPCResponseGroup response={accountObjectsResponse}
anchor={<a href="account_objects.html">account_objects</a>}
/>)
}
</div>
{ledgerResponse && (
<RPCResponseGroup response={ledgerResponse}
anchor={<a href="ledger.html">ledger</a>}
/>)
}
</div>
)}
</div>
</section>
</main>
</div>
</div>
);
}

View File

@@ -0,0 +1,163 @@
import { clsx } from 'clsx'
import { Client } from 'xrpl'
import React = require("react");
import { CLASS_GOOD } from "../components/LogEntry";
import { AccountFields } from "./XrplToml";
// Decode a hexadecimal string into a regular string, assuming 8-bit characters.
// Not proper unicode decoding, but it'll work for domains which are supposed
// to be a subset of ASCII anyway.
function decodeHex(hex) {
let str = '';
for (let i = 0; i < hex.length; i += 2) {
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16))
}
return str
}
function getWsUrlForNetwork(net: string) {
let wsNetworkUrl: string
if (net === "main") {
wsNetworkUrl = 'wss://s1.ripple.com:51233'
} else if (net == "testnet") {
wsNetworkUrl = 'wss://s.altnet.rippletest.net:51233'
} else if (net === "devnet") {
wsNetworkUrl = 'wss://s.devnet.rippletest.net:51233/'
} else if (net === "xahau") {
wsNetworkUrl = 'wss://xahau-test.net:51233'
} else {
wsNetworkUrl = undefined
}
return wsNetworkUrl
}
async function validateAddressDomainOnNet(addressToVerify: string, domain: string, net: string) {
if (!domain) { return undefined } // Can't validate an empty domain value
const wsNetworkUrl = getWsUrlForNetwork(net)
if(!wsNetworkUrl) {
console.error(`The XRPL TOML Checker does not currently support verifying addresses on ${net}.
Please open an issue to add support for this network.`)
return undefined
}
const api = new Client(wsNetworkUrl)
await api.connect()
let accountInfoResponse
try {
accountInfoResponse = await api.request({
"command": "account_info",
"account": addressToVerify
})
} catch(e) {
console.warn(`failed to look up address ${addressToVerify} on ${net} network"`, e)
return undefined
} finally {
await api.disconnect()
}
if (accountInfoResponse.result.account_data.Domain === undefined) {
console.info(`Address ${addressToVerify} has no Domain defined on-ledger`)
return undefined
}
let decodedDomain
try {
decodedDomain = decodeHex(accountInfoResponse.result.account_data.Domain)
} catch(e) {
console.warn("error decoding domain value", accountInfoResponse.result.account_data.Domain, e)
return undefined
}
if(decodedDomain) {
const doesDomainMatch = decodedDomain === domain
if(!doesDomainMatch) {
console.debug(addressToVerify, ": Domain mismatch ("+decodedDomain+" vs. "+domain+")")
}
return doesDomainMatch
} else {
console.debug(addressToVerify, ": Domain is undefined in settings")
return undefined
}
}
/**
* A formatted list item displaying content from a single field of a toml file.
*
* @param props Field info to display
* @returns A formatted list item
*/
function FieldListItem(props: { fieldName: string, fieldValue: string}) {
return (
<li key={props.fieldName}>
<strong>{props.fieldName}: </strong>
<span className={`fieldName`}>
{props.fieldValue}
</span>
</li>)
}
/**
* Get an array of HTML lists that can be used to display toml data.
* If no data exists or none matches the filter it will return an empty array instead.
*
* @param fields An array of objects to parse into bullet points
* @param filter Optional function to filter displayed fields to only ones which return true.
*/
export async function getListEntries(fields: Object[], filter?: Function, domainToVerify?: string) {
const formattedEntries: JSX.Element[] = []
for(let i = 0; i < fields.length; i++) {
const entry = fields[i]
if(!filter || filter(entry)) {
const fieldNames = Object.keys(entry)
const displayedFields: JSX.Element[] = []
fieldNames.forEach((fieldName) => {
if(entry[fieldName] && Array.isArray(entry[fieldName])) {
const internalList = []
entry[fieldName].forEach((value) => {
internalList.push(
<FieldListItem key={value} fieldName={fieldName} fieldValue={value}/>
)
})
displayedFields.push(<ol key={`ol-${displayedFields.length}`}>{internalList}</ol>)
} else {
displayedFields.push(
<FieldListItem key={fieldName} fieldName={fieldName} fieldValue={entry[fieldName]}/>
)
}
})
const key = `entry-${formattedEntries.length}`
const promises = []
if(domainToVerify) {
const accountEntry = entry as AccountFields
if(accountEntry.address) {
const net = accountEntry.network ?? "main"
const domainIsValid = validateAddressDomainOnNet(accountEntry.address, domainToVerify, net)
domainIsValid.then((wasValidated) => {
if(wasValidated) {
displayedFields.push(
<li className={CLASS_GOOD} key={`${key}-result`}>DOMAIN VALIDATED <i className="fa fa-check-circle"/></li>
)
}
})
promises.push(domainIsValid)
}
}
await Promise.all(promises)
formattedEntries.push((<li key={key}>
<ul className={clsx(domainToVerify && 'mb-3')} key={key + "-ul"}>{displayedFields}</ul>
</li>))
}
}
return formattedEntries
}

View File

@@ -0,0 +1,427 @@
import * as React from 'react';
import { useTranslate } from '@portal/hooks';
import axios, { AxiosError } from "axios";
import { parse } from "smol-toml";
import { getListEntries } from "./ListTomlFields";
import { addNewLogEntry, updateLogEntry, LogEntryItem, LogEntryStatus } from "../components/LogEntry";
import { MetadataField, XrplToml, AccountFields, TOML_PATH } from "./XrplToml";
/**
* Helper to log a list of fields from a toml file or display a relevant error message.
* Will return true if successfully displays at least one field from fields without erroring.
*
* @param setLogEntries A setter to update the logs with the new fields.
* @param headerText The initial message to include as a header for the list.
* @param fields A set of fields to parse and display. May be undefined, but if so,
* this function will simply return false. Simplifies typing.
* @param domainToVerify The domain to check
* @param filterDisplayedFieldsTo Limits the displayed fields to ones which match the predicate.
* @returns True if displayed any fields (after applying any given filters)
*/
async function validateAndDisplayFields(
setLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,
headerText: string,
fields?: Object[],
domainToVerify?: string,
filterDisplayedFieldsTo?: Function): Promise<boolean> {
const { translate } = useTranslate()
// If there's no data, do nothing
if(!fields) {
return false
}
// Otherwise display all relevant data in the toml file for these field
if(Array.isArray(fields)) {
let icon = undefined;
const formattedEntries = await getListEntries(fields, filterDisplayedFieldsTo, domainToVerify)
const relevantTomlFieldsExist = formattedEntries.length > 0
if(relevantTomlFieldsExist) {
addNewLogEntry(setLogEntries,
{
message: headerText,
id: headerText,
status: {
followUpMessage: (
<ol>
{formattedEntries}
</ol>
),
icon: icon
}
})
}
return relevantTomlFieldsExist
} else {
// Invalid toml data
addNewLogEntry(setLogEntries, {
message: headerText,
id: headerText,
status: {
icon: {
label: translate("WRONG TYPE - SHOULD BE TABLE-ARRAY"),
type: "ERROR",
}
}
})
return false
}
}
/**
* Check whether a metadata field on a toml file is valid, then display logs with the results.
*
* @param setLogEntries - A setter to update the logs
* @param metadata - Metadata from a toml file being verified
*/
function validateAndDisplayMetadata(
setLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,
metadata?: MetadataField) {
const { translate } = useTranslate()
if (metadata) {
const metadataId = 'metadata-log'
const metadataLogEntry = {
message: translate("Metadata section: "),
id: metadataId
}
addNewLogEntry(setLogEntries, metadataLogEntry)
// Uniquely checks if array, instead of if not array
if (Array.isArray(metadata)) {
updateLogEntry(setLogEntries, {...metadataLogEntry, status: {
icon: {
label: translate("WRONG TYPE - SHOULD BE TABLE"),
type: "ERROR",
},
}})
} else {
updateLogEntry(setLogEntries, {...metadataLogEntry, status: {
icon: {
label: translate("FOUND"),
type: "SUCCESS",
},
}})
if (metadata.modified) {
const modifiedLogId = 'modified-date-log'
const modifiedLogEntry = {
message: translate("Modified date: "),
id: modifiedLogId
}
addNewLogEntry(setLogEntries, modifiedLogEntry)
try {
updateLogEntry(setLogEntries, { ...modifiedLogEntry, status: {
icon: {
label: metadata.modified.toISOString(),
type: "SUCCESS",
},
}})
} catch(e) {
updateLogEntry(setLogEntries, { ...modifiedLogEntry, status: {
icon: {
label: translate("INVALID"),
type: "ERROR",
},
}})
}
}
}
}
}
/**
* Read in a toml file and verify it has the proper fields, then display those fields in the logs.
* This is the 3rd step for verifying a wallet, and the 2nd step for verifying a toml file itself.
*
* @param setLogEntries A setter to update the logs.
* @param tomlData Toml data to parse.
* @param addressToVerify The address we're actively looking to verify matches with this toml file.
* @param domain A website to look up further information about the toml file.
* @returns Nothing.
*/
async function parseXRPLToml(
setLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,
tomlData,
addressToVerify?: string,
domain?: string) {
const { translate } = useTranslate()
const parsingTomlLogEntry: LogEntryItem = {
message: translate("Parsing TOML data..."),
id: 'parsing-toml-data-log',
}
addNewLogEntry(setLogEntries, parsingTomlLogEntry)
let parsed: XrplToml
try {
parsed = parse(tomlData)
updateLogEntry(setLogEntries, {...parsingTomlLogEntry, status: {
icon: {
label: translate("SUCCESS"),
type: "SUCCESS",
},
}})
} catch(e) {
updateLogEntry(setLogEntries, {...parsingTomlLogEntry, status: {
icon: {
label: e,
type: "ERROR",
},
}})
return
}
validateAndDisplayMetadata(setLogEntries, parsed.METADATA)
const accountHeader = translate("Accounts:")
if(addressToVerify) {
const filterToSpecificAccount = (entry: AccountFields) => entry.address === addressToVerify
const accountFound = await validateAndDisplayFields(
setLogEntries,
accountHeader,
parsed.ACCOUNTS,
undefined,
filterToSpecificAccount)
const statusLogId = 'account-found-status-log'
if(accountFound) {
// Then share whether the domain / account pair as a whole has been validated
addNewLogEntry(setLogEntries, {
message: translate('Account has been found in TOML file and validated.'),
id: statusLogId,
status: {
icon: {
label: translate("DOMAIN VALIDATED"),
type: "SUCCESS",
check: true,
}
}
})
} else {
// We failed to find any entries which match the account we're looking for
addNewLogEntry(setLogEntries, {
message: translate("Account:"),
id: 'toml-account-entry-log',
status: {
icon: {
label: translate("NOT FOUND"),
type: "ERROR"
}
}
})
addNewLogEntry(setLogEntries, {
message: translate("Account not found in TOML file. Domain can not be verified."),
id: statusLogId,
status: {
icon: {
label: translate("VALIDATION FAILED"),
type: "ERROR",
}
}
})
}
} else {
// The final validation message is displayed under the validated account since in this case we're
// verifying a wallet address, not the toml file itself.
await validateAndDisplayFields(setLogEntries, translate(accountHeader), parsed.ACCOUNTS, domain)
// We then display the rest of the toml as additional information
await validateAndDisplayFields(setLogEntries, translate("Validators:"), parsed.VALIDATORS)
await validateAndDisplayFields(setLogEntries, translate("Principals:"), parsed.PRINCIPALS)
await validateAndDisplayFields(setLogEntries, translate("Servers:"), parsed.SERVERS)
await validateAndDisplayFields(setLogEntries, translate("Currencies:"), parsed.CURRENCIES)
}
}
/**
* Convert HTML error odes to status messages to display.
*
* @param status - HTML Error code
* @returns A human readable explanation for the HTML based on error code categories.
*/
function getHttpErrorCode(status?: number) {
let errCode;
if(status === 408) {
errCode = 'TIMEOUT'
} else if(status >= 400 && status < 500) {
errCode = 'CLIENT ERROR'
} else if (status >= 500 && status < 600) {
errCode = 'SERVER ERROR'
} else {
errCode = 'UNKNOWN'
}
return errCode
}
/**
* Extract and parse a toml file from a url derived via domain. If accountToVerify is
* passed in, this specifically verifies that address is in the toml file.
* For verifying a wallet, this is the 2nd step. For verifying a toml file itself, this is the 1st step.
*
* @param setLogEntries - A setter to update the log files.
* @param domain = The main section of a url - ex. validator.xrpl-labs.com
* @param accountToVerify - A wallet to optionally specifically check for.
*/
export async function fetchFile(
setLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,
domain: string,
accountToVerify?: string) {
const { translate } = useTranslate()
const url = "https://" + domain + TOML_PATH
const checkUrlId = `check-url-log`
const logEntry = {
message: translate(`Checking ${url} ...`),
id: checkUrlId,
}
addNewLogEntry(setLogEntries, logEntry)
try {
const response = await axios.get(url)
const data: string = response.data
updateLogEntry(setLogEntries, {...logEntry, status: {
icon: {
label: translate("FOUND"),
type: "SUCCESS",
},
}})
// Continue to the next step of verification
parseXRPLToml(setLogEntries, data, accountToVerify, domain)
} catch (e) {
const errorUpdate: LogEntryItem = {...logEntry, status: {
icon: {
label: translate(getHttpErrorCode((e as AxiosError)?.status)),
type: "ERROR",
},
followUpMessage: (<p>
{translate("Check if the file is actually hosted at the URL above, ")
+ translate("check your server's HTTPS settings and certificate, and make sure your server provides the required ")}
<a href="xrp-ledger-toml.html#cors-setup">{translate("CORS header.")}</a>
</p>)
}}
updateLogEntry(setLogEntries, errorUpdate)
}
}
/**
* Helper to display the result of trying to decode the domain decoding.
*
* @param setAccountLogEntries - A setter to update the displayed logs.
*/
function displayDecodedWalletLog(
setAccountLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,) {
const { translate } = useTranslate()
const logId = 'decoding-domain-hex'
addNewLogEntry(setAccountLogEntries, {
message: translate('Decoding domain hex'),
id: logId,
status: {
icon: {
label: translate('SUCCESS'),
type: 'SUCCESS',
},
}
})
}
/**
* Decode ascii hex into a string.
*
* @param hex - a hex string encoded in ascii.
* @returns The decoded string
*/
function decodeHexWallet(hex: string): string {
let decodedDomain = '';
for (let i = 0; i < hex.length; i += 2) {
decodedDomain += String.fromCharCode(parseInt(hex.substring(i, i + 2), 16))
}
return decodedDomain
}
/**
* The first step to verify an XRPL Wallet is verified with a toml file.
* Looks up the domain associated with the given accountToVerify and the status on success or failure.
*
* @param accountToVerify
* @param setAccountLogEntries
* @param socket
*/
export function fetchWallet(
setAccountLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,
accountToVerify: string,
socket?: WebSocket)
{
const {translate} = useTranslate()
// Reset the logs
setAccountLogEntries([])
const walletLogEntry = {
message: translate(`Checking domain of account`),
id: 'check-domain-account',
}
addNewLogEntry(setAccountLogEntries, walletLogEntry)
const url = "wss://xrplcluster.com"
if (typeof socket !== "undefined" && socket.readyState < 2) {
socket.close()
}
const data = {
"command": "account_info",
"account": accountToVerify,
}
socket = new WebSocket(url)
socket.addEventListener('message', (event) => {
let data;
// Defaults to error to simplify logic later on
let response: LogEntryStatus = {
icon: {
label: translate(`ERROR`),
type: `ERROR`,
},
};
try {
data = JSON.parse(event.data)
if (data.status === 'success') {
if (data.result.account_data.Domain) {
try {
response = {
icon: {
label: translate('SUCCESS'),
type: 'SUCCESS',
},
}
// Continue to the next step of validation
const decodedDomain = decodeHexWallet(data.result.account_data.Domain)
displayDecodedWalletLog(setAccountLogEntries)
fetchFile(setAccountLogEntries, decodedDomain, accountToVerify)
} catch(e) {
console.log(e)
response.followUpMessage = <p>{translate(`Error decoding domain field: ${data.result.account_data.Domain}`)}</p>
}
} else {
response.followUpMessage = <p>{translate("Make sure the account has the Domain field set.")}</p>
}
} else {
response.followUpMessage = <p>{translate("Make sure you are entering a valid XRP Ledger address.")}</p>
}
updateLogEntry(setAccountLogEntries, { ...walletLogEntry, status: response })
} catch {
socket.close()
return false
}
})
socket.addEventListener('open', () => {
socket.send(JSON.stringify(data))
})
}

View File

@@ -0,0 +1,49 @@
export const TOML_PATH = "/.well-known/xrp-ledger.toml"
export interface AccountFields {
address: string,
network: string,
desc: string
}
export interface ValidatorFields {
public_key: string,
network: string,
owner_country: string,
server_country: string,
unl: string
}
export interface PrincipalFields {
name: string,
email: string,
}
export interface ServerFields {
json_rpc: string,
ws: string,
peer: string,
network: string,
}
export interface CurrencyFields {
code: string,
display_decimals: string,
issuer: string,
network: string,
symbol: string
}
export interface MetadataField {
// TODO: There could be other fields here, but this is all the existing code used
modified: Date
}
export interface XrplToml {
ACCOUNTS?: AccountFields[],
VALIDATORS?: ValidatorFields[],
PRINCIPALS?: PrincipalFields[],
SERVERS?: ServerFields[],
CURRENCIES?: CurrencyFields[],
METADATA?: MetadataField
}

View File

@@ -0,0 +1,440 @@
import * as React from 'react';
import { useState } from 'react'
import { useTranslate } from '@portal/hooks';
import AlertTemplate from './components/AlertTemplate';
import { transitions, positions, Provider as AlertProvider } from 'react-alert'
import { useAlert } from 'react-alert'
import { isoTimeToRippleTime, type Client, type Wallet } from 'xrpl'
import { errorNotif, SubmitConstData, timeout, submitAndUpdateUI } from './utils';
import { InitButton } from './components/InitButton';
import { DestinationAddressInput } from './components/DestinationAddressInput';
import { StatusSidebar } from './components/StatusSidebar';
import { TransactionButton } from './components/TransactionButton';
export const frontmatter = {
seo: {
title: 'Transaction Sender',
description: "Send test transactions to the account of your choice to test how your software handles them.",
}
};
async function onClickCreateEscrow(
submitConstData: SubmitConstData,
sendingWallet: Wallet | undefined,
destinationAddress: string,
durationSeconds: number,
setEscrowWidthPercent: React.Dispatch<React.SetStateAction<number>>,
alsoSendEscrowFinish: boolean) {
if (Number.isNaN(durationSeconds) || durationSeconds < 1) {
errorNotif(submitConstData.alert, "Error: Escrow duration must be a positive number of seconds")
return
}
// This should never happen
if(sendingWallet === undefined) {
errorNotif(submitConstData.alert, "Error: No sending wallet specified, so unable to submit EscrowCreate")
return
}
const finishAfter = isoTimeToRippleTime(new Date()) + durationSeconds
const escrowCreateResponse = await submitAndUpdateUI(submitConstData, sendingWallet, {
TransactionType: "EscrowCreate",
Account: sendingWallet.address,
Destination: destinationAddress,
Amount: "1000000",
FinishAfter: finishAfter
})
if (escrowCreateResponse && alsoSendEscrowFinish) {
// Wait until there's a ledger with a close time > FinishAfter
// to submit the EscrowFinish
setEscrowWidthPercent(1)
const { client } = submitConstData
let latestCloseTime = -1
while (latestCloseTime <= finishAfter) {
const secondsLeft = (finishAfter - isoTimeToRippleTime(new Date()))
setEscrowWidthPercent(Math.min(99, Math.max(0, (1-(secondsLeft / durationSeconds)) * 100)))
if (secondsLeft <= 0) {
// System time has advanced past FinishAfter. But is there a new
// enough validated ledger?
latestCloseTime = (await client.request({
command: "ledger",
"ledger_index": "validated"}
)).result.ledger.close_time
}
// Update the progress bar & check again in 1 second.
await timeout(1000)
}
setEscrowWidthPercent(0)
if(escrowCreateResponse.result.Sequence === undefined) {
errorNotif(submitConstData.alert,
"Error: Unable to get the sequence number from EscrowCreate, so cannot submit an EscrowFinish transaction.")
console.error(`EscrowCreate did not return a sequence number.
This may be because we were unable to look up the transaction in a validated ledger.
The EscrowCreate response was ${escrowCreateResponse}`)
} else {
// Now submit the EscrowFinish
// Future feature: submit from a different sender, just to prove that
// escrows can be finished by a third party
await submitAndUpdateUI(submitConstData, sendingWallet, {
Account: sendingWallet.address,
TransactionType: "EscrowFinish",
Owner: sendingWallet.address,
OfferSequence: escrowCreateResponse.result.Sequence
})
}
}
// Reset in case they click the button again
setEscrowWidthPercent(0)
}
function TxSenderBody(): React.JSX.Element {
const { translate } = useTranslate();
const [client, setClient] = useState<Client | undefined>(undefined)
const alert = useAlert()
// Sidebar variables
const [balance, setBalance] = useState(0)
const [sendingWallet, setSendingWallet] = useState<Wallet | undefined>(undefined)
const [connectionReady, setConnectionReady] = useState(false)
const [txHistory, setTxHistory] = useState([])
// Used when submitting transactions to trace all transactions in the UI
// We cast here since client may be undefined to begin with, but will never be undefined
// When actually used since all buttons / transactions are disallowed before the Inititalization
// function where Client is defined. (This saves us many unnecessary type assertions later on)
const submitConstData = {
client,
setBalance,
setTxHistory,
alert,
} as SubmitConstData
// Manage the destinationAddress
const defaultDestinationAddress = "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"
const [destinationAddress, setDestinationAddress] = useState(defaultDestinationAddress)
const [isInitEnabled, setIsInitEnabled] = useState(true)
// Partial Payment variables
const [ppWidthPercent, setPpWidthPercent] = useState(0)
const [ppIssuerWallet, setPpIssuerWallet] = useState<Wallet | undefined>(undefined)
const ppCurrencyCode = "BAR"
const partialPaymentParams = {
setPpIssuerWallet,
setPpWidthPercent,
ppCurrencyCode,
}
// Payment button variables
const defaultDropsToSend = 100000
const [dropsToSendForPayment, setDropsToSendForPayment] = useState(defaultDropsToSend)
// Escrow variables
const defaultFinishAfter = 60
const [finishAfter, setFinishAfter] = useState(defaultFinishAfter)
const [finishEscrowAutomatically, setFinishEscrowAutomatically] = useState(false)
const [escrowWidthPercent, setEscrowWidthPercent] = useState(0)
// Payment Channel variables
const defaultPaymentChannelAmount = 100000
const [paymentChannelAmount, setPaymentChannelAmount] = useState(defaultPaymentChannelAmount)
// Issued Currency / Trust Line Variables
const trustCurrencyCode = "FOO"
const defaultIssueAmount = 100
const [issueAmount, setIssueAmount] = useState(defaultIssueAmount)
const defaultTrustLimit = 100000
const [trustLimit, setTrustLimit] = useState(defaultTrustLimit)
const commonTxButtonParams = {
submitConstData,
connectionReady,
sendingWallet
}
return (
<div className="row">
<StatusSidebar balance={balance} sendingWallet={sendingWallet} connectionReady={connectionReady} txHistory={txHistory}/>
<main className="main col-md-7 col-lg-6 order-md-3 page-tx-sender" role="main" id="main_content_body">
<section className="container-fluid pt-3 p-md-3">
<h1>{translate("Transaction Sender")}</h1>
<div className="content">
<p>{translate("This tool sends transactions to the ")}
<a href="dev-tools/xrp-faucets">{translate("XRP Testnet")}</a>
{translate(" address of your choice so you can test how you monitor and respond to incoming transactions.")}
</p>
<form>
<InitButton
isInitEnabled={isInitEnabled}
toInit={{
existingClient: client,
alert,
setClient,
setBalance,
setSendingWallet,
setIsInitEnabled,
setConnectionReady,
partialPaymentParams
}}/>
<DestinationAddressInput
{...{defaultDestinationAddress,
destinationAddress,
setDestinationAddress,
}}/>
<h3>{translate("Send Transaction")}</h3>
{/* Send Payment */}
<TransactionButton
id="send_xrp_payment"
{...commonTxButtonParams}
transaction={
{
TransactionType: "Payment",
// @ts-expect-error - sendingWallet is guaranteed to be defined by the time this button is clicked.
Account: sendingWallet?.address,
Destination: destinationAddress,
Amount: dropsToSendForPayment.toString()
}}
content=
{{
buttonText: "Send XRP Payment",
units: "drops of XRP",
longerDescription: (<div>{translate("Send a ")}<a href="send-xrp.html">{translate("simple XRP-to-XRP payment")}</a>{translate(".")}</div>),
}}
inputSettings={
{
defaultValue: defaultDropsToSend,
setInputValue: setDropsToSendForPayment,
min: 1,
max: 10000000000,
}}
/>
{/* Partial Payments */}
<TransactionButton
id="send_partial_payment"
{...commonTxButtonParams}
transaction={
{
TransactionType: "Payment",
// @ts-expect-error - sendingWallet is guaranteed to be defined by the time this button is clicked.
Account: sendingWallet?.address,
Destination: destinationAddress,
Amount: "1000000000000000", // 1 billion XRP
SendMax: {
value: (Math.random()*.01).toPrecision(15), // random very small amount
currency: ppCurrencyCode,
// @ts-expect-error - ppIssuerWallet is guaranteed to be defined by the time this button is clicked.
issuer: ppIssuerWallet?.address
},
Flags: 0x00020000 // tfPartialPayment
}}
content=
{{
buttonText: "Send Partial Payment",
units: "drops of XRP",
longerDescription: <div>{translate("Deliver a small amount of XRP with a large ")}
<code>{translate("Amount")}</code>{translate(" value, to test your handling of ")}
<a href="partial-payments.html">{translate("partial payments")}</a>{translate(".")}</div>,
buttonTitle: "(Please wait for partial payments setup to finish)",
}}
loadingBar={{
id: "pp_progress",
widthPercent: ppWidthPercent,
description: "(Getting ready to send partial payments)",
defaultOn: true,
}}
/>
{/* Escrow */}
<TransactionButton
id="create_escrow"
{...commonTxButtonParams}
transaction={
{
TransactionType: "EscrowCreate",
// @ts-expect-error - sendingWallet is guaranteed to be defined by the time this button is clicked.
Account: sendingWallet?.address,
Destination: destinationAddress,
Amount: "1000000",
FinishAfter: isoTimeToRippleTime(new Date()) + finishAfter
}}
content=
{{
buttonText: translate("Create Escrow"),
units: translate("seconds"),
longerDescription: (<div>{translate("Create a ")}<a href="escrow.html">{translate("time-based escrow")}</a>
{translate(" of 1 XRP for the specified number of seconds.")}</div>),
}}
inputSettings={
{
defaultValue: defaultFinishAfter,
setInputValue: setFinishAfter,
min: 5,
max: 10000,
}}
loadingBar={{
id: "escrow_progress",
widthPercent: escrowWidthPercent,
description: translate("(Waiting to release Escrow when it's ready)"),
defaultOn: false,
}}
checkBox={{
setCheckValue: setFinishEscrowAutomatically,
defaultValue: finishEscrowAutomatically,
description: translate("Finish automatically"),
}}
customOnClick={() => onClickCreateEscrow(
submitConstData,
sendingWallet,
destinationAddress,
finishAfter,
setEscrowWidthPercent,
finishEscrowAutomatically)}
/>
{/* Payment Channels
- Future feature: figure out channel ID and enable a button that creates
valid claims for the given payment channel to help test redeeming
*/}
<TransactionButton
id="create_payment_channel"
{...commonTxButtonParams}
transaction={{
TransactionType: "PaymentChannelCreate",
// @ts-expect-error - sendingWallet is guaranteed to be defined by the time this button is clicked.
Account: sendingWallet?.address,
Destination: destinationAddress,
Amount: paymentChannelAmount.toString(),
SettleDelay: 30,
// @ts-expect-error - sendingWallet is guaranteed to be defined by the time this button is clicked.
PublicKey: sendingWallet?.publicKey
}}
content={{
buttonText: translate("Create Payment Channel"),
units: translate("drops of XRP"),
longerDescription: (<div>{translate("Create a ")}<a href="payment-channels.html">{translate("payment channel")}</a>
{translate(" and fund it with the specified amount of XRP.")}</div>),
}}
inputSettings={
{
defaultValue: defaultPaymentChannelAmount,
setInputValue: setPaymentChannelAmount,
min: 1,
max: 10000000000,
}}
/>
{/* Send Issued Currency */}
{/* Future feature: Add ability to configure custom currency codes */}
<TransactionButton
id="send_issued_currency"
{...commonTxButtonParams}
transaction={
{
TransactionType: "Payment",
// @ts-expect-error - sendingWallet is guaranteed to be defined by the time this button is clicked.
Account: sendingWallet?.address,
Destination: destinationAddress,
Amount: {
currency: trustCurrencyCode,
value: issueAmount?.toString(),
// @ts-expect-error - sendingWallet is guaranteed to be defined by the time this button is clicked.
issuer: sendingWallet?.address
}
}}
content={{
buttonText: translate("Send Issued Currency"),
units: translate(trustCurrencyCode),
longerDescription: (<div>{translate("Your destination address needs a ")}
<a href="trust-lines-and-issuing.html">{translate("trust line")}</a>{translate(" to ")}
<span className="sending-address-item">{translate("(the test sender)")}</span>
{translate(" for the currency in question. Otherwise, you'll get tecPATH_DRY.")}</div>),
}}
inputSettings={
{
defaultValue: defaultIssueAmount,
setInputValue: setIssueAmount,
min: 1,
max: 10000000000,
}}
/>
{/* Create Trust Line */}
<TransactionButton
id="trust_for"
{...commonTxButtonParams}
transaction={
{
TransactionType: "TrustSet",
// @ts-expect-error - sendingWallet is guaranteed to be defined by the time this button is clicked.
Account: sendingWallet?.address,
LimitAmount: {
currency: trustCurrencyCode,
value: trustLimit.toString(),
issuer: destinationAddress
}
}}
content={{
buttonText: translate("Trust for"),
units: translate(trustCurrencyCode),
longerDescription: (<div>{translate("The test sender creates a ")}
<a href="trust-lines-and-issuing.html">{translate("trust line")}</a>
{translate(" to your account for the given currency.")}</div>),
}}
inputSettings={
{
defaultValue: defaultTrustLimit,
setInputValue: setTrustLimit,
min: 1,
max: 10000000000,
}}
/>
</form>
</div>
</section>
</main>
</div>
)
}
// Wrapper to allow for dynamic alerts when transactions complete
export default function TxSender(): React.JSX.Element {
const alertOptions = {
position: positions.BOTTOM_RIGHT,
timeout: 7000,
offset: '8px',
transition: transitions.FADE
}
return (
<AlertProvider template={AlertTemplate} {...alertOptions}>
<TxSenderBody/>
</AlertProvider>
)
}

View File

@@ -0,0 +1,100 @@
import * as React from 'react'
import { type Client, type Wallet, type Transaction, type TransactionMetadata, type TxResponse, SubmittableTransaction } from 'xrpl'
import { clsx } from 'clsx'
export const TESTNET_URL = "wss://s.altnet.rippletest.net:51233"
export function timeout(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Displaying transaction data
export function errorNotif(alert: any, msg: string): void {
console.log(msg)
alert.error(msg)
}
export function successNotif(alert: any, msg: string): void {
console.log(msg)
alert.show(msg, { type: 'success' })
}
export function logTx(txName: string, hash: string, finalResult: string, setTxHistory: React.Dispatch<React.SetStateAction<React.JSX.Element[]>>) {
let classes
let icon
const txLink = "https://testnet.xrpl.org/transactions/" + hash
if (finalResult === "tesSUCCESS") {
classes = "text-muted"
icon = <i className="fa fa-check-circle"/>
} else {
classes = "list-group-item-danger"
icon = <i className="fa fa-times-circle"/>
}
const li = <li key={hash} className={clsx("list-group-item fade-in p-1", classes)}>
{icon} {txName}: <a href={txLink} target="_blank" className="external-link">{hash}</a>
</li>
setTxHistory((prevState) => [li].concat(prevState))
}
// All unchanging information needed to submit & log data
export interface SubmitConstData {
client: Client,
setBalance: React.Dispatch<React.SetStateAction<number>>,
setTxHistory: React.Dispatch<React.SetStateAction<React.JSX.Element[]>>,
alert: any,
}
export async function submitAndUpdateUI(
submitConstData: SubmitConstData,
sendingWallet: Wallet,
tx: SubmittableTransaction,
silent: boolean = false): Promise<TxResponse<Transaction> | undefined> {
const { client, setBalance, setTxHistory } = submitConstData
let prepared;
try {
// Auto-fill fields like Fee and Sequence
prepared = await client.autofill(tx)
console.debug("Prepared:", prepared)
} catch(error) {
console.log(error)
if (!silent) {
errorNotif(alert, "Error preparing tx: "+error)
}
return
}
try {
const {tx_blob, hash} = sendingWallet.sign(prepared)
const result = await client.submitAndWait(tx_blob)
console.log("The result of submitAndWait is ", result)
let finalResult = (result.result.meta as TransactionMetadata).TransactionResult
if (!silent) {
if (finalResult === "tesSUCCESS") {
successNotif(submitConstData.alert, `${tx.TransactionType} tx succeeded (hash: ${hash})`)
} else {
errorNotif(submitConstData.alert, `${tx.TransactionType} tx failed with code ${finalResult}
(hash: ${hash})`)
}
logTx(tx.TransactionType, hash, finalResult, setTxHistory)
}
setBalance(await client.getXrpBalance(sendingWallet.address))
return result
} catch (error) {
console.log(error)
if (!silent) {
errorNotif(submitConstData.alert, `Error signing & submitting ${tx.TransactionType} tx: ${error}`)
}
setBalance(await client.getXrpBalance(sendingWallet.address))
return
}
}
export function canSendTransaction(connectionReady: boolean, sendingAddress: string | undefined): boolean {
return connectionReady && !!sendingAddress
}

View File

@@ -0,0 +1,370 @@
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 const frontmatter = {
seo: {
title: 'WebSocket API Tool',
description: "Interact directly with XRP Ledger servers using the WebSocket API with this handy tool.",
}
};
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

@@ -0,0 +1,210 @@
import * as React from 'react';
import { useTranslate } from '@portal/hooks';
import { useState } from 'react';
import { Client, dropsToXrp, Wallet } from 'xrpl';
import * as faucetData from './faucets.json'
import XRPLoader from '../../@theme/components/XRPLoader';
export const frontmatter = {
seo: {
title: 'XRP Faucets',
description: "Get test XRP for use on various non-production networks.",
}
};
interface FaucetInfo {
id: string,
wsUrl: string,
jsonRpcUrl: string,
faucetUrl: string,
shortName: string,
desc: string,
}
async function waitForSequence(client: Client, address: string):
Promise<{ sequence: string, balance: string }>
{
let response;
while (true) {
try {
response = await client.request({
command: "account_info",
account: address,
ledger_index: "validated"
})
break
} catch(e) {
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
console.log(response)
return { sequence: response.result.account_data.Sequence, balance: response.result.account_data.Balance}
}
function FaucetEndpoints({ faucet, givenKey } : { faucet: FaucetInfo, givenKey: string}) {
const { translate } = useTranslate();
return (<div key={givenKey}>
<h4>{translate(`${faucet.shortName} Servers`)}</h4>
<pre>
<code>
// WebSocket<br/>
{faucet.wsUrl}<br/>
<br/>
// JSON-RPC<br/>
{faucet.jsonRpcUrl}
</code>
</pre>
</div>)
}
function FaucetSidebar({ faucets }: { faucets: FaucetInfo[] }): React.JSX.Element {
return (<aside className="right-sidebar col-lg-6 order-lg-4" role="complementary">
{faucets.map(
(faucet) => <FaucetEndpoints faucet={faucet} key={faucet.shortName + " Endpoints"} givenKey={faucet.shortName + " Endpoints"}/>
)}
</aside>)
}
export default function XRPFaucets(): React.JSX.Element {
const { translate } = useTranslate();
const faucets: FaucetInfo[] = faucetData.knownFaucets
const [selectedFaucet, setSelectedFaucet] = useState(faucets[0])
return (
<div className="container-fluid" role="document" id="main_content_wrapper">
<div className="row">
<FaucetSidebar faucets={faucets}/>
<main className="main col-md-7 col-lg-6 order-md-3" role="main" id="main_content_body">
<section className="container-fluid pt-3 p-md-3">
<h1>{translate("XRP Faucets")}</h1>
<div className="content">
<p>{translate("These ")}<a href="parallel-networks.html">{translate("parallel XRP Ledger test networks")}</a> {translate("provide platforms for testing changes to the XRP Ledger and software built on it, without using real funds.")}</p>
<p>{translate("These funds are intended for")} <strong>{translate("testing")}</strong> {translate("only. Test networks' ledger history and balances are reset as necessary. Devnets may be reset without warning.")}</p>
<p>{translate("All balances and XRP on these networks are separate from Mainnet. As a precaution, do not use the Testnet or Devnet credentials on the Mainnet.")}</p>
<h3>{translate("Choose Network:")}</h3>
{ faucets.map((net) => (
<div className="form-check" key={"network-" + net.shortName}>
<input onChange={() => setSelectedFaucet(net)} className="form-check-input" type="radio"
name="faucet-selector" id={net.id} checked={selectedFaucet.shortName == net.shortName} />
<label className="form-check-label" htmlFor={net.id}>
<strong>{translate(net.shortName)}</strong>: {translate(net.desc)}
</label>
</div>
)) }
<br/>
<TestCredentials selectedFaucet={selectedFaucet}/>
</div>
</section>
</main>
</div>
</div>
)
}
async function generateFaucetCredentialsAndUpdateUI(
selectedFaucet: FaucetInfo,
setButtonClicked: React.Dispatch<React.SetStateAction<boolean>>,
setGeneratedCredentialsFaucet: React.Dispatch<React.SetStateAction<string>>,
setAddress: React.Dispatch<React.SetStateAction<string>>,
setSecret: React.Dispatch<React.SetStateAction<string>>,
setBalance: React.Dispatch<React.SetStateAction<string>>,
setSequence: React.Dispatch<React.SetStateAction<string>>): Promise<void> {
setButtonClicked(true)
// Clear existing credentials
setGeneratedCredentialsFaucet(selectedFaucet.shortName)
setAddress("")
setSecret("")
setBalance("")
setSequence("")
const { translate } = useTranslate();
const wallet = Wallet.generate()
const client = new Client(selectedFaucet.wsUrl)
await client.connect()
try {
setAddress(wallet.address)
setSecret(wallet.seed)
await client.fundWallet(wallet, { faucetHost: selectedFaucet.faucetUrl, usageContext: "xrpl.org-faucet" })
const response = await waitForSequence(client, wallet.address)
setSequence(response.sequence)
setBalance(response.balance)
} catch (e) {
alert(translate(`There was an error with the ${selectedFaucet.shortName} faucet. Please try again.`))
}
setButtonClicked(false)
}
function TestCredentials({selectedFaucet}) {
const { translate } = useTranslate();
const [generatedCredentialsFaucet, setGeneratedCredentialsFaucet] = useState("")
const [address, setAddress] = useState("")
const [secret, setSecret] = useState("")
const [balance, setBalance] = useState("")
const [sequence, setSequence] = useState("")
const [buttonClicked, setButtonClicked] = useState(false)
return (<div>
{/* <XRPLGuard> TODO: Re-add this once we find a good way to avoid browser/server mismatch errors */}
<div className="btn-toolbar" role="toolbar" aria-label="Button">
<button id="generate-creds-button" onClick={
() => generateFaucetCredentialsAndUpdateUI(
selectedFaucet,
setButtonClicked,
setGeneratedCredentialsFaucet,
setAddress,
setSecret,
setBalance,
setSequence)
} className="btn btn-primary mr-2 mb-2">
{translate(`Generate ${selectedFaucet.shortName} credentials`)}
</button>
</div>
{/* </XRPLGuard> */}
{generatedCredentialsFaucet && <div id="your-credentials">
<h2>{translate(`Your ${generatedCredentialsFaucet} Credentials`)}</h2>
</div>}
{(buttonClicked && address === "") && <XRPLoader message={translate("Generating keys..")}/>}
{address && <div id="address"><h3>{translate("Address")}</h3>{address}</div>}
{secret && <div id="secret"><h3>{translate("Secret")}</h3>{secret}</div>}
{(address && !balance) && (<div>
<br/>
<XRPLoader message={translate("Funding account...")}/>
</div>)}
{balance && <div id="balance">
<h3>{translate("Balance")}</h3>
{dropsToXrp(balance).toLocaleString("en")} {translate("XRP")}
</div>}
{sequence && <div id="sequence">
<h3>{translate("Sequence Number")}</h3>
{sequence}
</div>}
{(secret && !sequence) && <XRPLoader message={translate("Waiting...")}/>}
</div>
)
}

View File

@@ -0,0 +1,87 @@
import * as React from 'react';
import { useTranslate } from '@portal/hooks';
import { TextLookupForm, type TextLookupFormProps } from './components/TextLookupForm';
import { fetchFile, fetchWallet } from './toml-checker/ValidateTomlSteps';
import { LogEntryItem } from './components/LogEntry';
export const frontmatter = {
seo: {
title: 'xrp-ledger.toml Checker',
description: "Confirm that your site's xrp-ledger.toml file is set up correctly.",
}
};
/**
* Example data to test the tool with
*
* Domains:
* - Valid: validator.xrpl-labs.com
* - Not valid: sologenic.com
*
* Addresses:
* - Valid: rSTAYKxF2K77ZLZ8GoAwTqPGaphAqMyXV
* - No toml: rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz
* - No domain: rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh
*/
function handleSubmitWallet(
setAccountLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,
event: React.FormEvent<HTMLFormElement>,
addressToVerify: string) {
event.preventDefault()
setAccountLogEntries([])
fetchWallet(setAccountLogEntries, addressToVerify)
}
function handleSubmitDomain(
setDomainLogEntries: React.Dispatch<React.SetStateAction<LogEntryItem[]>>,
event: React.FormEvent<HTMLFormElement>,
domainAddress: string) {
event.preventDefault();
setDomainLogEntries([])
fetchFile(setDomainLogEntries, domainAddress)
}
export default function TomlChecker() {
const { translate } = useTranslate();
const domainButtonProps: TextLookupFormProps = {
title: `Look Up By Domain`,
description: <p>{translate(`This tool allows you to verify that your `)}<code>{translate(`xrp-ledger.toml`)}</code>
{translate(` file is syntactically correct and deployed properly.`)}</p>,
buttonDescription: `Check toml file`,
formPlaceholder: "example.com (Domain name to check)",
handleSubmit: handleSubmitDomain,
}
const addressButtonProps: TextLookupFormProps = {
title: `Look Up By Account`,
description: <p>{translate(`Enter an XRP Ledger address to see if that account is claimed by the domain it says owns it.`)}</p>,
buttonDescription: `Check account`,
formPlaceholder: `r... (${translate("Wallet Address to check")})`,
handleSubmit: handleSubmitWallet
}
return (
<div className="toml-checker row">
{/* This aside is empty but it keeps the formatting similar to other pages */}
<aside className="right-sidebar col-lg-3 order-lg-4" role="complementary"/>
<main className="main col-lg-9" role="main" id="main_content_body">
<section className="container-fluid">
<div className="p-3">
<h1>{translate(`xrp-ledger.toml Checker`)}</h1>
<p>{translate(`If you run an XRP Ledger validator or use the XRP Ledger for your business,
you can provide information about your usage of the XRP Ledger to the world in a machine-readable `)}
<a href="https://xrpl.org/xrp-ledger-toml.html"><code>{translate(`xrp-ledger.toml`)}</code>{translate(` file`)}</a>.</p>
</div>
<TextLookupForm {...domainButtonProps} />
<TextLookupForm {...addressButtonProps} />
</section>
</main>
</div>
)
}

12
resources/index.md Normal file
View File

@@ -0,0 +1,12 @@
---
html: resources.html
parent: index.html
metadata:
indexPage: true
---
# Resources
Other resources to help understand the XRP Ledger and develop on it.
{% child-pages /%}

File diff suppressed because it is too large Load Diff