diff --git a/package-lock.json b/package-lock.json index e3bd3b6..709635a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 97b69d5..52899b4 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/components/Grids/TradeOffers.tsx b/src/components/Grids/TradeOffers.tsx index 961e2c8..7e071ac 100644 --- a/src/components/Grids/TradeOffers.tsx +++ b/src/components/Grids/TradeOffers.tsx @@ -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 = ({ const [offers, setOffers] = useState([]); 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 = ({ 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 = ({ 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 = ({ 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; diff --git a/src/components/header/Header.tsx b/src/components/header/Header.tsx index 99bda52..28fda0a 100644 --- a/src/components/header/Header.tsx +++ b/src/components/header/Header.tsx @@ -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(""); const [coinAddresses, setCoinAddresses] = useState({}); const { isUsingGateway } = useContext(gameContext); + const setStuckTrades = useSetAtom(stuckTradesAtom) + const handleChange = (event: ChangeEvent) => { setChecked(false); @@ -492,6 +496,7 @@ export const Header = ({ onChange={(e) => { setFee(null) setSelectedCoin(e.target.value) + setStuckTrades([]) }} > diff --git a/src/components/sell/CreateSell.tsx b/src/components/sell/CreateSell.tsx index 49d3a2e..c395779 100644 --- a/src/components/sell/CreateSell.tsx +++ b/src/components/sell/CreateSell.tsx @@ -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", }} > - + + + {!isUsingGateway && ( + + )} + + item.status === "FAILED")} @@ -325,6 +345,55 @@ export const CreateSell = ({ qortAddress, show }) => { {info?.message} + {openStuckOrders && ( + + )} ); }; + + +const StuckOrders = ({setOpenStuckOrders})=> { + const [stuckTrades] = useAtom(stuckTradesAtom) + const address = useGlobal().auth.address + const filteredByAddress = stuckTrades?.filter((item)=> item?.qortalCreator === address) + + return ( + + + Stuck sell orders + + setOpenStuckOrders(false)} + sx={(theme) => ({ + position: "absolute", + right: 8, + top: 8, + color: theme.palette.grey[500], + })} + > + + + + + + {filteredByAddress?.length === 0 && ( + No stuck trades + )} + + + + + + + + ) +} \ No newline at end of file diff --git a/src/components/sell/FeeManager.tsx b/src/components/sell/FeeManager.tsx index 2c3e7f0..bc40a1d 100644 --- a/src/components/sell/FeeManager.tsx +++ b/src/components/sell/FeeManager.tsx @@ -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(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 + + + current fee: {fee} + + { 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} > - - Locking fees - - } - label="Enable custom locking fee" - /> - - {isEnabledCustomLockingFee && ( - <> - - - - - - - - - - - - Recommended fee selection (in sats per kb) - - - - - {!hideRecommendations && ( - <> - - Low - - - Medium - - - High - - - )} - - Custom - - - {recommendedFeeDisplay && ( - <> - - - - - {" "} - New fee: - {" "} - {recommendedFeeDisplay}{" "} - sats per kb - - - - - - )} - - - - {recommendedFee === "custom" && ( - - - - - Custom fee - - - setEditLockingFee(e.target.value)} - autoComplete="off" - /> - - - - )} - - )} - - - - - Update locking fee - - + {!isUsingGateway && ( + + Locking fees + + } + label="Enable custom locking fee" + /> + + {isEnabledCustomLockingFee && ( + <> + + + + + + current fee: {lockingFee} + + + + + + + + Recommended fee selection (in sats per kb) + + + + + {!hideRecommendations && ( + <> + + Low + + + Medium + + + High + + + )} + + Custom + + + {recommendedFeeDisplay && ( + <> + + + + + {" "} + New fee: + {" "} + {recommendedFeeDisplay} sats per kb + + + + + + )} + + + + {recommendedFee === "custom" && ( + + + + + Custom fee + + + setEditLockingFee(e.target.value)} + autoComplete="off" + /> + + + + )} + + )} + + + + Update locking fee + + + )} = { + Scroller: forwardRef((props, ref) => ( + + )), + Table: (props) => ( + + ), + TableHead: forwardRef((props, ref) => ( + + )), + TableRow, + TableBody: forwardRef((props, ref) => ( + + )), +}; + +function fixedHeaderContent() { + return ( + + Date + Amount (QORT) + Actions + + ); +} + +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 ( + <> + {formatTimestampForum(row?.timestamp)} + {row?.qortAmount} + + + + + ); +} + +export const StuckOrdersTable = ({ data }) => { + return ( + + rowContent(index, row)} + /> + + ); +}; diff --git a/src/global/state.ts b/src/global/state.ts index 74ee66c..1297334 100644 --- a/src/global/state.ts +++ b/src/global/state.ts @@ -5,3 +5,5 @@ import { atomWithReset } from 'jotai/utils'; export const selectedFeePublisherAtom = atomWithReset('Ice.JSON'); export const isEnabledCustomLockingFeeAtom = atomWithReset(false); + +export const stuckTradesAtom = atomWithReset([]);