added lock fee functionality

This commit is contained in:
PhilReact 2025-05-03 23:21:10 +03:00
parent e75fadf1bd
commit bbfd29e8fb
8 changed files with 179 additions and 79 deletions

15
package-lock.json generated
View File

@ -19,7 +19,7 @@
"jotai": "^2.12.3", "jotai": "^2.12.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.30.1", "moment": "^2.30.1",
"qapp-core": "^1.0.22", "qapp-core": "^1.0.24",
"react": "^19.1.0", "react": "^19.1.0",
"react-countdown-circle-timer": "^3.2.1", "react-countdown-circle-timer": "^3.2.1",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
@ -4298,6 +4298,12 @@
"node": ">=0.4.0" "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": { "node_modules/dir-glob": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -6575,9 +6581,9 @@
} }
}, },
"node_modules/qapp-core": { "node_modules/qapp-core": {
"version": "1.0.22", "version": "1.0.24",
"resolved": "https://registry.npmjs.org/qapp-core/-/qapp-core-1.0.22.tgz", "resolved": "https://registry.npmjs.org/qapp-core/-/qapp-core-1.0.24.tgz",
"integrity": "sha512-3q8Ebr9lpyDW7lTo91Rlak2EGIAXrU9DYhUBuLsYZMuRyI/bKoc0E4hHrrAo946eJ/5AxqNGmZDkYSJq5tOTZg==", "integrity": "sha512-KnrwiysaHlTR1rUnPGwN79gQgcjvtLr4xRH3EGGQcbXKCcbrklV7lv4KLUfFR4UwhskbgOXpJY5PECWdrlGXSw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/react-virtual": "^3.13.2", "@tanstack/react-virtual": "^3.13.2",
@ -6586,6 +6592,7 @@
"compressorjs": "^1.2.1", "compressorjs": "^1.2.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dexie": "^4.0.11",
"dompurify": "^3.2.4", "dompurify": "^3.2.4",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-hot-toast": "^2.5.2", "react-hot-toast": "^2.5.2",

View File

@ -21,7 +21,7 @@
"jotai": "^2.12.3", "jotai": "^2.12.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.30.1", "moment": "^2.30.1",
"qapp-core": "^1.0.22", "qapp-core": "^1.0.24",
"react": "^19.1.0", "react": "^19.1.0",
"react-countdown-circle-timer": "^3.2.1", "react-countdown-circle-timer": "^3.2.1",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",

View File

@ -60,6 +60,7 @@ export const baseLocalHost = window.location.host;
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import moment from "moment"; import moment from "moment";
import { RequestQueueWithPromise } from "qapp-core";
const copyToClipboard = (text: string) => { const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text); navigator.clipboard.writeText(text);
@ -84,6 +85,9 @@ export const autoSizeStrategy: SizeColumnsToContentStrategy = {
type: "fitCellContents", type: "fitCellContents",
}; };
const requestQueueGetNames = new RequestQueueWithPromise(4);
export const TradeOffers: React.FC<any> = ({ export const TradeOffers: React.FC<any> = ({
foreignCoinBalance, foreignCoinBalance,
fee, fee,
@ -175,9 +179,17 @@ export const TradeOffers: React.FC<any> = ({
setIsRemoveOrders(val); setIsRemoveOrders(val);
}; };
const isFetchingName = useRef({})
const getName = async (address) => { const getName = async (address) => {
try { 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(); const nameData = await response.json();
if (nameData?.length > 0) { if (nameData?.length > 0) {
setQortalNames((prev) => { setQortalNames((prev) => {

View File

@ -549,7 +549,7 @@ export const Header = ({
setSenderAddress(""); setSenderAddress("");
}} }}
backdrop backdrop
open={openCoinActionModal} open={!!openCoinActionModal}
> >
<CoinActionContainer> <CoinActionContainer>
{openCoinActionModal.type === "send" ? ( {openCoinActionModal.type === "send" ? (

View File

@ -30,7 +30,26 @@ import { usePublish, Service, QortalGetMetadata } from "qapp-core";
import { SetLeftFeature } from "ag-grid-community"; import { SetLeftFeature } from "ag-grid-community";
import { formatTimestampForum } from "../../utils/formatTime"; import { formatTimestampForum } from "../../utils/formatTime";
import { useAtom } from "jotai/react"; 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) { function calculateFeeFromRate(feePerKb, sizeInBytes) {
const fee = (feePerKb / 1000) * sizeInBytes; const fee = (feePerKb / 1000) * sizeInBytes;
@ -38,7 +57,8 @@ function calculateFeeFromRate(feePerKb, sizeInBytes) {
} }
function calculateRateFromFee(totalFee, sizeInBytes) { function calculateRateFromFee(totalFee, sizeInBytes) {
return (totalFee / sizeInBytes) * 1000; const fee = (totalFee / sizeInBytes) * 1000;
return fee.toFixed(0)
} }
export const FeeManager = ({ selectedCoin, setFee, fee }) => { export const FeeManager = ({ selectedCoin, setFee, fee }) => {
@ -49,13 +69,13 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
}); });
const { resource } = usePublish(3, "JSON", feeLocation); const { resource } = usePublish(3, "JSON", feeLocation);
const [selectedFeePublisher, setSelectedFeePublisher] = useAtom(selectedFeePublisherAtom) const [selectedFeePublisher, setSelectedFeePublisher] = useAtom(selectedFeePublisherAtom)
const [isEnabledCustomLockingFee, setIsEnabledCustomLockingFee] = useAtom(isEnabledCustomLockingFeeAtom)
const [editFee, setEditFee] = useState(""); const [editFee, setEditFee] = useState("");
const [openModal, setOpenModal] = useState(false); const [openModal, setOpenModal] = useState(false);
const [recommendedFee, setRecommendedFee] = useState("m"); const [recommendedFee, setRecommendedFee] = useState("m");
const [openAlert, setOpenAlert] = useState(false); const [openAlert, setOpenAlert] = useState(false);
const [info, setInfo] = useState<any>(null); const [info, setInfo] = useState<any>(null);
const [feeTimestamp, setFeeTimestamp] = useState(null)
const { getCoinLabel } = useContext(gameContext); const { getCoinLabel } = useContext(gameContext);
const handleCloseAlert = ( const handleCloseAlert = (
event?: React.SyntheticEvent | Event, event?: React.SyntheticEvent | Event,
@ -70,10 +90,15 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
}; };
const coin = useMemo(() => { const coin = useMemo(() => {
const coinLabel = getCoinLabel(selectedCoin) const coinLabel = getCoinLabel(selectedCoin)
if(typeof coinLabel !== 'string') return if(typeof coinLabel !== 'string') return null
return coinLabel?.toLowerCase(); return coinLabel?.toLowerCase();
}, [selectedCoin, getCoinLabel]); }, [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) => { const establishUpdateFeeForm = useCallback(async (coin) => {
setFee(""); setFee("");
// if the coin or type is not set, then abort // if the coin or type is not set, then abort
@ -107,24 +132,25 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
}, [coin, establishUpdateFeeForm]); }, [coin, establishUpdateFeeForm]);
const recommendedFeeData = useMemo(() => { const recommendedFeeData = useMemo(() => {
if(!resource?.qortalMetadata?.identifier?.includes(`${coin.toUpperCase()}`)) return
if (!resource?.data) return null; if (!resource?.data) return null;
const isValid = isValidFeeEstimate(resource.data)
if(!isValid) return null
return resource.data; return resource.data;
}, [resource?.data]); }, [resource, coin]);
const recommendedFeeDisplay = useMemo(() => { const recommendedFeeDisplay = useMemo(() => {
if (!selectedCoin || !recommendedFeeData) return null; if (!recommendedFeeData) return null;
const coinLabel = getCoinLabel(selectedCoin)
if(typeof coinLabel !== 'string') return if(!recommendedFeeData) return null
const coin = coinLabel?.toUpperCase(); return recommendedFeeData[recommendedFee] || null;
if(!recommendedFeeData[coin]) return null }, [recommendedFeeData, recommendedFee]);
return recommendedFeeData[coin][recommendedFee];
}, [recommendedFeeData, recommendedFee, selectedCoin]);
const hideRecommendations = useMemo(()=> { const hideRecommendations = useMemo(()=> {
if(selectedCoin === 'LITECOIN' || selectedCoin === 'BITCOIN' || selectedCoin === 'DOGECOIN') return false if(recommendedFeeData) return false
return true return true
}, [selectedCoin]) }, [recommendedFeeData])
useEffect(()=> { useEffect(()=> {
if(hideRecommendations){ if(hideRecommendations){
@ -134,6 +160,7 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
const updateFee = async () => { const updateFee = async () => {
const typeRequest = "feerequired"; const typeRequest = "feerequired";
const typeRequestLocking = "feekb";
try { try {
let feeToSave = editFee let feeToSave = editFee
@ -150,6 +177,19 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
1800000 1800000
); );
if(!isEnabledCustomLockingFee){
await qortalRequestWithTimeout(
{
action: "UPDATE_FOREIGN_FEE",
coin: coin,
type: typeRequestLocking,
value: calculateRateFromFee(feeToSave, 300),
},
1800000
);
}
if (response && !isNaN(+response)) { if (response && !isNaN(+response)) {
setFee(response); setFee(response);
setOpenAlert(true); setOpenAlert(true);
@ -181,21 +221,21 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
const getLatestFees = useCallback(async () => { const getLatestFees = useCallback(async () => {
try { try {
const coinLabel = getCoinLabel(selectedCoin)
if(typeof coinLabel !== 'string') return
const coin = coinLabel?.toUpperCase();
const identifier = `coinInfo-${coin}`
const res = await fetch( 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(); const data = await res.json();
if (data && data?.length > 0) { if (data && data?.length > 0) {
setFeeLocation(data[0]); setFeeLocation(data[0]);
const id = data[0].identifier;
const parts = id.split("-");
const timestampSec = parseInt(parts[2], 10);
setFeeTimestamp(timestampSec)
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
}, [selectedFeePublisher]); }, [selectedFeePublisher, selectedCoin]);
useEffect(() => { useEffect(() => {
getLatestFees(); getLatestFees();
@ -286,9 +326,9 @@ const timestampSec = parseInt(parts[2], 10);
> >
{!hideRecommendations && ( {!hideRecommendations && (
<> <>
<ToggleButton value="l">Low</ToggleButton> <ToggleButton value="low_fee_per_kb">Low</ToggleButton>
<ToggleButton value="m">Medium</ToggleButton> <ToggleButton value="medium_fee_per_kb">Medium</ToggleButton>
<ToggleButton value="h">High</ToggleButton> <ToggleButton value="high_fee_per_kb">High</ToggleButton>
</> </>
)} )}

View File

@ -11,6 +11,8 @@ import {
Box, Box,
Button, Button,
ButtonBase, ButtonBase,
Checkbox,
FormControlLabel,
IconButton, IconButton,
MenuItem, MenuItem,
Select, Select,
@ -32,21 +34,25 @@ import {
} from "../header/Header-styles"; } from "../header/Header-styles";
import { CustomInput, CustomLabel } from "./CreateSell"; import { CustomInput, CustomLabel } from "./CreateSell";
import { Spacer } from "../common/Spacer"; 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 { SetLeftFeature } from "ag-grid-community";
import { formatTimestampForum } from "../../utils/formatTime"; import { formatTimestampForum } from "../../utils/formatTime";
import { SelectRow } from "../header/Header"; import { SelectRow } from "../header/Header";
import { useAtom } from "jotai/react"; import { useAtom } from "jotai/react";
import { selectedFeePublisherAtom } from "../../global/state"; import { isEnabledCustomLockingFeeAtom, selectedFeePublisherAtom } from "../../global/state";
export const Settings = () => { export const Settings = () => {
const saveDataLocal = useGlobal().persistentOperations.saveData
const getDataLocal = useGlobal().persistentOperations.getData
const [openModal, setOpenModal] = useState(false); const [openModal, setOpenModal] = useState(false);
const [lockingFee, setLockingFee] = useState(""); const [lockingFee, setLockingFee] = useState("");
const [editLockingFee, setEditLockingFee] = useState(""); const [editLockingFee, setEditLockingFee] = useState("");
const [openAlert, setOpenAlert] = useState(false); const [openAlert, setOpenAlert] = useState(false);
const [info, setInfo] = useState<any>(null); const [info, setInfo] = useState<any>(null);
const [selectedCoin, setSelectedCoin] = useState("LTC"); const [selectedCoin, setSelectedCoin] = useState("LTC");
const [selectedFeePublisher, setSelectedFeePublisher] = useAtom(selectedFeePublisherAtom) const [selectedFeePublisher, setSelectedFeePublisher] = useAtom(selectedFeePublisherAtom)
const [isEnabledCustomLockingFee, setIsEnabledCustomLockingFee] = useAtom(isEnabledCustomLockingFeeAtom)
const handleCloseAlert = ( const handleCloseAlert = (
event?: React.SyntheticEvent | Event, event?: React.SyntheticEvent | Event,
reason?: SnackbarCloseReason reason?: SnackbarCloseReason
@ -94,6 +100,11 @@ export const Settings = () => {
} }
}; };
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setIsEnabledCustomLockingFee(event.target.checked);
saveDataLocal('isEnabledCustomLockingFee', event.target.checked)
};
const establishUpdateFeeForm = useCallback(async (coin) => { const establishUpdateFeeForm = useCallback(async (coin) => {
setLockingFee(""); setLockingFee("");
setEditLockingFee(""); setEditLockingFee("");
@ -125,8 +136,27 @@ export const Settings = () => {
}, []); }, []);
useEffect(() => { useEffect(() => {
if(!openModal) return
establishUpdateFeeForm(selectedCoin); 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 ( return (
<> <>
@ -163,48 +193,53 @@ export const Settings = () => {
padding: '5px' padding: '5px'
}}> }}>
<Typography>Locking fees</Typography> <Typography>Locking fees</Typography>
<CoinSelectRow sx={{ <FormControlLabel control={<Checkbox checked={isEnabledCustomLockingFee} onChange={handleChange} />} label="Enable custom locking fee" />
gap: '20px'
}}> {isEnabledCustomLockingFee && (
<Select <CoinSelectRow sx={{
size="small" gap: '20px'
value={selectedCoin} }}>
onChange={(e) => { <Select
setLockingFee(""); size="small"
setEditLockingFee(""); value={selectedCoin}
setSelectedCoin(e.target.value); onChange={(e) => {
}} setLockingFee("");
> setEditLockingFee("");
<MenuItem value={"LTC"}> setSelectedCoin(e.target.value);
<SelectRow coin="LTC" /> }}
</MenuItem> >
<MenuItem value={"DOGE"}> <MenuItem value={"LTC"}>
<SelectRow coin="DOGE" /> <SelectRow coin="LTC" />
</MenuItem> </MenuItem>
<MenuItem value={"BTC"}> <MenuItem value={"DOGE"}>
<SelectRow coin="BTC" /> <SelectRow coin="DOGE" />
</MenuItem> </MenuItem>
<MenuItem value={"DGB"}> <MenuItem value={"BTC"}>
<SelectRow coin="DGB" /> <SelectRow coin="BTC" />
</MenuItem> </MenuItem>
<MenuItem value={"RVN"}> <MenuItem value={"DGB"}>
<SelectRow coin="RVN" /> <SelectRow coin="DGB" />
</MenuItem> </MenuItem>
</Select> <MenuItem value={"RVN"}>
<Box> <SelectRow coin="RVN" />
<CustomLabel htmlFor="standard-adornment-name"> </MenuItem>
Locking fee for {selectedCoin} (sats) </Select>
</CustomLabel> <Box>
<Spacer height="5px" /> <CustomLabel htmlFor="standard-adornment-name">
<CustomInput Locking fee for {selectedCoin} (sats per kb)
id="standard-adornment-name" </CustomLabel>
type="number" <Spacer height="5px" />
value={editLockingFee} <CustomInput
onChange={(e) => setEditLockingFee(e.target.value)} id="standard-adornment-name"
autoComplete="off" type="number"
/> value={editLockingFee}
</Box> onChange={(e) => setEditLockingFee(e.target.value)}
</CoinSelectRow> autoComplete="off"
/>
</Box>
</CoinSelectRow>
)}
<ButtonBase <ButtonBase
onClick={updateLockingFee} onClick={updateLockingFee}
@ -242,7 +277,11 @@ export const Settings = () => {
size="small" size="small"
value={selectedFeePublisher} value={selectedFeePublisher}
onChange={(e) => { onChange={(e) => {
setSelectedFeePublisher(e.target.value); if(e.target.value){
setSelectedFeePublisher(e.target.value);
saveDataLocal('selectedFeePublisher', e.target.value)
}
}} }}
> >
<MenuItem value={"Foreign-Fee-Publisher"}> <MenuItem value={"Foreign-Fee-Publisher"}>

2
src/global.d.ts vendored
View File

@ -44,7 +44,7 @@ interface QortalRequestOptions {
foreignAmount?: number; foreignAmount?: number;
atAddress?: string; atAddress?: string;
type?: string type?: string
value?: string value?: string | number
} }
declare function qortalRequest(options: QortalRequestOptions): Promise<any>; declare function qortalRequest(options: QortalRequestOptions): Promise<any>;

View File

@ -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);