import React, { useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react"; import { AgGridReact } from "ag-grid-react"; import { ColDef, RowClassParams, RowNode, RowStyle, SizeColumnsToContentStrategy, } from "ag-grid-community"; import "ag-grid-community/styles/ag-grid.css"; import "ag-grid-community/styles/ag-theme-alpine.css"; import InfoOutlineIcon from "@mui/icons-material/InfoOutline"; import { Alert, Box, Button, Checkbox, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControlLabel, IconButton, Snackbar, SnackbarCloseReason, Tooltip, Typography, } from "@mui/material"; import gameContext from "../../contexts/gameContext"; import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; import { useModal } from "../common/useModal"; import FileSaver from "file-saver"; import { Spacer } from "../common/Spacer"; import { Hourglass } from "react-loader-spinner"; import ErrorIcon from "@mui/icons-material/Error"; import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import { CountdownCircleTimer } from "react-countdown-circle-timer"; import { BuyContainer, BuyContainerDivider, BuyOrderBtn, MainContainer, } from "./Table-styles"; // export const baseLocalHost = window.location.host; export const baseLocalHost = "devnet-nodes.qortal.link:11111"; import CloseIcon from "@mui/icons-material/Close"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import moment from "moment"; const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); }; interface RowData { amountQORT: number; priceUSD: number; totalUSD: number; seller: string; } export const saveFileToDisk = async (data) => { const dataString = JSON.stringify(data); const blob = new Blob([dataString], { type: "application/json" }); const fileName = "traderecord_" + Date.now() + "_" + ".json"; await FileSaver.saveAs(blob, fileName); }; export const autoSizeStrategy: SizeColumnsToContentStrategy = { type: "fitCellContents", }; export const TradeOffers: React.FC = ({ foreignCoinBalance, fee, }: any) => { const [offers, setOffers] = useState([]); const [signedUnlockingFees, setSignedUnlockingFees] = useState(null); const [qortalNames, setQortalNames] = useState({}); const { fetchOngoingTransactions, onGoingTrades, updateTransactionInDB, isUsingGateway, getCoinLabel, selectedCoin, } = useContext(gameContext); const isRemoveOrdersWithoutUnlockingFees = useRef(false); const listOfOngoingTradesAts = useMemo(() => { return ( onGoingTrades ?.filter((item) => item?.status !== "trade-failed") ?.map((trade) => trade?.qortalAtAddress) || [] ); }, [onGoingTrades]); const { isShow: isShowInfo, onCancel: onCancelInfo, onOk: onOkInfo, show: showInfo, message: messageInfo, } = useModal(); const { isShow: isShowTradesUnknownFee, onCancel: onCancelTradesUnknownFee, onOk: onOkTradesUnknownFee, show: showTradesUnknownFee, message: messageTradesUnknownFee, } = useModal(); const offersWithoutOngoing = useMemo(() => { return offers.filter( (item) => !listOfOngoingTradesAts.includes(item.qortalAtAddress) ); }, [listOfOngoingTradesAts, offers]); const initiatedFetchPresence = useRef(false); const initiatedFetchPresenceSocket = useRef(false); const [isShowBuyInProgress, setIsShowBuyInProgress] = useState(null); const socketRef = useRef(null); const socketPresenceRef = useRef(null); const [selectedOffer, setSelectedOffer] = useState(null); const [selectedOffers, setSelectedOffers] = useState([]); const [record, setRecord] = useState(null); const tradePresenceTxns = useRef([]); const offeringTrades = useRef([]); const blockedTradesList = useRef([]); const gridRef = useRef(null); const [openShowOfferDetails, setOpenShowOfferDetails] = useState(null); const [open, setOpen] = useState(false); const [info, setInfo] = useState(null); const BuyButton = () => { return BUY; }; const intervalGetSignedUnlockingFees = useRef(null); const signedUnlockingFeesRef = useRef(signedUnlockingFees); const feeRef = useRef(fee); const knownFees = useMemo(() => { const offersWithKnownFees = []; selectedOffers.forEach((offer) => { const feeEntry = signedUnlockingFees.find( (item) => item?.atAddress === offer.qortalAtAddress ); if (feeEntry && typeof feeEntry.fee === "number") { offersWithKnownFees.push({ ...offer, fee: feeEntry.fee }); } }); const totalKnownFees = offersWithKnownFees.reduce((sum, offer) => { return sum + (offer.fee || 0); }, 0); const feeInLtc = totalKnownFees / 1e8; return +feeInLtc.toFixed(8); }, [selectedOffers, signedUnlockingFees]); console.log("knownFees", knownFees); const defaultColDef = { resizable: true, // Make columns resizable by default sortable: true, // Make columns sortable by default suppressMovable: true, // Prevent columns from being movable }; const getName = async (address) => { try { const response = await fetch("/names/address/" + address); const nameData = await response.json(); if (nameData?.length > 0) { setQortalNames((prev) => { return { ...prev, [address]: nameData[0].name, }; }); } else { setQortalNames((prev) => { return { ...prev, [address]: null, }; }); } } catch (error) { // error } }; const restartTradeOffers = () => { if (socketRef.current) { socketRef.current.close(1000, "forced"); // Close with a custom reason socketRef.current = null; } offeringTrades.current = []; setOffers([]); setSelectedOffer(null); }; const restartPresence = () => { if (socketPresenceRef.current) { socketPresenceRef.current.close(1000, "forced"); // Close with a custom reason socketPresenceRef.current = null; } }; const columnDefs: ColDef[] = useMemo(() => { return [ { headerCheckboxSelection: true, // Adds a checkbox in the header for selecting all rows // checkboxSelection: true, // Adds checkboxes in each row for selection checkboxSelection: true, // disable default, we're rendering it manually headerName: "", // You can customize the header name width: 100, // Adjust the width as needed pinned: "left", // Optional, to pin this column on the left resizable: false, suppressRowClickSelection: true, cellRenderer: (params) => ( { const hasSignedFee = signedUnlockingFees?.find( (item) => item?.atAddress === params?.node?.data?.qortalAtAddress ); let fee = null; if (hasSignedFee) { fee = hasSignedFee.fee; } setOpenShowOfferDetails({ ...(params?.node?.data || {}), fee }); }} /> ), // suppressRowClickSelection: true, // prevent whole row selection on click }, { headerName: "QORT AMOUNT", field: "qortAmount", flex: 1, // Flex makes this column responsive minWidth: 150, // Ensure it doesn't shrink too much resizable: true, }, { headerName: `${getCoinLabel()}/QORT`, valueGetter: (params) => +params.data.foreignAmount / +params.data.qortAmount, sortable: true, sort: "asc", flex: 1, // Flex makes this column responsive minWidth: 150, // Ensure it doesn't shrink too much resizable: true, }, { headerName: `Total ${getCoinLabel()} Value`, field: "foreignAmount", flex: 1, // Flex makes this column responsive minWidth: 150, // Ensure it doesn't shrink too much resizable: true, }, { headerName: `Unlocking fee`, flex: 1, // Flex makes this column responsive minWidth: 150, // Ensure it doesn't shrink too much resizable: true, valueGetter: (params) => { if (params?.data?.qortalAtAddress) { console.log("22signedUnlockingFees", signedUnlockingFees); const hasSignedFee = signedUnlockingFees?.find( (item) => item?.atAddress === params.data.qortalAtAddress ); if (!hasSignedFee) return "Unknown"; return hasSignedFee.fee; } else return "Unknown"; }, }, { headerName: "Seller", field: "qortalCreator", flex: 1, // Flex makes this column responsive minWidth: 300, // Ensure it doesn't shrink too much resizable: true, valueGetter: (params) => { if (params?.data?.qortalCreator) { if (qortalNames[params?.data?.qortalCreator]) { return qortalNames[params?.data?.qortalCreator]; } else if (qortalNames[params?.data?.qortalCreator] === undefined) { getName(params?.data?.qortalCreator); return params?.data?.qortalCreator; } else { return params?.data?.qortalCreator; } } }, }, ]; }, [qortalNames, getCoinLabel, signedUnlockingFees]); // const onRowClicked = (event: any) => { // if(listOfOngoingTradesAts.includes(event.data.qortalAtAddress)) return // setSelectedOffer(event.data) // }; const restartTradePresenceWebSocket = () => { restartPresence(); setTimeout(() => initTradePresenceWebSocket(true), 50); }; const getNewBlockedTrades = async () => { const unconfirmedTransactionsList = async () => { const unconfirmedTransactionslUrl = `http://devnet-nodes.qortal.link:11112/transactions/unconfirmed?txType=MESSAGE&limit=0&reverse=true`; var addBlockedTrades = JSON.parse( localStorage.getItem("failedTrades") || "[]" ); await fetch(unconfirmedTransactionslUrl) .then((response) => { return response.json(); }) .then((data) => { data.map((item: any) => { const unconfirmedNessageTimeDiff = Date.now() - item.timestamp; const timeOneHour = 60 * 60 * 1000; if (Number(unconfirmedNessageTimeDiff) > Number(timeOneHour)) { const addBlocked = { timestamp: item.timestamp, recipient: item.recipient, }; addBlockedTrades.push(addBlocked); } }); localStorage.setItem( "failedTrades", JSON.stringify(addBlockedTrades) ); blockedTradesList.current = JSON.parse( localStorage.getItem("failedTrades") || "[]" ); }); }; await unconfirmedTransactionsList(); const filterUnconfirmedTransactionsList = async () => { let cleanBlockedTrades = blockedTradesList.current.reduce( (newArray, cut: any) => { if ( cut && !newArray.some((obj: any) => obj.recipient === cut.recipient) ) { newArray.push(cut); } return newArray; }, [] as any[] ); localStorage.setItem("failedTrades", JSON.stringify(cleanBlockedTrades)); blockedTradesList.current = JSON.parse( localStorage.getItem("failedTrades") || "[]" ); }; await filterUnconfirmedTransactionsList(); processOffersWithPresence(); }; const executeGetNewBlockTrades = useCallback(() => { getNewBlockedTrades(); }, []); useEffect(() => { subscribeToEvent("execute-get-new-block-trades", executeGetNewBlockTrades); return () => { unsubscribeFromEvent( "execute-get-new-block-trades", executeGetNewBlockTrades ); }; }, []); const processOffersWithPresence = () => { if (offeringTrades.current === null) return; async function asyncForEach(array: any, callback: any) { for (let index = 0; index < array.length; index++) { await callback(array[index], index, array); } } const filterOffersUsingTradePresence = (offeringTrade: any) => { return offeringTrade.tradePresenceExpiry > Date.now(); }; const startOfferPresenceMapping = async () => { if (tradePresenceTxns.current) { for (const tradePresence of tradePresenceTxns.current) { const offerIndex = offeringTrades.current.findIndex( (offeringTrade) => offeringTrade.qortalCreatorTradeAddress === tradePresence.tradeAddress ); if (offerIndex !== -1) { offeringTrades.current[offerIndex].tradePresenceExpiry = tradePresence.timestamp; } } } let filteredOffers = offeringTrades.current?.filter((offeringTrade) => filterOffersUsingTradePresence(offeringTrade) ) || []; let tradesPresenceCleaned: any[] = filteredOffers; blockedTradesList.current.forEach((item: any) => { const toDelete = item.recipient; tradesPresenceCleaned = tradesPresenceCleaned?.filter((el) => { return el.qortalCreatorTradeAddress !== toDelete; }) || []; }); if (tradesPresenceCleaned) { updateGridData(tradesPresenceCleaned); } }; startOfferPresenceMapping(); }; const restartTradeOffersWebSocket = () => { setTimeout(() => initTradeOffersWebSocket(true), 50); }; const initTradePresenceWebSocket = (restarted = false) => { if (socketPresenceRef.current) return; let socketTimeout: any; let socketLink; if (isUsingGateway) { socketLink = `wss://appnode.qortal.org/websockets/crosschain/tradepresence`; } else { socketLink = `${ window.location.protocol === "https:" ? "wss:" : "ws:" }//${baseLocalHost}/websockets/crosschain/tradepresence`; } socketPresenceRef.current = new WebSocket(socketLink); socketPresenceRef.current.onopen = () => { setTimeout(pingSocket, 50); }; socketPresenceRef.current.onmessage = (e) => { tradePresenceTxns.current = !initiatedFetchPresenceSocket.current ? JSON.parse(e.data) : [...tradePresenceTxns.current, ...JSON.parse(e.data)]; initiatedFetchPresenceSocket.current = true; processOffersWithPresence(); restarted = false; }; socketPresenceRef.current.onclose = (event) => { clearTimeout(socketTimeout); if (event.reason === "forced") { return; } restartTradePresenceWebSocket(); }; socketPresenceRef.current.onerror = (e) => { clearTimeout(socketTimeout); restartTradePresenceWebSocket(); }; const pingSocket = () => { socketPresenceRef.current.send("ping"); socketTimeout = setTimeout(pingSocket, 295000); }; }; const initTradeOffersWebSocket = (restarted = false) => { if (socketRef.current) return; let socketTimeout: any; let socketLink; if (isUsingGateway) { socketLink = `wss://appnode.qortal.org/websockets/crosschain/tradeoffers?foreignBlockchain=${selectedCoin}&includeHistoric=true`; } else { socketLink = `${ window.location.protocol === "https:" ? "wss:" : "ws:" }//${baseLocalHost}/websockets/crosschain/tradeoffers?foreignBlockchain=${selectedCoin}&includeHistoric=true`; } socketRef.current = new WebSocket(socketLink); socketRef.current.onopen = () => { setTimeout(pingSocket, 50); }; socketRef.current.onmessage = (e) => { offeringTrades.current = [ ...offeringTrades.current?.filter( (coin) => coin?.foreignBlockchain === selectedCoin ), ...JSON.parse(e.data)?.filter( (coin) => coin?.foreignBlockchain === selectedCoin ), ]; restarted = false; processOffersWithPresence(); }; socketRef.current.onclose = (event) => { clearTimeout(socketTimeout); if (event.reason === "forced") { return; } restartTradeOffersWebSocket(); socketRef.current = null; }; socketRef.current.onerror = (e) => { clearTimeout(socketTimeout); }; const pingSocket = () => { socketRef.current.send("ping"); socketTimeout = setTimeout(pingSocket, 295000); }; }; useEffect(() => { if (isUsingGateway === null) return; blockedTradesList.current = JSON.parse( localStorage.getItem("failedTrades") || "[]" ); if (!initiatedFetchPresence.current) { initiatedFetchPresence.current = true; initTradePresenceWebSocket(); } getNewBlockedTrades(); const intervalBlockTrades = setInterval(() => { initiatedFetchPresenceSocket.current = false; restartPresence(); initTradePresenceWebSocket(); getNewBlockedTrades(); }, 150000); return () => { clearInterval(intervalBlockTrades); }; }, [isUsingGateway]); const getSignedUnlockingFees = useCallback(async () => { try { const response = await fetch( `http://devnet-nodes.qortal.link:11112/crosschain/signedfees` ); const data = await response.json(); if (data && Array.isArray(data)) { setSignedUnlockingFees(data); } } catch (error) { console.error(error); } }, []); console.log("signed", signedUnlockingFees); useEffect(() => { getSignedUnlockingFees(); intervalGetSignedUnlockingFees.current = setInterval(() => { getSignedUnlockingFees(); }, 150000); return () => { if (intervalGetSignedUnlockingFees.current) { clearInterval(intervalGetSignedUnlockingFees.current); } }; }, [getSignedUnlockingFees]); useEffect(() => { if (isUsingGateway === null) return; if (selectedCoin === null) return; restartTradeOffers(); setTimeout(() => { initTradeOffersWebSocket(); }, 500); return () => { if (socketRef.current) { socketRef.current.close(1000, "forced"); } }; }, [isUsingGateway, selectedCoin]); const selectedTotalLTC = useMemo(() => { const total = selectedOffers.reduce((acc: number, curr: any) => { return acc + (+curr.foreignAmount || 0); // Ensure qortAmount is defined }, 0); const totalWithKnownFees = +total + +knownFees; return totalWithKnownFees; }, [selectedOffers, knownFees]); const buyOrder = async () => { try { isRemoveOrdersWithoutUnlockingFees.current = false; if (+foreignCoinBalance < +selectedTotalLTC.toFixed(4)) { setOpen(true); setInfo({ type: "error", message: `You don't have enough ${getCoinLabel()} or your balance was not retrieved`, }); return; } if (selectedOffers?.length < 1) return; let offersWithKnownFees = []; const offersWithUnknownFees = []; selectedOffers.forEach((offer) => { const feeEntry = signedUnlockingFees.find( (item) => item?.atAddress === offer.qortalAtAddress ); if (feeEntry && typeof feeEntry.fee === "number") { offersWithKnownFees.push({ ...offer, fee: feeEntry.fee }); } else { offersWithUnknownFees.push(offer); } }); console.log("offersWithKnownFees", offersWithKnownFees); console.log("offersWithUnknownFees", offersWithUnknownFees); if (offersWithUnknownFees?.length > 0) { await showTradesUnknownFee({ message: "", }); if (!isRemoveOrdersWithoutUnlockingFees.current) { offersWithKnownFees = [ ...offersWithKnownFees, ...offersWithUnknownFees, ]; } } console.log("offersWithKnownFees", offersWithKnownFees); setIsShowBuyInProgress({ status: "buying" }); // setOpen(true) // setInfo({ // type: 'info', // message: "Attempting to submit buy order. Please wait..." // }) const listOfATs = offersWithKnownFees; const response = await qortalRequestWithTimeout( { action: "CREATE_TRADE_BUY_ORDER", crosschainAtInfo: listOfATs, foreignBlockchain: selectedCoin, }, 900000 ); if (response?.error) { setIsShowBuyInProgress({ status: "error", message: response?.error || "Failed to submit trade order.", }); // setOpen(true) // setInfo({ // type: 'error', // message: response?.error || "Failed to submit trade order." // }) return; } if (response?.extra?.atAddresses) { setIsShowBuyInProgress({ status: "success" }); setSelectedOffers([]); const transactionData = { qortalAtAddresses: response?.extra?.atAddresses, qortAddress: response?.extra?.senderAddress, node: response?.extra?.node, status: response?.extra?.status ? response?.extra?.status : response.callResponse === true ? "trade-ongoing" : "trade-failed", encryptedMessageToBase58: response?.encryptedMessageToBase58, chatSignature: response?.chatSignature, sender: response?.extra?.senderAddress, senderPublicKey: response?.extra?.senderPublicKey, reference: response?.callResponse?.reference, }; // Update transactions in IndexedDB const result = await updateTransactionInDB(transactionData); setOpen(true); setInfo({ type: "success", message: "Submitted Order", }); fetchOngoingTransactions(); if (isUsingGateway) { setRecord(transactionData); await showInfo({ message: `Keep a record of your order in case your trade gets stuck`, }); } } } catch (error) { setIsShowBuyInProgress({ status: "error", message: error?.error || error?.message || "Failed to submit trade order.", }); // setOpen(true) // setInfo({ // type: 'error', // message: error?.error || error?.message || "Failed to submit trade order." // }) // console.error(error) } }; const getRowStyle = ( params: RowClassParams ): RowStyle | undefined => { if (listOfOngoingTradesAts.includes(params.data.qortalAtAddress)) { return { background: "#D9D9D91A" }; } if (params.data.qortalAtAddress === selectedOffer?.qortalAtAddress) { return { background: "#6D94F533" }; } const hasSignedFee = signedUnlockingFees?.find( (item) => item?.atAddress === params.data.qortalAtAddress ); console.log("hasSignedFee", hasSignedFee); if (hasSignedFee) { console.log("fee2fee", fee); if (fee && hasSignedFee?.fee > fee) { return { backgroundColor: "#ff6347" }; } } return undefined; }; // const onGridReady = (params) => { // const allColumnIds = params.columnApi.getAllColumns().map(col => col.getColId()); // params.columnApi.autoSizeColumns(allColumnIds, false); // }; const onSelectionChanged = (event: any) => { const selectedRows = event.api.getSelectedRows(); setSelectedOffers([...selectedRows]); // Set all selected rows }; const onRowClicked = (event: any) => { if (listOfOngoingTradesAts.includes(event.data.qortalAtAddress)) return; const selectedRows = gridRef.current?.api.getSelectedRows(); setSelectedOffers([...selectedRows]); // Always spread the array to ensure state updates correctly }; const updateGridData = (newData: any) => { if (gridRef.current) { setOffers(newData); } }; const getRowId = useCallback(function (params: any) { return String(params.data.qortalAtAddress); }, []); const selectedTotalQORT = useMemo(() => { const total = selectedOffers.reduce((acc: number, curr: any) => { return acc + (+curr.qortAmount || 0); // Ensure qortAmount is defined }, 0); return total; }, [selectedOffers]); const onGridReady = useCallback((params: any) => { params.api.sizeColumnsToFit(); // Adjust columns to fit the grid width const allColumnIds = params.columnApi .getAllColumns() .map((col: any) => col.getColId()); params.columnApi.autoSizeColumns(allColumnIds); // Automatically adjust the width to fit content }, []); const handleClose = ( event?: React.SyntheticEvent | Event, reason?: SnackbarCloseReason ) => { if (reason === "clickaway") { return; } setOpen(false); setInfo(null); }; useEffect(() => { signedUnlockingFeesRef.current = signedUnlockingFees; feeRef.current = fee; if (gridRef.current?.api) { console.log("Total rows:", gridRef.current.api.getDisplayedRowCount()); gridRef.current.api.forEachNode((rowNode: RowNode) => { const qortalAtAddress = rowNode.data?.qortalAtAddress; const hasSignedFee = signedUnlockingFeesRef.current?.find( (item) => item?.atAddress === qortalAtAddress ); const isSelectable = !hasSignedFee || hasSignedFee.fee <= feeRef.current; rowNode.setRowSelectable(isSelectable); // ✅ apply logic per row }); // Optional: refresh selection/checkbox visuals gridRef.current.api.refreshCells({ force: true }); } }, [signedUnlockingFees, fee]); console.log("signedUnlockingFees", signedUnlockingFees, fee); if (!signedUnlockingFees || !fee) return null; return (
params.data.qortalAtAddress} // Ensure rows have unique IDs gridOptions={{ isRowSelectable: (params) => { let selectable = true; const hasSignedFee = signedUnlockingFeesRef.current?.find( (item) => item?.atAddress === params.data.qortalAtAddress ); if (!hasSignedFee) selectable = true; console.log( "fee", feeRef.current, signedUnlockingFeesRef.current ); if (hasSignedFee && hasSignedFee?.fee > feeRef.current) selectable = false; return selectable; }, }} /> {/* {selectedOffer && ( )} */}
{selectedTotalQORT?.toFixed(8)} QORT foreignCoinBalance ? "red" : "unset", color: "white", }} > {selectedTotalLTC?.toFixed(8)}{" "} {`${getCoinLabel()} (with known fees)`} {foreignCoinBalance?.toFixed(8)}{" "} {`${getCoinLabel()} `} balance {BuyButton()} {info?.message} {isShowInfo && ( {"Download record"} {messageInfo.message} )} {isShowTradesUnknownFee && ( Warning Some of your buy orders have unknown unlocking fees. You may proceed with your purchases without removing these orders, but there is a higher risk that the trades may not go through. In such cases, you will be refunded. (isRemoveOrdersWithoutUnlockingFees.current = e.target.checked) } edge="start" tabIndex={-1} disableRipple sx={{ "&.Mui-checked": { color: "white", }, "& .MuiSvgIcon-root": { color: "white", }, }} /> } label={ Remove orders with unknown unlocking fees } /> )} {isShowBuyInProgress && ( {isShowBuyInProgress?.status === "error" && ( {` Failed to submit buy order.`} {isShowBuyInProgress?.message} )} {isShowBuyInProgress?.status === "success" && ( {` Successfully submitted order.`} You can see the progress of your order in the "Pending" table. Note: Submission of an order does not necessarily mean that your submission will be the one completing the order. Another account may have submitted it before you. )} {isShowBuyInProgress?.status === "buying" && ( {` Attempting to submit buy order`} Please do not leave this application until there is a response. Please wait! {isUsingGateway && ( <> Using gateway: might take up to 3 minutes to submit the buy order. { //nothing }} size={60} strokeWidth={4} > {({ remainingTime }) => ( {remainingTime} )} )} )} )} Buy {openShowOfferDetails?.qortAmount} QORT @{" "} {openShowOfferDetails?.foreignAmount} {getCoinLabel()} setOpenShowOfferDetails(null)} sx={{ position: "absolute", right: 8, top: 8, color: "#fff" }} > {fee && openShowOfferDetails?.fee && +fee < +openShowOfferDetails?.fee && ( The unlocking fee on this node is lower than the amount required for this order. If you're using your own node, you can change the fee by clicking the "Fee" button next to the coin selector. )} {openShowOfferDetails?.fee && ( )} ); }; const TradeRow = ({ label, value, extra, enableSlice, enableCopy, }: { label: string; value: string; extra?: string; enableSlice?: boolean; enableCopy?: boolean; }) => ( {label} {enableSlice && value?.length > 18 ? value?.slice(0, 6) + "..." + value.slice(-4) : value} {enableCopy && ( copyToClipboard(value)}> )} {extra && ( {extra} )} ); const SelectWithInfoCell = ({ selectTradeForDetails }) => { const handleInfoClick = (e: React.MouseEvent) => { e.stopPropagation(); // Prevents row selection selectTradeForDetails(); // alert(`Info for ${data.qortalAtAddress}`); // Replace with your own UI }; return (
{ e.stopPropagation(); handleInfoClick(e); }} onMouseDown={(e) => e.stopPropagation()} // 👈 this is key sx={{ minWidth: 0, padding: "0 4px" }} >
); };