mirror of
https://github.com/Qortal/q-trade.git
synced 2025-06-18 12:11:21 +00:00
added lock fee functionality
This commit is contained in:
parent
e75fadf1bd
commit
bbfd29e8fb
15
package-lock.json
generated
15
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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) => {
|
||||
|
@ -549,7 +549,7 @@ export const Header = ({
|
||||
setSenderAddress("");
|
||||
}}
|
||||
backdrop
|
||||
open={openCoinActionModal}
|
||||
open={!!openCoinActionModal}
|
||||
>
|
||||
<CoinActionContainer>
|
||||
{openCoinActionModal.type === "send" ? (
|
||||
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
@ -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,6 +193,9 @@ export const Settings = () => {
|
||||
padding: '5px'
|
||||
}}>
|
||||
<Typography>Locking fees</Typography>
|
||||
<FormControlLabel control={<Checkbox checked={isEnabledCustomLockingFee} onChange={handleChange} />} label="Enable custom locking fee" />
|
||||
|
||||
{isEnabledCustomLockingFee && (
|
||||
<CoinSelectRow sx={{
|
||||
gap: '20px'
|
||||
}}>
|
||||
@ -193,7 +226,7 @@ export const Settings = () => {
|
||||
</Select>
|
||||
<Box>
|
||||
<CustomLabel htmlFor="standard-adornment-name">
|
||||
Locking fee for {selectedCoin} (sats)
|
||||
Locking fee for {selectedCoin} (sats per kb)
|
||||
</CustomLabel>
|
||||
<Spacer height="5px" />
|
||||
<CustomInput
|
||||
@ -205,6 +238,8 @@ export const Settings = () => {
|
||||
/>
|
||||
</Box>
|
||||
</CoinSelectRow>
|
||||
)}
|
||||
|
||||
|
||||
<ButtonBase
|
||||
onClick={updateLockingFee}
|
||||
@ -242,7 +277,11 @@ export const Settings = () => {
|
||||
size="small"
|
||||
value={selectedFeePublisher}
|
||||
onChange={(e) => {
|
||||
if(e.target.value){
|
||||
setSelectedFeePublisher(e.target.value);
|
||||
saveDataLocal('selectedFeePublisher', e.target.value)
|
||||
}
|
||||
|
||||
}}
|
||||
>
|
||||
<MenuItem value={"Foreign-Fee-Publisher"}>
|
||||
|
2
src/global.d.ts
vendored
2
src/global.d.ts
vendored
@ -44,7 +44,7 @@ interface QortalRequestOptions {
|
||||
foreignAmount?: number;
|
||||
atAddress?: string;
|
||||
type?: string
|
||||
value?: string
|
||||
value?: string | number
|
||||
}
|
||||
|
||||
declare function qortalRequest(options: QortalRequestOptions): Promise<any>;
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user