mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2025-11-04 11:55:50 +00:00
454 lines
22 KiB
TypeScript
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,
|
|
}}
|
|
/>
|
|
</>
|
|
)
|
|
} |