Migrate the tx-sender page to Redocly

Update convert-template

Add basic page

Add it to the sidebar

Fix a broken link

Fix translate usage and add linebreaks

Fix indents

Add basic sidebar

Port over init button logic

Migrate submit_and_notify and start dest addr

Get the payment button working

Componentize the Send XRP Payment button

Add basic escrow button

Componentize payment channel

Migrate Trust For

Migrate Send Issued Currency

Add partial payment progres bar logic

Use the component for the partial payment

Add support for escrow finish

Log transactions in sidebar

Debugging partial payment setup

Add support for changing destinationAddress

Finish adding bootstrap growl notifications

Use 'client' instead of 'api'

Move DestinationAddressInput to component and remove ids

Split the page into separate files

Remove the old files for this page

Update links

Add space

Add comment deprecating bootstrap-growl jquery

Fix typing errors

PR Comments Pt 1

Small PR fixes

Encapsulate isValidDestinationAddress
This commit is contained in:
JST5000
2023-11-08 10:35:19 -08:00
committed by mDuo13
parent 64a91fc0a8
commit 0d0187b4ee
13 changed files with 1387 additions and 346 deletions

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,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,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,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<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>
)
}

100
content/dev-tools/utils.tsx Normal file
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

@@ -670,6 +670,8 @@
href: /dev-tools/xrp-faucets
page: /dev-tools/xrp-faucets.page.tsx
- label: Transaction Sender
href: /dev-tools/tx-sender
page: /dev-tools/tx-sender.page.tsx
- label: XRPL Learning Portal
href: https://learn.xrpl.org/
external: true

View File

@@ -22,6 +22,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
/**
* Replaced by react-alert after the Redocly migration.
* Please see tx-sender.page.tsx for an example of react-alerts in action.
*/
(function() {
var $;

View File

@@ -24,7 +24,7 @@ const mainBlock = content.substring(mainBlockOffset + 16, mainBlockEndOffset);
const classes = content.match(/{% block mainclasses %}(.+?){% endblock %}/)?.[1] || '';
const setStatements = mainBlock.match(/{% set ([\w\d]+) = ((.|\n)+?)%}/g);
const sets = setStatements.map(setStatement => {
const sets = setStatements?.map(setStatement => {
const setStatementParts = setStatement.split(' = ');
const setStatementName = setStatementParts[0].replace('{% set ', '');
const setStatementValue = setStatementParts[1].replace(/%}/g, '');
@@ -74,7 +74,7 @@ const jsxWithReplacedTranslate = jsxWithReplacedForLoops.replace(
const output = `import * as React from 'react';
import { useTranslate } from '@portal/hooks';
${sets.map(set => `const ${set.name} = ${set.value};`).join('\n\n')}
${sets?.map(set => `const ${set.name} = ${set.value};`).join('\n\n')}
const target= {prefix: ''}; // TODO: fixme

View File

@@ -11,8 +11,10 @@
"license": "MIT",
"dependencies": {
"@redocly/portal": "^0.66.0",
"clsx": "^2.0.0",
"lottie-react": "^2.4.0",
"moment": "^2.29.4",
"react-alert": "^7.0.3",
"xrpl": "^3.0.0-beta.1"
},
"overrides": {

View File

@@ -38,7 +38,7 @@
border: 1px solid $gray-200;
}
#pp_progress small {
.progress small {
margin-top: .5rem; // Fix "Getting ready to send..." position
}

File diff suppressed because it is too large Load Diff