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, FormControl, FormControlLabel, FormLabel, IconButton, Radio, RadioGroup, Snackbar, SnackbarCloseReason, Tooltip, Typography, useTheme, } 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"; // export const baseLocalHost = "127.0.0.1:22391"; import CloseIcon from "@mui/icons-material/Close"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import moment from "moment"; import { RequestQueueWithPromise } from "qapp-core"; import { useUpdateFee } from "../../hooks/useUpdateFee"; import { useSetAtom } from "jotai/react"; import { stuckTradesAtom } from "../../global/state"; 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", }; const requestQueueGetNames = new RequestQueueWithPromise(4); export const TradeOffers: React.FC = ({ foreignCoinBalance, fee, setFee }: any) => { const [offers, setOffers] = useState([]); const [signedUnlockingFees, setSignedUnlockingFees] = useState(null); const [qortalNames, setQortalNames] = useState({}); const setStuckTrades = useSetAtom(stuckTradesAtom) const { fetchOngoingTransactions, onGoingTrades, updateTransactionInDB, isUsingGateway, getCoinLabel, selectedCoin, } = useContext(gameContext); const isRemoveOrdersWithoutUnlockingFees = useRef(false); const [isRemoveOrders, setIsRemoveOrders] = useState('remove'); const updateFee = useUpdateFee({setFee, selectedCoin}) 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 { isShow: isShowAskToUpdateFee, onCancel: onCancelAskToUpdateFee, onOk: onOkAskToUpdateFee, show: showAskToUpdateFee, message: messageAskToUpdateFee, } = 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 lengthOfOffers = selectedOffers.length const totalKnownFees = lengthOfOffers * fee const feeInLtc = totalKnownFees / 1e8; return +feeInLtc.toFixed(8); }, [selectedOffers, fee]); const defaultColDef = { resizable: true, // Make columns resizable by default sortable: true, // Make columns sortable by default suppressMovable: true, // Prevent columns from being movable }; const handleChange = (event: React.ChangeEvent) => { const val = event.target.value isRemoveOrdersWithoutUnlockingFees.current = val === 'remove' ? true : false setIsRemoveOrders(val); }; const isFetchingName = useRef({}) const getName = async (address) => { try { if(isFetchingName.current[address]) return isFetchingName.current[address] = true const response = await requestQueueGetNames.enqueue( () => { return fetch("/names/primary/" + address); } ); const nameData = await response.json(); if (nameData?.name) { setQortalNames((prev) => { return { ...prev, [address]: nameData.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 rowTooltip = (params) => { let selectable = true; const hasSignedFee = signedUnlockingFees?.find( (item) => item?.atAddress === params.data.qortalAtAddress ); if (!hasSignedFee) selectable = true; if (hasSignedFee && hasSignedFee?.fee > feeRef.current) selectable = false; if(!selectable)return 'Your unlocking fee is to low to buy this order, Increase your fee to purchase' return '' }; 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, tooltipValueGetter: rowTooltip, 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, tooltipValueGetter: rowTooltip }, { 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, tooltipValueGetter: rowTooltip }, { headerName: `Total ${getCoinLabel()} Value`, field: "foreignAmount", flex: 1, // Flex makes this column responsive minWidth: 150, // Ensure it doesn't shrink too much resizable: true, tooltipValueGetter: rowTooltip }, { headerName: `Unlocking fee`, flex: 1, // Flex makes this column responsive minWidth: 150, // Ensure it doesn't shrink too much resizable: true, tooltipValueGetter: rowTooltip, valueGetter: (params) => { if (params?.data?.qortalAtAddress) { 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, tooltipValueGetter: rowTooltip, 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 = `/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 = useCallback(() => { 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 filterForStuckDrades = (offeringTrade: any) => { return (!offeringTrade?.tradePresenceExpiry || 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; } } } offeringTrades.current = Object.values( offeringTrades.current.reduce((acc, trade) => { const key = trade.qortalAtAddress; if (!acc[key] || trade.timestamp > acc[key].timestamp) { acc[key] = trade; } return acc; }, {} as Record) ); let filteredOffers = offeringTrades.current?.filter((offeringTrade) => filterOffersUsingTradePresence(offeringTrade) ) || []; const stuckTrades = offeringTrades.current?.filter((offeringTrade) => filterForStuckDrades(offeringTrade) ) || []; setStuckTrades(stuckTrades?.sort((a, b) => b.timestamp - a.timestamp)) 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(); },[setStuckTrades]); 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 fetchOffers = useCallback(async (selectedCoin) => { try { const response = await fetch( `/crosschain/tradeoffers?foreignBlockchain=${selectedCoin}&includeHistoric=true` ); const data = await response.json(); const transformed = data.map(item => ({ qortalAtAddress: item.qortalAtAddress, qortalCreator: item.qortalCreator, qortalCreatorTradeAddress: item.qortalCreatorTradeAddress, qortAmount: item.qortAmount, btcAmount: item.expectedBitcoin ?? item.btcAmount, // fallback if already correct foreignAmount: item.expectedForeignAmount ?? item.foreignAmount, tradeTimeout: item.tradeTimeout, mode: item.mode, timestamp: item.timestamp ?? item.creationTimestamp, foreignBlockchain: item.foreignBlockchain, acctName: item.acctName })); offeringTrades.current = [ ...transformed?.filter( (coin) => coin?.foreignBlockchain === selectedCoin && coin?.mode === 'OFFERING' ), ]; processOffersWithPresence(); } catch (error) { console.error(error) } },[]); const initTradeOffersWebSocket = async (restarted = false) => { if (socketRef.current) return; if(restarted === false){ await fetchOffers(selectedCoin) } 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 && coin?.mode === 'OFFERING' ), ...JSON.parse(e.data)?.filter( (coin) => coin?.foreignBlockchain === selectedCoin && coin?.mode === 'OFFERING' ), ]; 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( `/crosschain/signedfees` ); const data = await response.json(); if (data && Array.isArray(data)) { setSignedUnlockingFees(data); } } catch (error) { console.error(error); } }, []); 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); if (selectedCoin === "PIRATECHAIN") return total; const totalWithKnownFees = +total + +knownFees; return totalWithKnownFees; }, [selectedOffers, knownFees, selectedCoin]); const buyOrder = async () => { try { if (+foreignCoinBalance < +selectedTotalLTC.toFixed(8)) { 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 = []; if (selectedCoin !== "PIRATECHAIN") { 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); } }); if (offersWithUnknownFees?.length > 0) { await showTradesUnknownFee({ message: "", }); if (!isRemoveOrdersWithoutUnlockingFees.current) { offersWithKnownFees = [ ...offersWithKnownFees, ...offersWithUnknownFees, ]; } } } else { offersWithKnownFees = selectedOffers; } const highestFee = offersWithKnownFees.length ? Math.max( ...offersWithKnownFees .filter(o => typeof o.fee === 'number') .map(o => o.fee as number) ) : 0; if(highestFee < fee){ await showAskToUpdateFee({ message: highestFee }) } setIsShowBuyInProgress({ status: "buying" }); // setOpen(true) // setInfo({ // type: 'info', // message: "Attempting to submit buy order. Please wait..." // }) const listOfATs = offersWithKnownFees; if(listOfATs?.length === 0){ throw new Error('No buy orders selected') } 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 ); if (hasSignedFee) { if (fee && hasSignedFee?.fee > fee) { return { backgroundColor: "#FF0000" }; } } 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) { 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]); if (!signedUnlockingFees || ((fee === undefined || fee === null) && selectedCoin !== "PIRATECHAIN")) return null; return (
params.data.qortalAtAddress} // Ensure rows have unique IDs enableBrowserTooltips={true} gridOptions={{ isRowSelectable: (params) => { let selectable = true; const hasSignedFee = signedUnlockingFeesRef.current?.find( (item) => item?.atAddress === params.data.qortalAtAddress ); if (!hasSignedFee) selectable = true; if (hasSignedFee && hasSignedFee?.fee > feeRef.current) selectable = false; return selectable; }, }} /> {/* {selectedOffer && ( )} */}
{selectedTotalQORT?.toFixed(8)} QORT foreignCoinBalance ? "red" : "unset", color: "white", }} > {selectedTotalLTC?.toFixed(8)}{" "} {`${getCoinLabel()} ${ selectedCoin !== "PIRATECHAIN" ? "with unlocking 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. } label="Remove orders with unknown unlocking fees" /> } label="Keep orders with unknown unlocking fees" /> )} {isShowAskToUpdateFee && ( Suggestion Your current unlocking fee is higher than necessary. You can lower it to match the highest required fee and reduce costs. )} {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" }} >
); };