From bbfd29e8fbaf46789a100b9b9f681251d329a289 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Sat, 3 May 2025 23:21:10 +0300 Subject: [PATCH] added lock fee functionality --- package-lock.json | 15 ++- package.json | 2 +- src/components/Grids/TradeOffers.tsx | 14 ++- src/components/header/Header.tsx | 2 +- src/components/sell/FeeManager.tsx | 88 +++++++++++++----- src/components/sell/Settings.tsx | 131 +++++++++++++++++---------- src/global.d.ts | 2 +- src/global/state.ts | 4 +- 8 files changed, 179 insertions(+), 79 deletions(-) diff --git a/package-lock.json b/package-lock.json index f2853c1..44f6d18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "jotai": "^2.12.3", "lodash": "^4.17.21", "moment": "^2.30.1", - "qapp-core": "^1.0.22", + "qapp-core": "^1.0.24", "react": "^19.1.0", "react-countdown-circle-timer": "^3.2.1", "react-dom": "^19.1.0", @@ -4298,6 +4298,12 @@ "node": ">=0.4.0" } }, + "node_modules/dexie": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.11.tgz", + "integrity": "sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A==", + "license": "Apache-2.0" + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -6575,9 +6581,9 @@ } }, "node_modules/qapp-core": { - "version": "1.0.22", - "resolved": "https://registry.npmjs.org/qapp-core/-/qapp-core-1.0.22.tgz", - "integrity": "sha512-3q8Ebr9lpyDW7lTo91Rlak2EGIAXrU9DYhUBuLsYZMuRyI/bKoc0E4hHrrAo946eJ/5AxqNGmZDkYSJq5tOTZg==", + "version": "1.0.24", + "resolved": "https://registry.npmjs.org/qapp-core/-/qapp-core-1.0.24.tgz", + "integrity": "sha512-KnrwiysaHlTR1rUnPGwN79gQgcjvtLr4xRH3EGGQcbXKCcbrklV7lv4KLUfFR4UwhskbgOXpJY5PECWdrlGXSw==", "license": "MIT", "dependencies": { "@tanstack/react-virtual": "^3.13.2", @@ -6586,6 +6592,7 @@ "compressorjs": "^1.2.1", "crypto-js": "^4.2.0", "dayjs": "^1.11.13", + "dexie": "^4.0.11", "dompurify": "^3.2.4", "react-dropzone": "^14.3.8", "react-hot-toast": "^2.5.2", diff --git a/package.json b/package.json index 0be65a9..e4d50e6 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "jotai": "^2.12.3", "lodash": "^4.17.21", "moment": "^2.30.1", - "qapp-core": "^1.0.22", + "qapp-core": "^1.0.24", "react": "^19.1.0", "react-countdown-circle-timer": "^3.2.1", "react-dom": "^19.1.0", diff --git a/src/components/Grids/TradeOffers.tsx b/src/components/Grids/TradeOffers.tsx index cf8bc10..c540a05 100644 --- a/src/components/Grids/TradeOffers.tsx +++ b/src/components/Grids/TradeOffers.tsx @@ -60,6 +60,7 @@ export const baseLocalHost = window.location.host; import CloseIcon from "@mui/icons-material/Close"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import moment from "moment"; +import { RequestQueueWithPromise } from "qapp-core"; const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); @@ -84,6 +85,9 @@ export const autoSizeStrategy: SizeColumnsToContentStrategy = { type: "fitCellContents", }; + const requestQueueGetNames = new RequestQueueWithPromise(4); + + export const TradeOffers: React.FC = ({ foreignCoinBalance, fee, @@ -175,9 +179,17 @@ export const TradeOffers: React.FC = ({ setIsRemoveOrders(val); }; + const isFetchingName = useRef({}) + const getName = async (address) => { try { - const response = await fetch("/names/address/" + address); + if(isFetchingName.current[address]) return + isFetchingName.current[address] = true + const response = await requestQueueGetNames.enqueue( + () => { + return fetch("/names/address/" + address); + } + ); const nameData = await response.json(); if (nameData?.length > 0) { setQortalNames((prev) => { diff --git a/src/components/header/Header.tsx b/src/components/header/Header.tsx index d282b42..99bda52 100644 --- a/src/components/header/Header.tsx +++ b/src/components/header/Header.tsx @@ -549,7 +549,7 @@ export const Header = ({ setSenderAddress(""); }} backdrop - open={openCoinActionModal} + open={!!openCoinActionModal} > {openCoinActionModal.type === "send" ? ( diff --git a/src/components/sell/FeeManager.tsx b/src/components/sell/FeeManager.tsx index a0bd558..56db366 100644 --- a/src/components/sell/FeeManager.tsx +++ b/src/components/sell/FeeManager.tsx @@ -30,7 +30,26 @@ import { usePublish, Service, QortalGetMetadata } from "qapp-core"; import { SetLeftFeature } from "ag-grid-community"; import { formatTimestampForum } from "../../utils/formatTime"; import { useAtom } from "jotai/react"; -import { selectedFeePublisherAtom } from "../../global/state"; +import { isEnabledCustomLockingFeeAtom, selectedFeePublisherAtom } from "../../global/state"; + +type FeeEstimate = { + height: number; + time: number; + low_fee_per_kb: number; + medium_fee_per_kb: number; + high_fee_per_kb: number; +}; + function isValidFeeEstimate(obj: any): obj is FeeEstimate { + return ( + typeof obj === 'object' && + obj !== null && + typeof obj.height === 'number' && + typeof obj.time === 'number' && + typeof obj.low_fee_per_kb === 'number' && + typeof obj.medium_fee_per_kb === 'number' && + typeof obj.high_fee_per_kb === 'number' + ); +} function calculateFeeFromRate(feePerKb, sizeInBytes) { const fee = (feePerKb / 1000) * sizeInBytes; @@ -38,7 +57,8 @@ function calculateFeeFromRate(feePerKb, sizeInBytes) { } function calculateRateFromFee(totalFee, sizeInBytes) { - return (totalFee / sizeInBytes) * 1000; + const fee = (totalFee / sizeInBytes) * 1000; + return fee.toFixed(0) } export const FeeManager = ({ selectedCoin, setFee, fee }) => { @@ -49,13 +69,13 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => { }); const { resource } = usePublish(3, "JSON", feeLocation); const [selectedFeePublisher, setSelectedFeePublisher] = useAtom(selectedFeePublisherAtom) + const [isEnabledCustomLockingFee, setIsEnabledCustomLockingFee] = useAtom(isEnabledCustomLockingFeeAtom) const [editFee, setEditFee] = useState(""); const [openModal, setOpenModal] = useState(false); const [recommendedFee, setRecommendedFee] = useState("m"); const [openAlert, setOpenAlert] = useState(false); const [info, setInfo] = useState(null); - const [feeTimestamp, setFeeTimestamp] = useState(null) const { getCoinLabel } = useContext(gameContext); const handleCloseAlert = ( event?: React.SyntheticEvent | Event, @@ -70,10 +90,15 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => { }; const coin = useMemo(() => { const coinLabel = getCoinLabel(selectedCoin) - if(typeof coinLabel !== 'string') return + if(typeof coinLabel !== 'string') return null return coinLabel?.toLowerCase(); }, [selectedCoin, getCoinLabel]); + + const feeTimestamp = useMemo(()=> { + if(!resource?.qortalMetadata?.identifier?.includes(`${coin.toUpperCase()}`)) return + return resource?.data?.time || null + }, [resource, coin]) const establishUpdateFeeForm = useCallback(async (coin) => { setFee(""); // if the coin or type is not set, then abort @@ -107,24 +132,25 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => { }, [coin, establishUpdateFeeForm]); const recommendedFeeData = useMemo(() => { + if(!resource?.qortalMetadata?.identifier?.includes(`${coin.toUpperCase()}`)) return if (!resource?.data) return null; - + const isValid = isValidFeeEstimate(resource.data) + if(!isValid) return null return resource.data; - }, [resource?.data]); + }, [resource, coin]); const recommendedFeeDisplay = useMemo(() => { - if (!selectedCoin || !recommendedFeeData) return null; - const coinLabel = getCoinLabel(selectedCoin) - if(typeof coinLabel !== 'string') return - const coin = coinLabel?.toUpperCase(); - if(!recommendedFeeData[coin]) return null - return recommendedFeeData[coin][recommendedFee]; - }, [recommendedFeeData, recommendedFee, selectedCoin]); + if (!recommendedFeeData) return null; + + if(!recommendedFeeData) return null + return recommendedFeeData[recommendedFee] || null; + }, [recommendedFeeData, recommendedFee]); const hideRecommendations = useMemo(()=> { - if(selectedCoin === 'LITECOIN' || selectedCoin === 'BITCOIN' || selectedCoin === 'DOGECOIN') return false + if(recommendedFeeData) return false return true - }, [selectedCoin]) + }, [recommendedFeeData]) + useEffect(()=> { if(hideRecommendations){ @@ -134,6 +160,7 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => { const updateFee = async () => { const typeRequest = "feerequired"; + const typeRequestLocking = "feekb"; try { let feeToSave = editFee @@ -150,6 +177,19 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => { 1800000 ); + if(!isEnabledCustomLockingFee){ + await qortalRequestWithTimeout( + { + action: "UPDATE_FOREIGN_FEE", + coin: coin, + type: typeRequestLocking, + value: calculateRateFromFee(feeToSave, 300), + }, + 1800000 + ); + + } + if (response && !isNaN(+response)) { setFee(response); setOpenAlert(true); @@ -181,21 +221,21 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => { const getLatestFees = useCallback(async () => { try { + const coinLabel = getCoinLabel(selectedCoin) + if(typeof coinLabel !== 'string') return + const coin = coinLabel?.toUpperCase(); + const identifier = `coinInfo-${coin}` const res = await fetch( - `/arbitrary/resources/searchsimple?service=JSON&identifier=foreign-fee&name=${selectedFeePublisher}&prefix=true&limit=1&reverse=true` + `/arbitrary/resources/searchsimple?service=JSON&identifier=${identifier}&name=${selectedFeePublisher}&prefix=true&limit=1&reverse=true` ); const data = await res.json(); if (data && data?.length > 0) { setFeeLocation(data[0]); - const id = data[0].identifier; -const parts = id.split("-"); -const timestampSec = parseInt(parts[2], 10); - setFeeTimestamp(timestampSec) } } catch (error) { console.error(error) } - }, [selectedFeePublisher]); + }, [selectedFeePublisher, selectedCoin]); useEffect(() => { getLatestFees(); @@ -286,9 +326,9 @@ const timestampSec = parseInt(parts[2], 10); > {!hideRecommendations && ( <> - Low - Medium - High + Low + Medium + High )} diff --git a/src/components/sell/Settings.tsx b/src/components/sell/Settings.tsx index 0b414c0..c68a55b 100644 --- a/src/components/sell/Settings.tsx +++ b/src/components/sell/Settings.tsx @@ -11,6 +11,8 @@ import { Box, Button, ButtonBase, + Checkbox, + FormControlLabel, IconButton, MenuItem, Select, @@ -32,21 +34,25 @@ import { } from "../header/Header-styles"; import { CustomInput, CustomLabel } from "./CreateSell"; import { Spacer } from "../common/Spacer"; -import { usePublish, Service, QortalGetMetadata } from "qapp-core"; +import { usePublish, Service, QortalGetMetadata, useGlobal } from "qapp-core"; import { SetLeftFeature } from "ag-grid-community"; import { formatTimestampForum } from "../../utils/formatTime"; import { SelectRow } from "../header/Header"; import { useAtom } from "jotai/react"; -import { selectedFeePublisherAtom } from "../../global/state"; +import { isEnabledCustomLockingFeeAtom, selectedFeePublisherAtom } from "../../global/state"; export const Settings = () => { + const saveDataLocal = useGlobal().persistentOperations.saveData + const getDataLocal = useGlobal().persistentOperations.getData const [openModal, setOpenModal] = useState(false); const [lockingFee, setLockingFee] = useState(""); + const [editLockingFee, setEditLockingFee] = useState(""); const [openAlert, setOpenAlert] = useState(false); const [info, setInfo] = useState(null); const [selectedCoin, setSelectedCoin] = useState("LTC"); const [selectedFeePublisher, setSelectedFeePublisher] = useAtom(selectedFeePublisherAtom) + const [isEnabledCustomLockingFee, setIsEnabledCustomLockingFee] = useAtom(isEnabledCustomLockingFeeAtom) const handleCloseAlert = ( event?: React.SyntheticEvent | Event, reason?: SnackbarCloseReason @@ -94,6 +100,11 @@ export const Settings = () => { } }; + const handleChange = (event: React.ChangeEvent) => { + setIsEnabledCustomLockingFee(event.target.checked); + saveDataLocal('isEnabledCustomLockingFee', event.target.checked) + }; + const establishUpdateFeeForm = useCallback(async (coin) => { setLockingFee(""); setEditLockingFee(""); @@ -125,8 +136,27 @@ export const Settings = () => { }, []); useEffect(() => { + if(!openModal) return establishUpdateFeeForm(selectedCoin); - }, [selectedCoin, establishUpdateFeeForm]); + }, [selectedCoin, establishUpdateFeeForm, openModal]); + + useEffect(()=> { + const getSavedSelectedPublisher = async ()=> { + try { + const res = await getDataLocal('selectedFeePublisher') + if(res){ + setSelectedFeePublisher(res) + } + const res2 = await getDataLocal('isEnabledCustomLockingFee') + if(res2){ + setIsEnabledCustomLockingFee(res) + } + } catch (error) { + console.error(error) + } + } + getSavedSelectedPublisher() + }, []) return ( <> @@ -163,48 +193,53 @@ export const Settings = () => { padding: '5px' }}> Locking fees - - - - - Locking fee for {selectedCoin} (sats) - - - setEditLockingFee(e.target.value)} - autoComplete="off" - /> - - + } label="Enable custom locking fee" /> + + {isEnabledCustomLockingFee && ( + + + + + Locking fee for {selectedCoin} (sats per kb) + + + setEditLockingFee(e.target.value)} + autoComplete="off" + /> + + + )} + { size="small" value={selectedFeePublisher} onChange={(e) => { - setSelectedFeePublisher(e.target.value); + if(e.target.value){ + setSelectedFeePublisher(e.target.value); + saveDataLocal('selectedFeePublisher', e.target.value) + } + }} > diff --git a/src/global.d.ts b/src/global.d.ts index cd9543e..c34b1d8 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -44,7 +44,7 @@ interface QortalRequestOptions { foreignAmount?: number; atAddress?: string; type?: string - value?: string + value?: string | number } declare function qortalRequest(options: QortalRequestOptions): Promise; diff --git a/src/global/state.ts b/src/global/state.ts index d80242f..74ee66c 100644 --- a/src/global/state.ts +++ b/src/global/state.ts @@ -2,4 +2,6 @@ import { atomWithReset } from 'jotai/utils'; -export const selectedFeePublisherAtom = atomWithReset('Foreign-Fee-Publisher'); +export const selectedFeePublisherAtom = atomWithReset('Ice.JSON'); + +export const isEnabledCustomLockingFeeAtom = atomWithReset(false);