From 8a8239f5bbbef8f922d8d32780cb07b2e62535a8 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Wed, 25 Dec 2024 07:12:33 +0200 Subject: [PATCH] added buying modal --- package-lock.json | 106 ++- package.json | 2 + src/components/Grids/TradeOffers.tsx | 1139 ++++++++++++++++---------- src/components/sell/CreateSell.tsx | 4 +- 4 files changed, 807 insertions(+), 444 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9a44d49..d6a4b93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,8 +19,10 @@ "lodash": "^4.17.21", "moment": "^2.30.1", "react": "^18.2.0", + "react-countdown-circle-timer": "^3.2.1", "react-dom": "^18.2.0", "react-ga4": "^2.1.0", + "react-loader-spinner": "^6.1.6", "react-router-dom": "^6.23.0", "react-toastify": "^10.0.5", "sass": "^1.76.0", @@ -3236,6 +3238,11 @@ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -3826,6 +3833,14 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001660", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz", @@ -4003,6 +4018,24 @@ "node": ">=8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -5984,7 +6017,6 @@ "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, "funding": [ { "type": "github", @@ -6218,7 +6250,6 @@ "version": "8.4.38", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", - "dev": true, "funding": [ { "type": "opencollective", @@ -6242,6 +6273,11 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -6332,6 +6368,14 @@ "node": ">=0.10.0" } }, + "node_modules/react-countdown-circle-timer": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/react-countdown-circle-timer/-/react-countdown-circle-timer-3.2.1.tgz", + "integrity": "sha512-yBAy/9ILXOiFbLBM+3jS72TW5LeRcH8wkRC9NNqMpUkCXkGjSnaeRbJMsR9lsYF0oVXjSDbJaRbCuVMT+9HnKA==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -6354,6 +6398,22 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, + "node_modules/react-loader-spinner": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-6.1.6.tgz", + "integrity": "sha512-x5h1Jcit7Qn03MuKlrWcMG9o12cp9SNDVHVJTNRi9TgtGPKcjKiXkou4NRfLAtXaFB3+Z8yZsVzONmPzhv2ErA==", + "dependencies": { + "react-is": "^18.2.0", + "styled-components": "^6.1.2" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -6788,6 +6848,11 @@ "node": ">= 0.4" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7041,6 +7106,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/styled-components": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.13.tgz", + "integrity": "sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw==", + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.38", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -7210,6 +7307,11 @@ "typescript": ">=4.2.0" } }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 25ab599..f23f0d3 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,10 @@ "lodash": "^4.17.21", "moment": "^2.30.1", "react": "^18.2.0", + "react-countdown-circle-timer": "^3.2.1", "react-dom": "^18.2.0", "react-ga4": "^2.1.0", + "react-loader-spinner": "^6.1.6", "react-router-dom": "^6.23.0", "react-toastify": "^10.0.5", "sass": "^1.76.0", diff --git a/src/components/Grids/TradeOffers.tsx b/src/components/Grids/TradeOffers.tsx index 2807901..cabf34c 100644 --- a/src/components/Grids/TradeOffers.tsx +++ b/src/components/Grids/TradeOffers.tsx @@ -1,18 +1,47 @@ -import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; -import { AgGridReact } from 'ag-grid-react'; -import { ColDef, RowClassParams, RowStyle, SizeColumnsToContentStrategy } from 'ag-grid-community'; -import 'ag-grid-community/styles/ag-grid.css'; -import 'ag-grid-community/styles/ag-theme-alpine.css'; -import axios from 'axios'; -import { sendRequestToExtension } from '../../App'; -import { Alert, Box, Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Snackbar, SnackbarCloseReason, 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 React, { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { AgGridReact } from "ag-grid-react"; +import { + ColDef, + RowClassParams, + RowStyle, + SizeColumnsToContentStrategy, +} from "ag-grid-community"; +import "ag-grid-community/styles/ag-grid.css"; +import "ag-grid-community/styles/ag-theme-alpine.css"; +import axios from "axios"; +import { sendRequestToExtension } from "../../App"; +import { + Alert, + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Snackbar, + SnackbarCloseReason, + 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"; -export const baseLocalHost = window.location.host -// export const baseLocalHost = '127.0.0.1:12391' +// export const baseLocalHost = window.location.host +export const baseLocalHost = "127.0.0.1:12391"; interface RowData { amountQORT: number; @@ -22,26 +51,35 @@ interface RowData { } export const saveFileToDisk = async (data) => { - const dataString = JSON.stringify(data); - const blob = new Blob([dataString], { type: 'application/json' }); -const fileName = "traderecord_" + Date.now() + '_' + ".json"; + 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' + await FileSaver.saveAs(blob, fileName); }; -export const TradeOffers: React.FC = ({foreignCoinBalance}:any) => { - const [offers, setOffers] = useState([]) - const [qortalNames, setQortalNames] = useState({}) - const { fetchOngoingTransactions, onGoingTrades, updateTransactionInDB, isUsingGateway, getCoinLabel, selectedCoin } = useContext(gameContext); - const listOfOngoingTradesAts = useMemo(()=> { - return onGoingTrades?.filter((item)=> item?.status !== 'trade-failed')?.map((trade)=> trade?.qortalAtAddress) || [] - }, [onGoingTrades]) +export const autoSizeStrategy: SizeColumnsToContentStrategy = { + type: "fitCellContents", +}; + +export const TradeOffers: React.FC = ({ foreignCoinBalance }: any) => { + const [offers, setOffers] = useState([]); + const [qortalNames, setQortalNames] = useState({}); + const { + fetchOngoingTransactions, + onGoingTrades, + updateTransactionInDB, + isUsingGateway, + getCoinLabel, + selectedCoin, + } = useContext(gameContext); + const listOfOngoingTradesAts = useMemo(() => { + return ( + onGoingTrades + ?.filter((item) => item?.status !== "trade-failed") + ?.map((trade) => trade?.qortalAtAddress) || [] + ); + }, [onGoingTrades]); const { isShow: isShowInfo, onCancel: onCancelInfo, @@ -50,121 +88,149 @@ export const TradeOffers: React.FC = ({foreignCoinBalance}:any) => { message: messageInfo, } = useModal(); - const offersWithoutOngoing = useMemo(()=> { - return offers.filter((item)=> !listOfOngoingTradesAts.includes(item.qortalAtAddress)) - }, [listOfOngoingTradesAts, offers]) - const initiatedFetchPresence = useRef(false) - const initiatedFetchPresenceSocket = useRef(false) + 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 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 [open, setOpen] = useState(false) - const [info, setInfo] = useState(null) + const [open, setOpen] = useState(false); + const [info, setInfo] = useState(null); const BuyButton = () => { return ( - ); }; - + 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)=> { + 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 - } - }) - } + 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 = ()=> { + const restartTradeOffers = () => { if (socketRef.current) { - socketRef.current.close(1000, 'forced'); // Close with a custom reason - socketRef.current = null + socketRef.current.close(1000, "forced"); // Close with a custom reason + socketRef.current = null; } - offeringTrades.current = [] - setOffers([]) - setSelectedOffer(null) - } + offeringTrades.current = []; + setOffers([]); + setSelectedOffer(null); + }; - const restartPresence = ()=> { + const restartPresence = () => { if (socketPresenceRef.current) { - socketPresenceRef.current.close(1000, 'forced'); // Close with a custom reason - socketPresenceRef.current = null + socketPresenceRef.current.close(1000, "forced"); // Close with a custom reason + socketPresenceRef.current = null; } - } + }; - const columnDefs: ColDef[] = useMemo(()=> { + 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 headerName: "", // You can customize the header name width: 50, // Adjust the width as needed - pinned: 'left', // Optional, to pin this column on the left + 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: "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]) + { + 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: "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]); // const onRowClicked = (event: any) => { // if(listOfOngoingTradesAts.includes(event.data.qortalAtAddress)) return @@ -173,235 +239,256 @@ export const TradeOffers: React.FC = ({foreignCoinBalance}:any) => { // }; const restartTradePresenceWebSocket = () => { - restartPresence() - setTimeout(() => initTradePresenceWebSocket(true), 50) - } - - + restartPresence(); + setTimeout(() => initTradePresenceWebSocket(true), 50); + }; const getNewBlockedTrades = async () => { const unconfirmedTransactionsList = async () => { + const unconfirmedTransactionslUrl = `/transactions/unconfirmed?txType=MESSAGE&limit=0&reverse=true`; - const unconfirmedTransactionslUrl = `/transactions/unconfirmed?txType=MESSAGE&limit=0&reverse=true` + var addBlockedTrades = JSON.parse( + localStorage.getItem("failedTrades") || "[]" + ); - 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) - } + await fetch(unconfirmedTransactionslUrl) + .then((response) => { + return response.json(); }) - localStorage.setItem("failedTrades", JSON.stringify(addBlockedTrades)) - blockedTradesList.current = JSON.parse(localStorage.getItem('failedTrades') || '[]') - }) - } + .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() + 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") || "[]") - } + 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() - } + await filterUnconfirmedTransactionsList(); + processOffersWithPresence(); + }; - const executeGetNewBlockTrades = useCallback(()=> { - getNewBlockedTrades() - - }, []) + const executeGetNewBlockTrades = useCallback(() => { + getNewBlockedTrades(); + }, []); useEffect(() => { subscribeToEvent("execute-get-new-block-trades", executeGetNewBlockTrades); return () => { - unsubscribeFromEvent("execute-get-new-block-trades", executeGetNewBlockTrades); + unsubscribeFromEvent( + "execute-get-new-block-trades", + executeGetNewBlockTrades + ); }; }, []); const processOffersWithPresence = () => { - if (offeringTrades.current === null) return + 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) + 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); + const offerIndex = offeringTrades.current.findIndex( + (offeringTrade) => + offeringTrade.qortalCreatorTradeAddress === + tradePresence.tradeAddress + ); if (offerIndex !== -1) { - offeringTrades.current[offerIndex].tradePresenceExpiry = tradePresence.timestamp; + offeringTrades.current[offerIndex].tradePresenceExpiry = + tradePresence.timestamp; } } } - let filteredOffers = offeringTrades.current?.filter((offeringTrade) => filterOffersUsingTradePresence(offeringTrade)) || [] - let tradesPresenceCleaned: any[] = filteredOffers - + 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 - }) || [] - }) + const toDelete = item.recipient; + tradesPresenceCleaned = + tradesPresenceCleaned?.filter((el) => { + return el.qortalCreatorTradeAddress !== toDelete; + }) || []; + }); if (tradesPresenceCleaned) { - updateGridData(tradesPresenceCleaned) + updateGridData(tradesPresenceCleaned); } - } + }; - startOfferPresenceMapping() - } + startOfferPresenceMapping(); + }; const restartTradeOffersWebSocket = () => { - setTimeout(() => initTradeOffersWebSocket(true), 50) - } + 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` + 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`; - + socketLink = `${ + window.location.protocol === "https:" ? "wss:" : "ws:" + }//${baseLocalHost}/websockets/crosschain/tradepresence`; } - - - socketPresenceRef.current = new WebSocket(socketLink) + + socketPresenceRef.current = new WebSocket(socketLink); socketPresenceRef.current.onopen = () => { - setTimeout(pingSocket, 50) - } + 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 - } + 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 + clearTimeout(socketTimeout); + if (event.reason === "forced") { + return; } - restartTradePresenceWebSocket() - } + restartTradePresenceWebSocket(); + }; socketPresenceRef.current.onerror = (e) => { - clearTimeout(socketTimeout) - restartTradePresenceWebSocket() - } + clearTimeout(socketTimeout); + restartTradePresenceWebSocket(); + }; const pingSocket = () => { - socketPresenceRef.current.send('ping') - socketTimeout = setTimeout(pingSocket, 295000) - } - } - - - + socketPresenceRef.current.send("ping"); + socketTimeout = setTimeout(pingSocket, 295000); + }; + }; const initTradeOffersWebSocket = (restarted = false) => { + if (socketRef.current) return; + let socketTimeout: any; - if(socketRef.current) return - let socketTimeout: any - - let socketLink - if(isUsingGateway){ - socketLink = `wss://appnode.qortal.org/websockets/crosschain/tradeoffers?foreignBlockchain=${selectedCoin}&includeHistoric=true` + 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` - + socketLink = `${ + window.location.protocol === "https:" ? "wss:" : "ws:" + }//${baseLocalHost}/websockets/crosschain/tradeoffers?foreignBlockchain=${selectedCoin}&includeHistoric=true`; } - socketRef.current = new WebSocket(socketLink) + socketRef.current = new WebSocket(socketLink); socketRef.current.onopen = () => { - setTimeout(pingSocket, 50) - } + 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() - } + 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 + clearTimeout(socketTimeout); + if (event.reason === "forced") { + return; } - restartTradeOffersWebSocket() - socketRef.current = null - } + restartTradeOffersWebSocket(); + socketRef.current = null; + }; socketRef.current.onerror = (e) => { - clearTimeout(socketTimeout) - } + clearTimeout(socketTimeout); + }; const pingSocket = () => { - socketRef.current.send('ping') - socketTimeout = setTimeout(pingSocket, 295000) - } - } - + socketRef.current.send("ping"); + socketTimeout = setTimeout(pingSocket, 295000); + }; + }; useEffect(() => { - blockedTradesList.current = JSON.parse(localStorage.getItem('failedTrades') || '[]') - if(!initiatedFetchPresence.current){ - initiatedFetchPresence.current = true - initTradePresenceWebSocket() - + blockedTradesList.current = JSON.parse( + localStorage.getItem("failedTrades") || "[]" + ); + if (!initiatedFetchPresence.current) { + initiatedFetchPresence.current = true; + initTradePresenceWebSocket(); } - getNewBlockedTrades() + getNewBlockedTrades(); const intervalBlockTrades = setInterval(() => { - - initiatedFetchPresenceSocket.current = false - restartPresence() - initTradePresenceWebSocket() - getNewBlockedTrades() - }, 150000) + initiatedFetchPresenceSocket.current = false; + restartPresence(); + initTradePresenceWebSocket(); + getNewBlockedTrades(); + }, 150000); return () => { - clearInterval(intervalBlockTrades) - } - }, [isUsingGateway]) - - + clearInterval(intervalBlockTrades); + }; + }, [isUsingGateway]); useEffect(() => { - if(selectedCoin === null) return - restartTradeOffers() + if (selectedCoin === null) return; + restartTradeOffers(); setTimeout(() => { - initTradeOffersWebSocket() - + initTradeOffersWebSocket(); }, 500); return () => { - if(socketRef.current){ - socketRef.current.close(1000, 'forced'); + if (socketRef.current) { + socketRef.current.close(1000, "forced"); } - } - }, [isUsingGateway, selectedCoin]) - - - - + }; + }, [isUsingGateway, selectedCoin]); const selectedTotalLTC = useMemo(() => { return selectedOffers.reduce((acc: number, curr: any) => { @@ -409,7 +496,6 @@ export const TradeOffers: React.FC = ({foreignCoinBalance}:any) => { }, 0); }, [selectedOffers]); - const buyOrder = async () => { try { if(+foreignCoinBalance < +selectedTotalLTC.toFixed(4)){ @@ -420,35 +506,50 @@ export const TradeOffers: React.FC = ({foreignCoinBalance}:any) => { }) return } - - if (selectedOffers?.length < 1) return - setOpen(true) - setInfo({ - type: 'info', - message: "Attempting to submit buy order. Please wait..." - }) - const listOfATs = selectedOffers - const response = await qortalRequestWithTimeout({ - action: "CREATE_TRADE_BUY_ORDER", - crosschainAtInfo: listOfATs, - foreignBlockchain: selectedCoin - }, 900000); - - if(response?.error){ - setOpen(true) - setInfo({ - type: 'error', - message: response?.error || "Failed to submit trade order." - }) - return + + if (selectedOffers?.length < 1) return; + + setIsShowBuyInProgress({ status: "buying" }); + + // setOpen(true) + // setInfo({ + // type: 'info', + // message: "Attempting to submit buy order. Please wait..." + // }) + const listOfATs = selectedOffers; + 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) { - setSelectedOffers([]) + 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', + status: response?.extra?.status + ? response?.extra?.status + : response.callResponse === true + ? "trade-ongoing" + : "trade-failed", encryptedMessageToBase58: response?.encryptedMessageToBase58, chatSignature: response?.chatSignature, sender: response?.extra?.senderAddress, @@ -456,43 +557,44 @@ export const TradeOffers: React.FC = ({foreignCoinBalance}:any) => { reference: response?.callResponse?.reference, }; - - // Update transactions in IndexedDB const result = await updateTransactionInDB(transactionData); - setOpen(true) + setOpen(true); setInfo({ - type: 'success', - message: "Submitted Order" - }) - fetchOngoingTransactions() - if(isUsingGateway){ - setRecord(transactionData) + 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) { - setOpen(true) - setInfo({ - type: 'error', - message: error?.error || error?.message || "Failed to submit trade order." - }) - console.error(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 => { - + const getRowStyle = ( + params: RowClassParams + ): RowStyle | undefined => { if (listOfOngoingTradesAts.includes(params.data.qortalAtAddress)) { - return { background: '#D9D9D91A'}; + return { background: "#D9D9D91A" }; } if (params.data.qortalAtAddress === selectedOffer?.qortalAtAddress) { - return { background: '#6D94F533'}; + return { background: "#6D94F533" }; } return undefined; }; @@ -503,146 +605,171 @@ export const TradeOffers: React.FC = ({foreignCoinBalance}:any) => { 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(() => { - return selectedOffers.reduce((acc: number, curr: any) => { - return acc + (+curr.qortAmount || 0); // Ensure qortAmount is defined - }, 0); -}, [selectedOffers]); + const selectedTotalQORT = useMemo(() => { + return selectedOffers.reduce((acc: number, curr: any) => { + return acc + (+curr.qortAmount || 0); // Ensure qortAmount is defined + }, 0); + }, [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 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; + } - -const handleClose = ( - event?: React.SyntheticEvent | Event, - reason?: SnackbarCloseReason, -) => { - if (reason === 'clickaway') { - return; - } - - setOpen(false); - setInfo(null) -}; - - - + setOpen(false); + setInfo(null); + }; return ( - -
- params.data.qortalAtAddress} // Ensure rows have unique IDs - /> - {/* {selectedOffer && ( + +
+ params.data.qortalAtAddress} // Ensure rows have unique IDs + /> + {/* {selectedOffer && ( )} */} -
-
- - - {selectedTotalQORT?.toFixed(3)} QORT - - foreignCoinBalance ? 'red' : 'white', - }}>{selectedTotalLTC?.toFixed(4)} {`${getCoinLabel()} `} - - +
+
+ + + + {selectedTotalQORT?.toFixed(3)} QORT + + + foreignCoinBalance ? "red" : "white", + }} + > + {selectedTotalLTC?.toFixed(4)}{" "} + {`${getCoinLabel()} `} + + + + {foreignCoinBalance?.toFixed(4)}{" "} + + {`${getCoinLabel()} `} balance + + - {foreignCoinBalance?.toFixed(4)} {`${getCoinLabel()} `} balance + {BuyButton()} - {BuyButton()} - - + {info?.message} @@ -655,35 +782,167 @@ const handleClose = ( > {"Download record"} - + {messageInfo.message} - + - )} + {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}} + + + + )} + + )} + + + + + + )} ); }; - diff --git a/src/components/sell/CreateSell.tsx b/src/components/sell/CreateSell.tsx index 66f3f14..d4e6c7a 100644 --- a/src/components/sell/CreateSell.tsx +++ b/src/components/sell/CreateSell.tsx @@ -232,7 +232,7 @@ export const CreateSell = ({qortAddress, show}) => { /> - {`Price Each (${getCoinLabel()})`} + {`Price of Each QORT (in ${getCoinLabel()})`} { {`${qortAmount * foreignAmount} ${getCoinLabel()}`} for {qortAmount} QORT Total sell amount needs to be greater than: {minimumAmountSellTrades[selectedCoin]?.value} {' '} {minimumAmountSellTrades[selectedCoin]?.ticker}