import { NotepadText, Send, Trash, Settings2, ArrowBigRightDash, ArrowBigDownDash } from "lucide-react" import React, { useState, useEffect, useCallback, useMemo } from "react" import { useLocation } from "react-router-dom" import { lastValueFrom, tap } from "rxjs" import { ss58Decode } from "@polkadot-labs/hdkd-helpers" import { toHex } from "@polkadot-api/utils" import { useChainSpecV1, useSystemAccount, useUnstableProvider, useMetadata, useTransferCalldata, useExistentialDeposit } from "../hooks" import { submitTransaction$ } from "../api" import { FollowTransaction, TransactionError, TransactionHistory, } from "../types" import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "../components/ui/accordion" import { Input } from "../components/ui/input" import { Button } from "../components/ui/button" import { Sender, Receiver } from "./Accounts" export const Transactions = () => { const location = useLocation() const queryParams = new URLSearchParams(location.search) const initialReceiver = queryParams.get("address") ?? "" const [transactionHistory, setTransactionHistory] = useState( JSON.parse(localStorage.getItem('transactionHistory') ?? '[]') || [] ) const [historyLifetimeDuration, setHistoryLifetimeDuration] = useState( Number(localStorage.getItem("historyLifetimeDuration") ?? 259200) // default is 3 days ) const [historyMaxRecords, setHistoryMaxRecords] = useState( Number(localStorage.getItem("historyMaxRecords") ?? 5) ) const [defaultTransactAmount, setDefaultTransactAmount] = useState( localStorage.getItem("defaultTransactAmount") ?? "" ) const [transactionStatus, setTransactionStatus] = useState() const [error, setError] = useState() const [isSubmittingTransaction, setIsSubmittingTransaction] = useState(false) const [activeTab, onActiveTabChanged] = useState("transact") const [receiver, setReceiver] = useState(initialReceiver) const [amount, setAmount] = useState(defaultTransactAmount) const metadata = useMetadata() const existentialDeposit = useExistentialDeposit() const chainSpecV1 = useChainSpecV1() const tokenDecimals: number = chainSpecV1?.properties?.tokenDecimals ?? 0 const tokenSymbol: string = chainSpecV1?.properties?.tokenSymbol ?? "" const convertedAmount = useMemo(() => { try { return BigInt(Number(amount) * Math.pow(10, tokenDecimals)) } catch { return 0n } }, [amount, tokenDecimals]) const convertedTimestamp = ({ timestamp }: { timestamp: number }) => { const secondsInMinute = 60; const secondsInHour = secondsInMinute * 60; const secondsInDay = secondsInHour * 24; const days = Math.floor(timestamp / secondsInDay); const hours = Math.floor((timestamp % secondsInDay) / secondsInHour); const minutes = Math.floor((timestamp % secondsInHour) / secondsInMinute); const seconds = timestamp % secondsInMinute; return `${days} days, ${hours} hours, ${minutes} minutes, ${seconds} seconds`; } const { provider, clientFull, chainId, account, accounts, connectAccount } = useUnstableProvider() const receiverObject = useMemo(() => { try { const [, prefix] = ss58Decode(receiver) if (prefix !== 1995 && prefix !== 1996) { throw new Error("bad prefix") } return { isValid: true, address: receiver } } catch (e) { return { isValid: false, address: receiver } } }, [receiver]) const calldata = useTransferCalldata( receiverObject.isValid ? receiverObject.address : undefined, convertedAmount > 0n ? convertedAmount : undefined ) const senderAccount = useSystemAccount({ account: account ? account.address : undefined }) const receiverAccount = useSystemAccount({ account: receiverObject.isValid ? receiverObject.address : undefined }) const applyDecimals = (value = 0n, decimals = 0, tokenSymbol = "CSPR") => { if (!value) return `0 ${tokenSymbol}` const numberValue = Number(value) / Math.pow(10, decimals) const formatter = new Intl.NumberFormat("en-US", { minimumFractionDigits: 6, maximumFractionDigits: 6, }) return `${formatter.format(numberValue)} ${tokenSymbol}` } useEffect(() => { const deadline = Math.floor(Date.now() / 1000) + historyLifetimeDuration const cleanedTransactionHistory = transactionHistory.slice(-historyMaxRecords).filter(transaction => (transaction.timestamp ?? 0) < deadline && transaction.txHash ) localStorage.setItem("transactionHistory", JSON.stringify(cleanedTransactionHistory)) }, [transactionHistory, historyLifetimeDuration, historyMaxRecords]) useEffect(() => { localStorage.setItem("historyLifetimeDuration", historyLifetimeDuration.toString()) }, [historyLifetimeDuration]) useEffect(() => { localStorage.setItem("historyMaxRecords", historyMaxRecords.toString()) }, [historyMaxRecords]) useEffect(() => { localStorage.setItem("defaultTransactAmount", defaultTransactAmount) }, [defaultTransactAmount]) const handleOnTransfer = useCallback(async () => { setIsSubmittingTransaction(true) setTransactionStatus(undefined) setError(undefined) const transactStory: TransactionHistory = { sender: account?.address ?? "", receiver: receiverObject?.address ?? "", amount, timestamp: Math.floor(Date.now() / 1000), calldata: calldata ?? "", tokenSymbol: tokenSymbol ?? "", status: "Initiated" } try { const tx = await provider!.createTx( chainId ?? "", account ? toHex(ss58Decode(account.address)[0]) : "", calldata ?? "" ) await lastValueFrom( submitTransaction$(clientFull, tx) .pipe( tap(({ txEvent }) => { let status: string = "" switch (txEvent.type) { case "broadcasted": status = "broadcasted to available peers" transactStory.status = "Broadcasted" break case "txBestBlocksState": status = `included in block #${txEvent.block.number}` transactStory.blockNumber = txEvent.block.number transactStory.status = "Mined" break case "finalized": status = `finalized at block #${txEvent.block.number}` transactStory.blockNumber = txEvent.block.number transactStory.status = "Finalized" break case "throttled": status = "throttling to detect chain head..." transactStory.status = "Throttled" break } transactStory.txHash = txEvent.txHash transactStory.blockHash = txEvent.block?.hash setTransactionStatus({ status, hash: txEvent.block?.hash, }) }), ), ) } catch (err) { if (err instanceof Error) { const currentError = { type: "error", error: err.message } transactStory.error = currentError setError(currentError) } console.error(err) } finally { setAmount(defaultTransactAmount ?? "") setIsSubmittingTransaction(false) setTransactionHistory([...transactionHistory, transactStory]) } }, [ provider, chainId, account, amount, calldata, clientFull, defaultTransactAmount, receiverObject?.address, tokenSymbol, transactionHistory ]) return (

{activeTab === "transact" && ( <> acc?.address ?? "") ?? []} senderAccount={senderAccount} senderBalance={applyDecimals(senderAccount?.data.free ?? 0n, tokenDecimals, tokenSymbol)} tokenDecimals={tokenDecimals} tokenSymbol={tokenSymbol} connectAccount={connectAccount} applyDecimals={applyDecimals} /> {!error && transactionStatus && (

Transaction status: {`${transactionStatus.status}`}

{transactionStatus.hash && (

{transactionStatus.hash}

)}
)} {!error && !metadata && (

Downloading chain metadata...

)} {error && (

Error: {error.error}

)} )} {activeTab === "history" && (
{transactionHistory.length === 0 ? (
There are currently no stored transactions.
) : ( {transactionHistory.map((props: TransactionHistory) => (
{props.error && ( Error: {props.error?.error ?? ""} )} {!props.error && ( <> {props.status} at block #{props.blockNumber} Transfered amount: {props.amount} {props.tokenSymbol} )} Execution datetime: {new Date(props.timestamp * 1000).toLocaleString("en-US")}
Tx hash:
Block hash:
Calldata:
))}
) }
)} {activeTab === "settings" && (
Max lifetime:
{ const newValue = +e.target.value if (newValue) setHistoryLifetimeDuration(newValue) }} className="sm:w-[350px] w-full" /> {convertedTimestamp({ timestamp: historyLifetimeDuration})}
Max records: { const newValue = +e.target.value if (newValue) setHistoryMaxRecords(newValue) }} className="sm:w-[350px] w-full" />
Default amount: { setDefaultTransactAmount(e.target.value) setAmount(e.target.value) }} className="sm:w-[350px] w-full" placeholder="Amount will be empty" />
)}
) }