+
+ {/* Loading icon for when transaction is being submitted */}
+
+
+
+
+
+
+
+
+ {inputSettings &&
+ ) => {
+ // 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 &&
)
+}
diff --git a/content/dev-tools/tx-sender.page.tsx b/content/dev-tools/tx-sender.page.tsx
new file mode 100644
index 0000000000..0b77921b72
--- /dev/null
+++ b/content/dev-tools/tx-sender.page.tsx
@@ -0,0 +1,433 @@
+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';
+
+async function onClickCreateEscrow(
+ submitConstData: SubmitConstData,
+ sendingWallet: Wallet | undefined,
+ destinationAddress: string,
+ durationSeconds: number,
+ setEscrowWidthPercent: React.Dispatch>,
+ 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(undefined)
+
+ const alert = useAlert()
+
+ // Sidebar variables
+ const [balance, setBalance] = useState(0)
+ const [sendingWallet, setSendingWallet] = useState(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(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 (
+
+
+
+
+
+
{translate("Transaction Sender")}
+
+
{translate("This tool sends transactions to the ")}
+ {translate("XRP Testnet")}
+ {translate(" address of your choice so you can test how you monitor and respond to incoming transactions.")}
+
+
),
+ }}
+ inputSettings={
+ {
+ defaultValue: defaultDropsToSend,
+ setInputValue: setDropsToSendForPayment,
+ min: 1,
+ max: 10000000000,
+ }}
+ />
+
+ {/* Partial Payments */}
+ {translate("Deliver a small amount of XRP with a large ")}
+ {translate("Amount")}{translate(" value, to test your handling of ")}
+ {translate("partial payments")}{translate(".")}
,
+ 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 */}
+ {translate("Create a ")}{translate("time-based escrow")}
+ {translate(" of 1 XRP for the specified number of seconds.")}),
+ }}
+ 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
+ */}
+ {translate("Create a ")}{translate("payment channel")}
+ {translate(" and fund it with the specified amount of XRP.")}),
+ }}
+ inputSettings={
+ {
+ defaultValue: defaultPaymentChannelAmount,
+ setInputValue: setPaymentChannelAmount,
+ min: 1,
+ max: 10000000000,
+ }}
+ />
+
+ {/* Send Issued Currency */}
+ {/* Future feature: Add ability to configure custom currency codes */}
+ {translate("Your destination address needs a ")}
+ {translate("trust line")}{translate(" to ")}
+ {translate("(the test sender)")}
+ {translate(" for the currency in question. Otherwise, you'll get tecPATH_DRY.")}),
+ }}
+ inputSettings={
+ {
+ defaultValue: defaultIssueAmount,
+ setInputValue: setIssueAmount,
+ min: 1,
+ max: 10000000000,
+ }}
+ />
+
+ {/* Create Trust Line */}
+ {translate("The test sender creates a ")}
+ {translate("trust line")}
+ {translate(" to your account for the given currency.")}),
+ }}
+ inputSettings={
+ {
+ defaultValue: defaultTrustLimit,
+ setInputValue: setTrustLimit,
+ min: 1,
+ max: 10000000000,
+ }}
+ />
+
+
+
+
+
+ )
+}
+
+// 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 (
+
+
+
+ )
+}
diff --git a/content/dev-tools/utils.tsx b/content/dev-tools/utils.tsx
new file mode 100644
index 0000000000..8a3a838d2e
--- /dev/null
+++ b/content/dev-tools/utils.tsx
@@ -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 {
+ 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>) {
+ let classes
+ let icon
+ const txLink = "https://testnet.xrpl.org/transactions/" + hash
+ if (finalResult === "tesSUCCESS") {
+ classes = "text-muted"
+ icon =
+ } else {
+ classes = "list-group-item-danger"
+ icon =
+ }
+ const li =