Files
xrpl-dev-portal/resources/dev-tools/tx-sender.page.tsx
2025-07-02 12:32:52 +03:00

454 lines
22 KiB
TypeScript

import * as React from 'react';
import { useState } from 'react'
import { useThemeHooks } from '@redocly/theme/core/hooks';
import { Link } from '@redocly/theme/components/Link/Link';
import { toast, Toaster } from 'react-hot-toast';
import AlertTemplate from './components/AlertTemplate';
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.tx_json.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.tx_json.Sequence
})
}
}
// Reset in case they click the button again
setEscrowWidthPercent(0)
}
function TxSenderBody({ alert }: { alert: (message: string, type?: string) => void }): React.JSX.Element {
const { useTranslate } = useThemeHooks();
const { translate } = useTranslate();
const [client, setClient] = useState<Client | undefined>(undefined)
// 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("resources.dev-tool.tx-sender.content.part1", "This tool sends transactions to the ")}
<a href="../xrp-faucets">{translate("XRP Testnet")}</a>
{translate("resources.dev-tool.tx-sender.content.part2", " 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("resources.dev-tool.tx-sender.send-xrp-desc.part1", "Send a ")}<Link to="../../docs/tutorials/how-tos/send-xrp">{translate("resources.dev-tool.tx-sender.send-xrp-desc.part2", "simple XRP-to-XRP payment")}</Link>{translate("resources.dev-tool.tx-sender.send-xrp-desc.part3", ".")}</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 ")}
<Link to="/docs/concepts/payment-types/partial-payments">{translate("partial payments")}</Link>{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 ")}<Link to="/docs/concepts/payment-types/escrow">{translate("time-based escrow")}</Link>
{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 ")}<Link to="../../docs/concepts/payment-types/payment-channels">{translate("payment channel")}</Link>
{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 ")}
<Link to="../../docs/concepts/tokens/fungible-tokens#trust-lines">{translate("trust line")}</Link>{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 ")}
<Link to="../../docs/concepts/tokens/fungible-tokens#trust-lines">{translate("trust line")}</Link>
{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 alert = (message: string, type: string = 'info') => {
toast.custom((t) => (
<AlertTemplate
message={message}
options={{ type }}
style={{}}
close={() => {
toast.dismiss(t.id);
toast.remove(t.id);
}}
/>
), {
duration: 7000,
position: 'bottom-right',
});
};
return (
<>
<TxSenderBody alert={alert} />
<Toaster
containerStyle={{
bottom: 8,
right: 8,
}}
/>
</>
)
}