mirror of
https://github.com/Qortal/q-trade.git
synced 2025-06-15 19:01:21 +00:00
436 lines
12 KiB
TypeScript
436 lines
12 KiB
TypeScript
import { ColDef } from "ag-grid-community";
|
|
import { AgGridReact } from "ag-grid-react";
|
|
import React, {
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
import { autoSizeStrategy, baseLocalHost } from "../Grids/TradeOffers";
|
|
import {
|
|
Alert,
|
|
Box,
|
|
Snackbar,
|
|
SnackbarCloseReason,
|
|
Typography,
|
|
} from "@mui/material";
|
|
import gameContext from "../../contexts/gameContext";
|
|
import { BuyContainerDivider } from "../Grids/Table-styles";
|
|
|
|
const defaultColDef = {
|
|
resizable: true, // Make columns resizable by default
|
|
sortable: true, // Make columns sortable by default
|
|
suppressMovable: true, // Prevent columns from being movable
|
|
};
|
|
|
|
export default function TradeBotList({ qortAddress, failedTradeBots }) {
|
|
const [tradeBotList, setTradeBotList] = useState([]);
|
|
const [selectedTrade, setSelectedTrade] = useState(null);
|
|
const tradeBotListRef = useRef([]);
|
|
const offeringTrades = useRef<any[]>([]);
|
|
const qortAddressRef = useRef(null);
|
|
const gridRef = useRef<any>(null);
|
|
const {
|
|
updateTemporaryFailedTradeBots,
|
|
fetchTemporarySellOrders,
|
|
deleteTemporarySellOrder,
|
|
getCoinLabel,
|
|
selectedCoin,
|
|
} = useContext(gameContext);
|
|
const [open, setOpen] = useState(false);
|
|
const [info, setInfo] = useState<any>(null);
|
|
const filteredOutTradeBotListWithoutFailed = useMemo(() => {
|
|
const list = tradeBotList.filter(
|
|
(item) =>
|
|
!failedTradeBots.some(
|
|
(failedItem) => failedItem.atAddress === item.atAddress
|
|
)
|
|
);
|
|
return list;
|
|
}, [failedTradeBots, tradeBotList]);
|
|
|
|
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 columnDefs: ColDef[] = useMemo(() => {
|
|
return [
|
|
{
|
|
headerCheckboxSelection: false, // Adds a checkbox in the header for selecting all rows
|
|
checkboxSelection: true, // Adds checkboxes in each row for selection
|
|
headerName: "Select", // You can customize the header name
|
|
width: 50, // Adjust the width as needed
|
|
pinned: "left", // Optional, to pin this column on the left
|
|
resizable: false,
|
|
},
|
|
{
|
|
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: "Status",
|
|
field: "status",
|
|
flex: 1, // Flex makes this column responsive
|
|
minWidth: 300, // Ensure it doesn't shrink too much
|
|
resizable: true,
|
|
},
|
|
];
|
|
}, [selectedCoin, getCoinLabel]);
|
|
useEffect(() => {
|
|
if (qortAddress) {
|
|
qortAddressRef.current = qortAddress;
|
|
}
|
|
}, [qortAddress]);
|
|
|
|
const restartTradeOffersWebSocket = () => {
|
|
setTimeout(() => initTradeOffersWebSocket(true), 50);
|
|
};
|
|
|
|
const processTradeBotState = (state) => {
|
|
if (state.creatorAddress === qortAddressRef.current) {
|
|
switch (state.tradeState) {
|
|
case "BOB_WAITING_FOR_AT_CONFIRM":
|
|
return "PENDING";
|
|
|
|
case "BOB_WAITING_FOR_MESSAGE":
|
|
return "LISTED";
|
|
|
|
case "BOB_WAITING_FOR_AT_REDEEM":
|
|
return "TRADING";
|
|
|
|
case "BOB_DONE":
|
|
case "BOB_REFUNDED":
|
|
case "ALICE_DONE":
|
|
case "ALICE_REFUNDED":
|
|
return null;
|
|
|
|
case "ALICE_WAITING_FOR_AT_LOCK":
|
|
return "BUYING";
|
|
|
|
case "ALICE_REFUNDING_A":
|
|
return "REFUNDING";
|
|
|
|
default:
|
|
return null; // Return null or a default value if no tradeState matches
|
|
}
|
|
}
|
|
return null; // Return null if creatorAddress doesn't match qortAddressRef.current
|
|
};
|
|
|
|
const processTradeBots = (tradeBots) => {
|
|
let sellTrades = [...tradeBotListRef.current]; // Start with the existing trades
|
|
|
|
tradeBots.forEach((trade) => {
|
|
const status = processTradeBotState(trade);
|
|
|
|
if (status) {
|
|
// Check if the trade is already in the list
|
|
const existingIndex = sellTrades.findIndex(
|
|
(existingTrade) => existingTrade.atAddress === trade.atAddress
|
|
);
|
|
|
|
if (existingIndex > -1) {
|
|
// Replace the existing trade if it exists
|
|
sellTrades[existingIndex] = { ...trade, status };
|
|
} else {
|
|
// Add new trade if it doesn't exist
|
|
sellTrades.push({ ...trade, status });
|
|
}
|
|
}
|
|
});
|
|
setTradeBotList(sellTrades);
|
|
tradeBotListRef.current = sellTrades;
|
|
};
|
|
|
|
const restartTradeOffers = () => {
|
|
if (socketRef.current) {
|
|
socketRef.current.close(1000, "forced"); // Close with a custom reason
|
|
socketRef.current = null;
|
|
}
|
|
offeringTrades.current = [];
|
|
setTradeBotList([]);
|
|
tradeBotListRef.current = [];
|
|
};
|
|
|
|
const socketRef = useRef(null);
|
|
|
|
const initTradeOffersWebSocket = (restarted = false) => {
|
|
let tradeOffersSocketCounter = 0;
|
|
let socketTimeout: any;
|
|
// let socketLink = `ws://127.0.0.1:12391/websockets/crosschain/tradebot?foreignBlockchain=LITECOIN`;
|
|
let socketLink = `${
|
|
window.location.protocol === "https:" ? "wss:" : "ws:"
|
|
}//${baseLocalHost}/websockets/crosschain/tradebot?foreignBlockchain=${selectedCoin}`;
|
|
socketRef.current = new WebSocket(socketLink);
|
|
socketRef.current.onopen = () => {
|
|
setTimeout(pingSocket, 50);
|
|
tradeOffersSocketCounter += 1;
|
|
};
|
|
socketRef.current.onmessage = (e) => {
|
|
tradeOffersSocketCounter += 1;
|
|
restarted = false;
|
|
processTradeBots(JSON.parse(e.data));
|
|
};
|
|
socketRef.current.onclose = (event) => {
|
|
clearTimeout(socketTimeout);
|
|
if (event.reason === "forced") {
|
|
return;
|
|
}
|
|
restartTradeOffersWebSocket();
|
|
};
|
|
socketRef.current.onerror = (e) => {
|
|
clearTimeout(socketTimeout);
|
|
};
|
|
const pingSocket = () => {
|
|
socketRef.current.send("ping");
|
|
socketTimeout = setTimeout(pingSocket, 295000);
|
|
};
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!qortAddress) return;
|
|
if (selectedCoin === null) return;
|
|
restartTradeOffers();
|
|
|
|
setTimeout(() => {
|
|
initTradeOffersWebSocket();
|
|
}, 500);
|
|
return () => {
|
|
if (socketRef.current) {
|
|
socketRef.current.close(1000, "forced");
|
|
}
|
|
};
|
|
}, [qortAddress, selectedCoin]);
|
|
|
|
const onSelectionChanged = (event: any) => {
|
|
const selectedRows = event.api.getSelectedRows();
|
|
if (selectedRows[0]) {
|
|
setSelectedTrade(selectedRows[0]);
|
|
} else {
|
|
setSelectedTrade(null);
|
|
}
|
|
};
|
|
|
|
const handleClose = (
|
|
event?: React.SyntheticEvent | Event,
|
|
reason?: SnackbarCloseReason
|
|
) => {
|
|
if (reason === "clickaway") {
|
|
return;
|
|
}
|
|
|
|
setOpen(false);
|
|
setInfo(null);
|
|
};
|
|
|
|
const cancelSell = async () => {
|
|
try {
|
|
if (!selectedTrade) return;
|
|
setOpen(true);
|
|
|
|
setInfo({
|
|
type: "info",
|
|
message: "Attempting to cancel sell order",
|
|
});
|
|
const res = await qortalRequestWithTimeout(
|
|
{
|
|
action: "CANCEL_TRADE_SELL_ORDER",
|
|
qortAmount: selectedTrade.qortAmount,
|
|
foreignBlockchain: selectedTrade.foreignBlockchain,
|
|
foreignAmount: selectedTrade.foreignAmount,
|
|
atAddress: selectedTrade.atAddress,
|
|
},
|
|
900000
|
|
);
|
|
if (res?.signature) {
|
|
await deleteTemporarySellOrder(selectedTrade.atAddress);
|
|
|
|
setSelectedTrade(null);
|
|
setOpen(true);
|
|
setInfo({
|
|
type: "success",
|
|
message:
|
|
"Sell order canceled. Please wait a couple of minutes for the network to propogate the changes",
|
|
});
|
|
}
|
|
if (res?.error && res?.failedTradeBot) {
|
|
setOpen(true);
|
|
setInfo({
|
|
type: "error",
|
|
message: "Unable to cancel sell order. Please try again.",
|
|
});
|
|
}
|
|
} catch (error) {
|
|
if (error?.error && error?.failedTradeBot) {
|
|
setOpen(true);
|
|
setInfo({
|
|
type: "error",
|
|
message: "Unable to cancel sell order. Please try again.",
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const CancelButton = () => {
|
|
return (
|
|
<button
|
|
disabled={!selectedTrade || selectedTrade?.status === "PENDING"}
|
|
onClick={cancelSell}
|
|
style={{
|
|
borderRadius: "8px",
|
|
width: "150px",
|
|
height: "auto",
|
|
minHeight: "30px",
|
|
background:
|
|
!selectedTrade || selectedTrade?.status === "PENDING"
|
|
? "gray"
|
|
: "#4D7345",
|
|
color: "white",
|
|
cursor:
|
|
!selectedTrade || selectedTrade?.status === "PENDING"
|
|
? "default"
|
|
: "pointer",
|
|
border: "1px solid #375232",
|
|
boxShadow: "0px 2.77px 2.21px 0px #00000005",
|
|
marginRight: "15px",
|
|
}}
|
|
>
|
|
Cancel sell order
|
|
</button>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
}}
|
|
>
|
|
<div
|
|
className="ag-theme-alpine-dark"
|
|
style={{ height: 400, width: "100%" }}
|
|
>
|
|
<AgGridReact
|
|
ref={gridRef}
|
|
columnDefs={columnDefs}
|
|
defaultColDef={defaultColDef}
|
|
rowData={filteredOutTradeBotListWithoutFailed}
|
|
// onRowClicked={onRowClicked}
|
|
onSelectionChanged={onSelectionChanged}
|
|
// getRowStyle={getRowStyle}
|
|
autoSizeStrategy={autoSizeStrategy}
|
|
rowSelection="single" // Enable multi-select
|
|
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
|
|
/>
|
|
{/* {selectedOffer && (
|
|
<Button onClick={buyOrder}>Buy</Button>
|
|
|
|
)} */}
|
|
</div>
|
|
<div
|
|
style={{
|
|
height: "120px",
|
|
}}
|
|
/>
|
|
<Box
|
|
sx={{
|
|
width: "calc(100% - 14px)",
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
position: "fixed",
|
|
bottom: "0px",
|
|
height: "100px",
|
|
padding: "7px",
|
|
background: "#323336",
|
|
}}
|
|
>
|
|
<BuyContainerDivider />
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
gap: "5px",
|
|
flexDirection: "column",
|
|
width: "100%",
|
|
}}
|
|
>
|
|
{/* <Typography sx={{
|
|
fontSize: '16px',
|
|
color: 'white',
|
|
width: 'calc(100% - 75px)'
|
|
}}>{selectedTotalQORT?.toFixed(3)} QORT</Typography> */}
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
gap: "20px",
|
|
alignItems: "center",
|
|
width: "calc(100% - 75px)",
|
|
}}
|
|
>
|
|
{/* <Typography sx={{
|
|
fontSize: '16px',
|
|
color: selectedTotalLTC > foreignCoinBalance ? 'red' : 'white',
|
|
}}><span>{selectedTotalLTC?.toFixed(4)}</span> <span style={{
|
|
marginLeft: 'auto'
|
|
}}>LTC</span></Typography> */}
|
|
</Box>
|
|
{/* <Typography sx={{
|
|
fontSize: '16px',
|
|
color: 'white',
|
|
|
|
}}><span>{foreignCoinBalance?.toFixed(4)}</span> <span style={{
|
|
marginLeft: 'auto'
|
|
}}>LTC balance</span></Typography> */}
|
|
</Box>
|
|
{CancelButton()}
|
|
</Box>
|
|
<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>
|
|
</div>
|
|
);
|
|
}
|