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",
"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",

View File

@ -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",

View File

@ -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<any> = ({
foreignCoinBalance,
fee,
@ -175,9 +179,17 @@ export const TradeOffers: React.FC<any> = ({
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) => {

View File

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

View File

@ -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<any>(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 && (
<>
<ToggleButton value="l">Low</ToggleButton>
<ToggleButton value="m">Medium</ToggleButton>
<ToggleButton value="h">High</ToggleButton>
<ToggleButton value="low_fee_per_kb">Low</ToggleButton>
<ToggleButton value="medium_fee_per_kb">Medium</ToggleButton>
<ToggleButton value="high_fee_per_kb">High</ToggleButton>
</>
)}

View File

@ -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<any>(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<HTMLInputElement>) => {
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'
}}>
<Typography>Locking fees</Typography>
<CoinSelectRow sx={{
gap: '20px'
}}>
<Select
size="small"
value={selectedCoin}
onChange={(e) => {
setLockingFee("");
setEditLockingFee("");
setSelectedCoin(e.target.value);
}}
>
<MenuItem value={"LTC"}>
<SelectRow coin="LTC" />
</MenuItem>
<MenuItem value={"DOGE"}>
<SelectRow coin="DOGE" />
</MenuItem>
<MenuItem value={"BTC"}>
<SelectRow coin="BTC" />
</MenuItem>
<MenuItem value={"DGB"}>
<SelectRow coin="DGB" />
</MenuItem>
<MenuItem value={"RVN"}>
<SelectRow coin="RVN" />
</MenuItem>
</Select>
<Box>
<CustomLabel htmlFor="standard-adornment-name">
Locking fee for {selectedCoin} (sats)
</CustomLabel>
<Spacer height="5px" />
<CustomInput
id="standard-adornment-name"
type="number"
value={editLockingFee}
onChange={(e) => setEditLockingFee(e.target.value)}
autoComplete="off"
/>
</Box>
</CoinSelectRow>
<FormControlLabel control={<Checkbox checked={isEnabledCustomLockingFee} onChange={handleChange} />} label="Enable custom locking fee" />
{isEnabledCustomLockingFee && (
<CoinSelectRow sx={{
gap: '20px'
}}>
<Select
size="small"
value={selectedCoin}
onChange={(e) => {
setLockingFee("");
setEditLockingFee("");
setSelectedCoin(e.target.value);
}}
>
<MenuItem value={"LTC"}>
<SelectRow coin="LTC" />
</MenuItem>
<MenuItem value={"DOGE"}>
<SelectRow coin="DOGE" />
</MenuItem>
<MenuItem value={"BTC"}>
<SelectRow coin="BTC" />
</MenuItem>
<MenuItem value={"DGB"}>
<SelectRow coin="DGB" />
</MenuItem>
<MenuItem value={"RVN"}>
<SelectRow coin="RVN" />
</MenuItem>
</Select>
<Box>
<CustomLabel htmlFor="standard-adornment-name">
Locking fee for {selectedCoin} (sats per kb)
</CustomLabel>
<Spacer height="5px" />
<CustomInput
id="standard-adornment-name"
type="number"
value={editLockingFee}
onChange={(e) => setEditLockingFee(e.target.value)}
autoComplete="off"
/>
</Box>
</CoinSelectRow>
)}
<ButtonBase
onClick={updateLockingFee}
@ -242,7 +277,11 @@ export const Settings = () => {
size="small"
value={selectedFeePublisher}
onChange={(e) => {
setSelectedFeePublisher(e.target.value);
if(e.target.value){
setSelectedFeePublisher(e.target.value);
saveDataLocal('selectedFeePublisher', e.target.value)
}
}}
>
<MenuItem value={"Foreign-Fee-Publisher"}>

2
src/global.d.ts vendored
View File

@ -44,7 +44,7 @@ interface QortalRequestOptions {
foreignAmount?: number;
atAddress?: string;
type?: string
value?: string
value?: string | number
}
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);