q-trade/src/components/Grids/TradeOffers.tsx
2025-05-02 18:35:57 +03:00

1399 lines
44 KiB
TypeScript

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<any> = ({
foreignCoinBalance,
fee,
}: any) => {
const [offers, setOffers] = useState<any[]>([]);
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<any>(null);
const [selectedOffers, setSelectedOffers] = useState<any>([]);
const [record, setRecord] = useState(null);
const tradePresenceTxns = useRef<any[]>([]);
const offeringTrades = useRef<any[]>([]);
const blockedTradesList = useRef([]);
const gridRef = useRef<any>(null);
const [openShowOfferDetails, setOpenShowOfferDetails] = useState(null);
const [open, setOpen] = useState(false);
const [info, setInfo] = useState<any>(null);
const BuyButton = () => {
return <BuyOrderBtn onClick={buyOrder}>BUY</BuyOrderBtn>;
};
const intervalGetSignedUnlockingFees = useRef<number | null>(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) => (
<SelectWithInfoCell
{...params}
selectTradeForDetails={() => {
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<any, any>
): 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 (
<MainContainer>
<div
className="ag-theme-alpine-dark"
style={{ height: 400, width: "100%" }}
>
<AgGridReact
ref={gridRef}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
rowData={offersWithoutOngoing}
onRowClicked={onRowClicked}
onSelectionChanged={onSelectionChanged}
getRowStyle={getRowStyle}
autoSizeStrategy={autoSizeStrategy}
rowSelection={selectedCoin === "PIRATECHAIN" ? "single" : "multiple"} // Enable multi-select
rowMultiSelectWithClick={true}
suppressHorizontalScroll={false} // Allow horizontal scroll on mobile if needed
suppressCellFocus={true} // Prevents cells from stealing focus in mobile
// pagination={true}
// paginationPageSize={10}
onGridReady={onGridReady}
// domLayout='autoHeight'
getRowId={(params) => 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 && (
<Button onClick={buyOrder}>Buy</Button>
)} */}
</div>
<div
style={{
height: "150px",
}}
/>
<BuyContainer>
<BuyContainerDivider />
<Box
sx={{
display: "flex",
gap: "5px",
flexDirection: "column",
width: "100%",
}}
>
<Typography
sx={{
fontSize: "16px",
color: "white",
width: "calc(100% - 75px)",
}}
>
{selectedTotalQORT?.toFixed(8)} QORT
</Typography>
<Box
sx={{
display: "flex",
gap: "20px",
alignItems: "center",
width: "calc(100% - 75px)",
}}
>
<Typography
sx={{
fontSize: "16px",
backgroundColor:
selectedTotalLTC > foreignCoinBalance ? "red" : "unset",
color: "white",
}}
>
<span>{selectedTotalLTC?.toFixed(8)}</span>{" "}
<span
style={{
marginLeft: "auto",
}}
>{`${getCoinLabel()} (with known fees)`}</span>
</Typography>
</Box>
<Typography
sx={{
fontSize: "16px",
color: "white",
}}
>
<span>{foreignCoinBalance?.toFixed(8)}</span>{" "}
<span
style={{
marginLeft: "auto",
}}
>
{`${getCoinLabel()} `} balance
</span>
</Typography>
</Box>
{BuyButton()}
</BuyContainer>
<Snackbar
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
open={open}
onClose={handleClose}
>
<Alert
onClose={handleClose}
severity={info?.type}
variant="filled"
sx={{ width: "100%" }}
>
{info?.message}
</Alert>
</Snackbar>
{isShowInfo && (
<Dialog
open={isShowInfo}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{"Download record"}</DialogTitle>
<DialogContent>
<DialogContentText
id="alert-dialog-description"
sx={{
color: "white",
}}
>
{messageInfo.message}
</DialogContentText>
<Button
onClick={async () => {
try {
const fileName = "traderecord_" + Date.now() + "_" + ".json";
const dataString = JSON.stringify(record);
const blob = new Blob([dataString], {
type: "application/json",
});
await qortalRequest({
action: "SAVE_FILE",
filename: fileName,
blob,
});
} catch (error) {}
}}
>
Save Record
</Button>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={onOkInfo} autoFocus>
Close
</Button>
</DialogActions>
</Dialog>
)}
{isShowTradesUnknownFee && (
<Dialog
open={isShowTradesUnknownFee}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Warning</DialogTitle>
<DialogContent>
<DialogContentText
id="alert-dialog-description"
sx={{ color: "white" }}
>
Some of your buy orders have unknown unlocking fees.
</DialogContentText>
<Spacer height="20px" />
<DialogContentText
id="alert-dialog-description"
sx={{ color: "white" }}
>
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.
</DialogContentText>
<Spacer height="20px" />
<FormControlLabel
sx={{
margin: 0,
}}
control={
<Checkbox
onChange={(e) =>
(isRemoveOrdersWithoutUnlockingFees.current =
e.target.checked)
}
edge="start"
tabIndex={-1}
disableRipple
sx={{
"&.Mui-checked": {
color: "white",
},
"& .MuiSvgIcon-root": {
color: "white",
},
}}
/>
}
label={
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography sx={{ fontSize: "14px" }}>
Remove orders with unknown unlocking fees
</Typography>
</Box>
}
/>
</DialogContent>
<DialogActions>
<Button
variant="contained"
onClick={onCancelTradesUnknownFee}
autoFocus
>
Close
</Button>
<Button
variant="contained"
onClick={onOkTradesUnknownFee}
autoFocus
>
Continue
</Button>
</DialogActions>
</Dialog>
)}
{isShowBuyInProgress && (
<Dialog
open={isShowBuyInProgress}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
PaperProps={{
style: {
backgroundColor: "rgb(39, 40, 44)",
background: "rgb(39, 40, 44)",
},
}}
>
<DialogContent
sx={{
width: "450px",
height: "450px",
maxHeight: "calc(90vh - 55px)",
maxWidth: "90%",
background: "rgb(39, 40, 44)",
overflow: "auto",
}}
>
{isShowBuyInProgress?.status === "error" && (
<Box>
<Typography>
<ErrorIcon
sx={{
color: "red",
}}
/>
{` Failed to submit buy order.`}
</Typography>
<Spacer height="20px" />
<Typography>{isShowBuyInProgress?.message}</Typography>
</Box>
)}
{isShowBuyInProgress?.status === "success" && (
<Box>
<Typography>
<CheckCircleIcon
sx={{
color: "green",
}}
/>
{` Successfully submitted order.`}
</Typography>
<Spacer height="20px" />
<Typography>
You can see the progress of your order in the "Pending" table.
</Typography>
<Spacer height="20px" />
<Typography>
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.
</Typography>
</Box>
)}
{isShowBuyInProgress?.status === "buying" && (
<Box>
<Typography>
<Hourglass
visible={true}
height="25"
width="25"
ariaLabel="hourglass-loading"
wrapperStyle={{}}
wrapperClass=""
colors={["#306cce", "#72a1ed"]}
/>
{` Attempting to submit buy order`}
</Typography>
<Spacer height="20px" />
<Typography>
Please do not leave this application until there is a
response. Please wait!
</Typography>
<Spacer height="20px" />
{isUsingGateway && (
<>
<Typography>
Using gateway: might take up to 3 minutes to submit the
buy order.
</Typography>
<Spacer height="20px" />
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
<CountdownCircleTimer
isPlaying
duration={180}
colors={["#004777", "#F7B801", "#A30000", "#A30000"]}
colorsTime={[7, 5, 2, 0]}
onComplete={() => {
//nothing
}}
size={60}
strokeWidth={4}
>
{({ remainingTime }) => (
<Typography>{remainingTime}</Typography>
)}
</CountdownCircleTimer>
</Box>
</>
)}
</Box>
)}
</DialogContent>
<DialogActions
sx={{
background: "rgb(39, 40, 44)",
}}
>
<Button
disabled={isShowBuyInProgress?.status === "buying"}
variant="outlined"
onClick={() => {
setIsShowBuyInProgress(null);
}}
autoFocus
>
Close
</Button>
</DialogActions>
</Dialog>
)}
<Dialog
open={!!openShowOfferDetails}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
PaperProps={{
style: {
backgroundColor: "rgb(39, 40, 44)",
background: "rgb(39, 40, 44)",
},
}}
>
<DialogTitle
sx={{
maxHeight: "calc(90vh - 55px)",
maxWidth: "90%",
background: "rgb(39, 40, 44)",
overflow: "auto",
}}
>
<Typography variant="subtitle1">
Buy {openShowOfferDetails?.qortAmount} QORT @{" "}
{openShowOfferDetails?.foreignAmount} {getCoinLabel()}
</Typography>
</DialogTitle>
<IconButton
aria-label="close"
onClick={() => setOpenShowOfferDetails(null)}
sx={{ position: "absolute", right: 8, top: 8, color: "#fff" }}
>
<CloseIcon />
</IconButton>
<DialogContent dividers sx={{ borderColor: "#333" }}>
{fee &&
openShowOfferDetails?.fee &&
+fee < +openShowOfferDetails?.fee && (
<Box
sx={{
background: "red",
padding: "10px",
borderRadius: "5px",
}}
>
<Typography sx={{ color: "white" }}>
The unlocking fee on this node is lower than the amount
required for this order.
</Typography>
<Spacer height="10px" />
<Typography sx={{ color: "white" }}>
If you're using your own node, you can change the fee by
clicking the "Fee" button next to the coin selector.
</Typography>
</Box>
)}
<TradeRow
enableSlice
enableCopy
label="Seller"
value={openShowOfferDetails?.qortalCreator}
extra={qortalNames[openShowOfferDetails?.qortalCreator]}
/>
<TradeRow
label="Amount"
value={`${openShowOfferDetails?.qortAmount} QORT`}
/>
<TradeRow
label="Total"
value={`${openShowOfferDetails?.foreignAmount} ${getCoinLabel()}`}
/>
<TradeRow
label="Price"
value={`${
+openShowOfferDetails?.foreignAmount /
+openShowOfferDetails?.qortAmount
} ${getCoinLabel()}/QORT`}
/>
{openShowOfferDetails?.fee && (
<TradeRow
label="Unlocking fee"
value={`${openShowOfferDetails?.fee} sats`}
/>
)}
<TradeRow
enableSlice
enableCopy
label="AT Address"
value={openShowOfferDetails?.qortalAtAddress}
/>
</DialogContent>
<DialogActions
sx={{
background: "rgb(39, 40, 44)",
}}
>
<Button
variant="outlined"
onClick={() => {
setOpenShowOfferDetails(null);
}}
autoFocus
>
Close
</Button>
</DialogActions>
</Dialog>
</MainContainer>
);
};
const TradeRow = ({
label,
value,
extra,
enableSlice,
enableCopy,
}: {
label: string;
value: string;
extra?: string;
enableSlice?: boolean;
enableCopy?: boolean;
}) => (
<Box
sx={{
display: "flex",
flexDirection: "column",
mb: 2,
}}
>
<Typography variant="caption" color="gray">
{label}
</Typography>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
flexWrap: "wrap",
}}
>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{enableSlice && value?.length > 18
? value?.slice(0, 6) + "..." + value.slice(-4)
: value}
</Typography>
{enableCopy && (
<Tooltip title="Copy">
<IconButton size="small" onClick={() => copyToClipboard(value)}>
<ContentCopyIcon fontSize="small" />
</IconButton>
</Tooltip>
)}
{extra && (
<Typography variant="body2" color="gray">
{extra}
</Typography>
)}
</Box>
</Box>
);
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 (
<div
className="ag-cell-ignore-selection"
style={{ display: "flex", alignItems: "center", gap: 6 }}
>
<IconButton
size="small"
onClick={handleInfoClick}
onClickCapture={(e) => {
e.stopPropagation();
handleInfoClick(e);
}}
onMouseDown={(e) => e.stopPropagation()} // 👈 this is key
sx={{ minWidth: 0, padding: "0 4px" }}
>
<InfoOutlineIcon />
</IconButton>
</div>
);
};