added stuck sell order table

This commit is contained in:
PhilReact 2025-05-09 13:50:31 +03:00
parent e0ab3846d7
commit 733c49e33c
9 changed files with 454 additions and 216 deletions

10
package-lock.json generated
View File

@ -28,6 +28,7 @@
"react-qr-code": "^2.0.15",
"react-router-dom": "^6.23.0",
"react-toastify": "^10.0.5",
"react-virtuoso": "^4.12.7",
"sass": "^1.76.0",
"short-unique-id": "^5.2.0",
"socket.io-client": "^4.7.5"
@ -6846,6 +6847,15 @@
"react-dom": ">=16.6.0"
}
},
"node_modules/react-virtuoso": {
"version": "4.12.7",
"resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.12.7.tgz",
"integrity": "sha512-njJp764he6Fi1p89PUW0k2kbyWu9w/y+MwdxmwK2kvdwwzVDbz2c2wMj5xdSruBFVgFTsI7Z85hxZR7aSHBrbQ==",
"peerDependencies": {
"react": ">=16 || >=17 || >= 18 || >= 19",
"react-dom": ">=16 || >=17 || >= 18 || >=19"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",

View File

@ -30,6 +30,7 @@
"react-qr-code": "^2.0.15",
"react-router-dom": "^6.23.0",
"react-toastify": "^10.0.5",
"react-virtuoso": "^4.12.7",
"sass": "^1.76.0",
"short-unique-id": "^5.2.0",
"socket.io-client": "^4.7.5"

View File

@ -56,12 +56,15 @@ import {
export const baseLocalHost = window.location.host;
// export const baseLocalHost = "devnet-nodes.qortal.link:11111";
// export const baseLocalHost = "127.0.0.1:12391";
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);
@ -97,6 +100,7 @@ export const TradeOffers: React.FC<any> = ({
const [offers, setOffers] = useState<any[]>([]);
const [signedUnlockingFees, setSignedUnlockingFees] = useState(null);
const [qortalNames, setQortalNames] = useState({});
const setStuckTrades = useSetAtom(stuckTradesAtom)
const {
fetchOngoingTransactions,
onGoingTrades,
@ -448,6 +452,10 @@ export const TradeOffers: React.FC<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) {
@ -467,6 +475,11 @@ export const TradeOffers: React.FC<any> = ({
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) => {
@ -549,10 +562,10 @@ export const TradeOffers: React.FC<any> = ({
socketRef.current.onmessage = (e) => {
offeringTrades.current = [
...offeringTrades.current?.filter(
(coin) => coin?.foreignBlockchain === selectedCoin
(coin) => coin?.foreignBlockchain === selectedCoin && coin?.mode === 'OFFERING'
),
...JSON.parse(e.data)?.filter(
(coin) => coin?.foreignBlockchain === selectedCoin
(coin) => coin?.foreignBlockchain === selectedCoin && coin?.mode === 'OFFERING'
),
];
restarted = false;

View File

@ -69,6 +69,8 @@ import UnsignedFees from "../sell/UnsignedFees";
import { FeeManager } from "../sell/FeeManager";
import { Info } from "../sell/Info";
import { Settings } from "../sell/Settings";
import { useSetAtom } from "jotai/react";
import { stuckTradesAtom } from "../../global/state";
const checkIfLocal = async () => {
try {
@ -171,6 +173,8 @@ export const Header = ({
const [amount, setAmount] = useState<string>("");
const [coinAddresses, setCoinAddresses] = useState({});
const { isUsingGateway } = useContext(gameContext);
const setStuckTrades = useSetAtom(stuckTradesAtom)
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setChecked(false);
@ -492,6 +496,7 @@ export const Header = ({
onChange={(e) => {
setFee(null)
setSelectedCoin(e.target.value)
setStuckTrades([])
}}
>
<MenuItem value={"LITECOIN"}>

View File

@ -4,6 +4,7 @@ import {
Button,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
IconButton,
InputLabel,
@ -13,12 +14,16 @@ import {
Typography,
styled,
} from "@mui/material";
import React, { useContext } from "react";
import React, { useContext, useEffect, useState } from "react";
import { BootstrapDialog } from "../Terms";
import CloseIcon from "@mui/icons-material/Close";
import { Spacer } from "../common/Spacer";
import gameContext from "../../contexts/gameContext";
import TradeBotList from "./TradeBotList";
import { stuckTradesAtom } from "../../global/state";
import { useAtom } from "jotai/react";
import { StuckOrdersTable } from "./StuckOrdersTable";
import { useGlobal } from "qapp-core";
export const CustomLabel = styled(InputLabel)`
font-weight: 400;
@ -97,6 +102,7 @@ export const CustomInput = styled(TextField)({
export const CreateSell = ({ qortAddress, show }) => {
const [open, setOpen] = React.useState(false);
const [openStuckOrders, setOpenStuckOrders] = React.useState(false);
const [qortAmount, setQortAmount] = React.useState(0);
const [foreignAmount, setForeignAmount] = React.useState(0);
const {
@ -220,7 +226,21 @@ export const CreateSell = ({ qortAddress, show }) => {
display: show ? "block" : "none",
}}
>
<Button onClick={handleClickOpen}>New Sell Order</Button>
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<Button sx={{
margin: '10px 0px'
}} variant="outlined" onClick={handleClickOpen}>New Sell Order</Button>
{!isUsingGateway && (
<Button sx={{
margin: '10px 0px'
}} variant="outlined" onClick={()=> setOpenStuckOrders(true)}>Stuck orders</Button>
)}
</Box>
<TradeBotList
qortAddress={qortAddress}
failedTradeBots={sellOrders.filter((item) => item.status === "FAILED")}
@ -325,6 +345,55 @@ export const CreateSell = ({ qortAddress, show }) => {
{info?.message}
</Alert>
</Snackbar>
{openStuckOrders && (
<StuckOrders setOpenStuckOrders={setOpenStuckOrders} />
)}
</div>
);
};
const StuckOrders = ({setOpenStuckOrders})=> {
const [stuckTrades] = useAtom(stuckTradesAtom)
const address = useGlobal().auth.address
const filteredByAddress = stuckTrades?.filter((item)=> item?.qortalCreator === address)
return (
<BootstrapDialog
aria-labelledby="customized-dialog-title"
open={true}
maxWidth="lg"
fullWidth={true}
>
<DialogTitle sx={{ m: 0, p: 2 }} id="customized-dialog-title">
Stuck sell orders
</DialogTitle>
<IconButton
aria-label="close"
onClick={()=> setOpenStuckOrders(false)}
sx={(theme) => ({
position: "absolute",
right: 8,
top: 8,
color: theme.palette.grey[500],
})}
>
<CloseIcon />
</IconButton>
<DialogContent dividers>
<DialogContentText></DialogContentText>
<Spacer height="20px" />
{filteredByAddress?.length === 0 && (
<DialogContentText>No stuck trades</DialogContentText>
)}
<Spacer height="20px" />
<StuckOrdersTable data={filteredByAddress} />
</DialogContent>
<DialogActions>
<Button autoFocus onClick={()=> setOpenStuckOrders(false)}>
Close
</Button>
</DialogActions>
</BootstrapDialog>
)
}

View File

@ -83,7 +83,10 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
const [openModal, setOpenModal] = useState(false);
const [recommendedFee, setRecommendedFee] = useState("medium_fee_per_kb");
const {hideRecommendations, recommendedFeeDisplay} = useRecommendedFees({selectedCoin, recommendedFee})
const { hideRecommendations, recommendedFeeDisplay } = useRecommendedFees({
selectedCoin,
recommendedFee,
});
const [openAlert, setOpenAlert] = useState(false);
const [info, setInfo] = useState<any>(null);
@ -143,10 +146,6 @@ const {hideRecommendations, recommendedFeeDisplay} = useRecommendedFees({selecte
establishUpdateFeeForm(coin);
}, [coin, establishUpdateFeeForm]);
useEffect(() => {
if (hideRecommendations) {
setRecommendedFee("custom");
@ -158,13 +157,12 @@ const {hideRecommendations, recommendedFeeDisplay} = useRecommendedFees({selecte
const typeRequestLocking = "feekb";
try {
let feeToSave = editFee;
if (recommendedFee !== "custom") {
feeToSave = calculateFeeFromRate(recommendedFeeDisplay, 300);
}
if(+fee === +feeToSave){
return
if (+fee === +feeToSave) {
return;
}
const response = await qortalRequestWithTimeout(
{
@ -215,8 +213,6 @@ const {hideRecommendations, recommendedFeeDisplay} = useRecommendedFees({selecte
}
};
if (fee === null || fee === undefined) return;
return (
<>
@ -274,6 +270,18 @@ const {hideRecommendations, recommendedFeeDisplay} = useRecommendedFees({selecte
</Typography>
</HeaderRow>
</CoinActionRow>
<CoinActionRow>
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Typography>current fee: {fee}</Typography>
</Box>
</CoinActionRow>
<CoinActionRow>
<HeaderRow>
<Box

View File

@ -52,11 +52,14 @@ export const Settings = () => {
const [lockingFee, setLockingFee] = useState("");
const [recommendedFee, setRecommendedFee] = useState("medium_fee_per_kb");
const [selectedCoin, setSelectedCoin] = useState("LITECOIN");
const { hideRecommendations, recommendedFeeDisplay, coin } = useRecommendedFees({
selectedCoin,
recommendedFee
});
const {
isUsingGateway
} = useContext(gameContext);
const { hideRecommendations, recommendedFeeDisplay, coin } =
useRecommendedFees({
selectedCoin,
recommendedFee,
});
const [editLockingFee, setEditLockingFee] = useState("");
const [openAlert, setOpenAlert] = useState(false);
@ -85,7 +88,7 @@ export const Settings = () => {
try {
let feeToSave = editLockingFee;
if (recommendedFee !== "custom") {
feeToSave = recommendedFeeDisplay
feeToSave = recommendedFeeDisplay;
}
const response = await qortalRequestWithTimeout(
{
@ -183,11 +186,11 @@ export const Settings = () => {
getSavedSelectedPublisher();
}, []);
useEffect(() => {
if (hideRecommendations) {
setRecommendedFee("custom");
}
}, [hideRecommendations]);
useEffect(() => {
if (hideRecommendations) {
setRecommendedFee("custom");
}
}, [hideRecommendations]);
return (
<>
@ -218,197 +221,206 @@ export const Settings = () => {
}}
open={openModal}
>
<CoinActionContainer
sx={{
border: "1px solid #3F3F3F",
borderRadius: "5px",
padding: "5px",
}}
>
<Typography>Locking fees</Typography>
<FormControlLabel
control={
<Checkbox
checked={isEnabledCustomLockingFee}
onChange={handleChange}
/>
}
label="Enable custom locking fee"
/>
{isEnabledCustomLockingFee && (
<>
<CoinSelectRow
sx={{
gap: "20px",
width: '100%',
justifyContent: 'center'
}}
>
<Select
size="small"
value={selectedCoin}
onChange={(e) => {
setLockingFee("");
setEditLockingFee("");
setSelectedCoin(e.target.value);
}}
>
<MenuItem value={"LITECOIN"}>
<SelectRow coin="LTC" />
</MenuItem>
<MenuItem value={"DOGECOIN"}>
<SelectRow coin="DOGE" />
</MenuItem>
<MenuItem value={"BITCOIN"}>
<SelectRow coin="BTC" />
</MenuItem>
<MenuItem value={"DIGIBYTE"}>
<SelectRow coin="DGB" />
</MenuItem>
<MenuItem value={"RAVENCOIN"}>
<SelectRow coin="RVN" />
</MenuItem>
<MenuItem value={"PIRATECHAIN"}>
<SelectRow coin="ARRR" />
</MenuItem>
</Select>
<Box>
</Box>
</CoinSelectRow>
<CoinActionRow>
<HeaderRow>
<Box
sx={{
width: "100%",
}}
>
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<CustomLabel
sx={{
fontSize: "16px",
}}
htmlFor="standard-adornment-name"
>
Recommended fee selection (in sats per kb)
</CustomLabel>
<Spacer height="10px" />
<ToggleButtonGroup
color="primary"
value={recommendedFee}
exclusive
onChange={handleChangeRecommended}
aria-label="Platform"
>
{!hideRecommendations && (
<>
<ToggleButton value="low_fee_per_kb">
Low
</ToggleButton>
<ToggleButton value="medium_fee_per_kb">
Medium
</ToggleButton>
<ToggleButton value="high_fee_per_kb">
High
</ToggleButton>
</>
)}
<ToggleButton value="custom">Custom</ToggleButton>
</ToggleButtonGroup>
</Box>
{recommendedFeeDisplay && (
<>
<Spacer height="15px" />
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
<Typography
sx={{
color: "white",
fontSize: "18px",
}}
>
<span
style={{
fontWeight: "bold",
}}
>
{" "}
New fee:
</span>{" "}
{recommendedFeeDisplay}{" "}
sats per kb
</Typography>
</Box>
<Spacer height="10px" />
</>
)}
</Box>
</HeaderRow>
</CoinActionRow>
{recommendedFee === "custom" && (
<CoinActionRow>
<HeaderRow>
<Box>
<CustomLabel htmlFor="standard-adornment-name">
Custom fee
</CustomLabel>
<Spacer height="5px" />
<CustomInput
id="standard-adornment-name"
type="number"
value={editLockingFee}
onChange={(e) => setEditLockingFee(e.target.value)}
autoComplete="off"
/>
</Box>
</HeaderRow>
</CoinActionRow>
)}
</>
)}
<ButtonBase
onClick={updateLockingFee}
disabled={recommendedFee === "custom" && !editLockingFee}
sx={{
minHeight: "42px",
border: "1px solid gray",
color: "white",
display: "flex",
alignItems: "center",
padding: "5px 20px",
gap: "10px",
borderRadius: "5px",
"&:hover": {
border: "1px solid white", // Border color on hover
},
}}
>
<ChangeCircleIcon
sx={{
color: "white",
}}
/>
<Typography>Update locking fee</Typography>
</ButtonBase>
</CoinActionContainer>
{!isUsingGateway && (
<CoinActionContainer
sx={{
border: "1px solid #3F3F3F",
borderRadius: "5px",
padding: "5px",
}}
>
<Typography>Locking fees</Typography>
<FormControlLabel
control={
<Checkbox
checked={isEnabledCustomLockingFee}
onChange={handleChange}
/>
}
label="Enable custom locking fee"
/>
{isEnabledCustomLockingFee && (
<>
<CoinSelectRow
sx={{
gap: "20px",
width: "100%",
justifyContent: "center",
}}
>
<Select
size="small"
value={selectedCoin}
onChange={(e) => {
setLockingFee("");
setEditLockingFee("");
setSelectedCoin(e.target.value);
}}
>
<MenuItem value={"LITECOIN"}>
<SelectRow coin="LTC" />
</MenuItem>
<MenuItem value={"DOGECOIN"}>
<SelectRow coin="DOGE" />
</MenuItem>
<MenuItem value={"BITCOIN"}>
<SelectRow coin="BTC" />
</MenuItem>
<MenuItem value={"DIGIBYTE"}>
<SelectRow coin="DGB" />
</MenuItem>
<MenuItem value={"RAVENCOIN"}>
<SelectRow coin="RVN" />
</MenuItem>
<MenuItem value={"PIRATECHAIN"}>
<SelectRow coin="ARRR" />
</MenuItem>
</Select>
</CoinSelectRow>
<CoinSelectRow>
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Typography>current fee: {lockingFee}</Typography>
</Box>
</CoinSelectRow>
<CoinActionRow>
<HeaderRow>
<Box
sx={{
width: "100%",
}}
>
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<CustomLabel
sx={{
fontSize: "16px",
}}
htmlFor="standard-adornment-name"
>
Recommended fee selection (in sats per kb)
</CustomLabel>
<Spacer height="10px" />
<ToggleButtonGroup
color="primary"
value={recommendedFee}
exclusive
onChange={handleChangeRecommended}
aria-label="Platform"
>
{!hideRecommendations && (
<>
<ToggleButton value="low_fee_per_kb">
Low
</ToggleButton>
<ToggleButton value="medium_fee_per_kb">
Medium
</ToggleButton>
<ToggleButton value="high_fee_per_kb">
High
</ToggleButton>
</>
)}
<ToggleButton value="custom">Custom</ToggleButton>
</ToggleButtonGroup>
</Box>
{recommendedFeeDisplay && (
<>
<Spacer height="15px" />
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
<Typography
sx={{
color: "white",
fontSize: "18px",
}}
>
<span
style={{
fontWeight: "bold",
}}
>
{" "}
New fee:
</span>{" "}
{recommendedFeeDisplay} sats per kb
</Typography>
</Box>
<Spacer height="10px" />
</>
)}
</Box>
</HeaderRow>
</CoinActionRow>
{recommendedFee === "custom" && (
<CoinActionRow>
<HeaderRow>
<Box>
<CustomLabel htmlFor="standard-adornment-name">
Custom fee
</CustomLabel>
<Spacer height="5px" />
<CustomInput
id="standard-adornment-name"
type="number"
value={editLockingFee}
onChange={(e) => setEditLockingFee(e.target.value)}
autoComplete="off"
/>
</Box>
</HeaderRow>
</CoinActionRow>
)}
</>
)}
<ButtonBase
onClick={updateLockingFee}
disabled={recommendedFee === "custom" && !editLockingFee}
sx={{
minHeight: "42px",
border: "1px solid gray",
color: "white",
display: "flex",
alignItems: "center",
padding: "5px 20px",
gap: "10px",
borderRadius: "5px",
"&:hover": {
border: "1px solid white", // Border color on hover
},
}}
>
<ChangeCircleIcon
sx={{
color: "white",
}}
/>
<Typography>Update locking fee</Typography>
</ButtonBase>
</CoinActionContainer>
)}
<Spacer height="20px" />
<CoinActionContainer
sx={{

View File

@ -0,0 +1,118 @@
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Button,
} from "@mui/material";
import { forwardRef } from "react";
import { TableVirtuoso, TableComponents } from "react-virtuoso";
import {
dismissToast,
showError,
showLoading,
showSuccess,
useGlobal,
} from "qapp-core";
import { formatTimestampForum } from "../../utils/formatTime";
interface NameData {
name: string;
isSelling?: boolean;
}
const VirtuosoTableComponents: TableComponents<NameData> = {
Scroller: forwardRef<HTMLDivElement>((props, ref) => (
<TableContainer component={Paper} {...props} ref={ref} />
)),
Table: (props) => (
<Table
{...props}
sx={{ borderCollapse: "separate", tableLayout: "fixed" }}
/>
),
TableHead: forwardRef<HTMLTableSectionElement>((props, ref) => (
<TableHead {...props} ref={ref} />
)),
TableRow,
TableBody: forwardRef<HTMLTableSectionElement>((props, ref) => (
<TableBody {...props} ref={ref} />
)),
};
function fixedHeaderContent() {
return (
<TableRow sx={{ backgroundColor: "background.paper" }}>
<TableCell>Date</TableCell>
<TableCell>Amount (QORT)</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
);
}
function rowContent(_index: number, row: NameData) {
const cancelSell = async (name: string) => {
const loadId = showLoading("Attempting to cancel sell...please wait");
try {
const res = await qortalRequestWithTimeout(
{
action: "CANCEL_TRADE_SELL_ORDER",
qortAmount: row.qortAmount,
foreignBlockchain: row.foreignBlockchain,
foreignAmount: row.foreignAmount,
atAddress: row.qortalAtAddress,
},
900000
);
if (res?.signature) {
showSuccess(
"Canceled sell order. It might take awhile for the cancel sell order changes to show up."
);
}
} catch (error) {
showError(error?.message || "Unable to cancel sell order");
console.log("error", error);
} finally {
dismissToast(loadId);
}
};
return (
<>
<TableCell>{formatTimestampForum(row?.timestamp)}</TableCell>
<TableCell>{row?.qortAmount}</TableCell>
<TableCell>
<Button
variant="contained"
size="small"
onClick={() => cancelSell(row.name)}
>
Cancel sell
</Button>
</TableCell>
</>
);
}
export const StuckOrdersTable = ({ data }) => {
return (
<Paper
sx={{
height: "80vh", // Header + footer height
width: "100%",
}}
>
<TableVirtuoso
data={data}
components={VirtuosoTableComponents}
fixedHeaderContent={fixedHeaderContent}
itemContent={(index, row) => rowContent(index, row)}
/>
</Paper>
);
};

View File

@ -5,3 +5,5 @@ import { atomWithReset } from 'jotai/utils';
export const selectedFeePublisherAtom = atomWithReset('Ice.JSON');
export const isEnabledCustomLockingFeeAtom = atomWithReset(false);
export const stuckTradesAtom = atomWithReset([]);