2025-04-12 16:40:32 +02:00

855 lines
24 KiB
TypeScript

import {
Alert,
Box,
Button,
Card,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Divider,
IconButton,
InputBase,
InputLabel,
Snackbar,
Typography,
} from '@mui/material';
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import CloseIcon from '@mui/icons-material/Close';
import { MyContext, getBaseApiReact } from '../../App';
import {
executeEvent,
subscribeToEvent,
unsubscribeFromEvent,
} from '../../utils/events';
import { getFee, getNameOrAddress } from '../../background';
import CopyToClipboard from 'react-copy-to-clipboard';
import { AddressBox } from '../../styles/App-styles';
import { Spacer } from '../../common/Spacer';
import Copy from '../../assets/svgs/Copy.svg';
import { Loader } from '../Loader';
import { FidgetSpinner } from 'react-loader-spinner';
import { useModal } from '../../common/useModal';
export const Minting = ({
setIsOpenMinting,
myAddress,
groups,
show,
setTxList,
txList,
}) => {
const [mintingAccounts, setMintingAccounts] = useState([]);
const [accountInfo, setAccountInfo] = useState(null);
const [rewardSharePublicKey, setRewardSharePublicKey] = useState('');
const [mintingKey, setMintingKey] = useState('');
const [rewardsharekey, setRewardsharekey] = useState('');
const [rewardShares, setRewardShares] = useState([]);
const [nodeInfos, setNodeInfos] = useState({});
const [openSnack, setOpenSnack] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const { show: showKey, message } = useModal();
const { isShow: isShowNext, onOk, show: showNext } = useModal();
const [info, setInfo] = useState(null);
const [names, setNames] = useState({});
const [accountInfos, setAccountInfos] = useState({});
const [showWaitDialog, setShowWaitDialog] = useState(false);
const isPartOfMintingGroup = useMemo(() => {
if (groups?.length === 0) return false;
return !!groups?.find((item) => item?.groupId?.toString() === '694');
}, [groups]);
const getMintingAccounts = useCallback(async () => {
try {
const url = `${getBaseApiReact()}/admin/mintingaccounts`;
const response = await fetch(url);
if (!response.ok) {
throw new Error('network error');
}
const data = await response.json();
setMintingAccounts(data);
} catch (error) {}
}, []);
const accountIsMinting = useMemo(() => {
return !!mintingAccounts?.find(
(item) => item?.recipientAccount === myAddress
);
}, [mintingAccounts, myAddress]);
const getName = async (address) => {
try {
const response = await fetch(
`${getBaseApiReact()}/names/address/${address}`
);
const nameData = await response.json();
if (nameData?.length > 0) {
setNames((prev) => {
return {
...prev,
[address]: nameData[0].name,
};
});
} else {
setNames((prev) => {
return {
...prev,
[address]: null,
};
});
}
} catch (error) {
// error
}
};
const getAccountInfo = async (address: string, others?: boolean) => {
try {
if (!others) {
setIsLoading(true);
}
const url = `${getBaseApiReact()}/addresses/${address}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error('network error');
}
const data = await response.json();
if (others) {
setAccountInfos((prev) => {
return {
...prev,
[address]: data,
};
});
} else {
setAccountInfo(data);
}
} catch (error) {
} finally {
if (!others) {
setIsLoading(false);
}
}
};
const refreshRewardShare = () => {
if (!myAddress) return;
getRewardShares(myAddress);
};
useEffect(() => {
subscribeToEvent('refresh-rewardshare-list', refreshRewardShare);
return () => {
unsubscribeFromEvent('refresh-rewardshare-list', refreshRewardShare);
};
}, [myAddress]);
const handleNames = (address) => {
if (!address) return undefined;
if (names[address]) return names[address];
if (names[address] === null) return address;
getName(address);
return address;
};
const handleAccountInfos = (address, field) => {
if (!address) return undefined;
if (accountInfos[address]) return accountInfos[address]?.[field];
if (accountInfos[address] === null) return undefined;
getAccountInfo(address, true);
return undefined;
};
const calculateBlocksRemainingToLevel1 = (address) => {
if (!address) return undefined;
if (!accountInfos[address]) return undefined;
return 7200 - accountInfos[address]?.blocksMinted || 0;
};
const getNodeInfos = async () => {
try {
const url = `${getBaseApiReact()}/admin/status`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await response.json();
setNodeInfos(data);
} catch (error) {
console.error('Request failed', error);
}
};
const getRewardShares = useCallback(async (address) => {
try {
const url = `${getBaseApiReact()}/addresses/rewardshares?involving=${address}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error('network error');
}
const data = await response.json();
setRewardShares(data);
return data;
} catch (error) {}
}, []);
const addMintingAccount = useCallback(async (val) => {
try {
setIsLoading(true);
return await new Promise((res, rej) => {
window
.sendMessage(
'ADMIN_ACTION',
{
type: 'addmintingaccount',
value: val,
},
180000,
true
)
.then((response) => {
if (!response?.error) {
res(response);
setMintingKey('');
setTimeout(() => {
getMintingAccounts();
}, 300);
return;
}
rej({ message: response.error });
})
.catch((error) => {
rej({ message: error.message || 'An error occurred' });
});
});
} catch (error) {
setInfo({
type: 'error',
message: error?.message || 'Unable to add minting account',
});
setOpenSnack(true);
} finally {
setIsLoading(false);
}
}, []);
const removeMintingAccount = useCallback(async (val, acct) => {
try {
setIsLoading(true);
return await new Promise((res, rej) => {
window
.sendMessage(
'ADMIN_ACTION',
{
type: 'removemintingaccount',
value: val,
},
180000,
true
)
.then((response) => {
if (!response?.error) {
res(response);
setTimeout(() => {
getMintingAccounts();
}, 300);
return;
}
rej({ message: response.error });
})
.catch((error) => {
rej({ message: error.message || 'An error occurred' });
});
});
} catch (error) {
setInfo({
type: 'error',
message: error?.message || 'Unable to remove minting account',
});
setOpenSnack(true);
} finally {
setIsLoading(false);
}
}, []);
const createRewardShare = useCallback(async (publicKey, recipient) => {
const fee = await getFee('REWARD_SHARE');
await show({
message: 'Would you like to perform an REWARD_SHARE transaction?',
publishFee: fee.fee + ' QORT',
});
return await new Promise((res, rej) => {
window
.sendMessage('createRewardShare', {
recipientPublicKey: publicKey,
})
.then((response) => {
if (!response?.error) {
setTxList((prev) => [
{
recipient,
...response,
type: 'add-rewardShare',
label: `Add rewardshare: awaiting confirmation`,
labelDone: `Add rewardshare: success!`,
done: false,
},
...prev,
]);
res(response);
return;
}
rej({ message: response.error });
})
.catch((error) => {
rej({ message: error.message || 'An error occurred' });
});
});
}, []);
const getRewardSharePrivateKey = useCallback(async (publicKey) => {
return await new Promise((res, rej) => {
window
.sendMessage('getRewardSharePrivateKey', {
recipientPublicKey: publicKey,
})
.then((response) => {
if (!response?.error) {
res(response);
return;
}
rej({ message: response.error });
})
.catch((error) => {
rej({ message: error.message || 'An error occurred' });
});
});
}, []);
const waitUntilRewardShareIsConfirmed = async (timeoutMs = 600000) => {
const pollingInterval = 30000;
const startTime = Date.now();
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
while (Date.now() - startTime < timeoutMs) {
const rewardShares = await getRewardShares(myAddress);
const findRewardShare = rewardShares?.find(
(item) =>
item?.recipient === myAddress && item?.mintingAccount === myAddress
);
if (findRewardShare) {
return true; // Exit early if found
}
await sleep(pollingInterval); // Wait before the next poll
}
throw new Error('Timeout waiting for reward share confirmation');
};
const startMinting = async () => {
try {
setIsLoading(true);
const findRewardShare = rewardShares?.find(
(item) =>
item?.recipient === myAddress && item?.mintingAccount === myAddress
);
if (findRewardShare) {
const privateRewardShare = await getRewardSharePrivateKey(
accountInfo?.publicKey
);
addMintingAccount(privateRewardShare);
} else {
await createRewardShare(accountInfo?.publicKey, myAddress);
setShowWaitDialog(true);
await waitUntilRewardShareIsConfirmed();
await showNext({
message: '',
});
const privateRewardShare = await getRewardSharePrivateKey(
accountInfo?.publicKey
);
setShowWaitDialog(false);
addMintingAccount(privateRewardShare);
}
} catch (error) {
setShowWaitDialog(false);
setInfo({
type: 'error',
message: error?.message || 'Unable to start minting',
});
setOpenSnack(true);
} finally {
setIsLoading(false);
}
};
const getPublicKeyFromAddress = async (address) => {
const url = `${getBaseApiReact()}/addresses/publickey/${address}`;
const response = await fetch(url);
const data = await response.text();
return data;
};
const checkIfMinterGroup = async (address) => {
const url = `${getBaseApiReact()}/groups/member/${address}`;
const response = await fetch(url);
const data = await response.json();
return !!data?.find((grp) => grp?.groupId?.toString() === '694');
};
const removeRewardShare = useCallback(async (rewardShare) => {
return await new Promise((res, rej) => {
window
.sendMessage('removeRewardShare', {
rewardShareKeyPairPublicKey: rewardShare.rewardSharePublicKey,
recipient: rewardShare.recipient,
percentageShare: -1,
})
.then((response) => {
if (!response?.error) {
res(response);
setTxList((prev) => [
{
...rewardShare,
...response,
type: 'remove-rewardShare',
label: `Remove rewardshare: awaiting confirmation`,
labelDone: `Remove rewardshare: success!`,
done: false,
},
...prev,
]);
return;
}
rej({ message: response.error });
})
.catch((error) => {
rej({ message: error.message || 'An error occurred' });
});
});
}, []);
const handleRemoveRewardShare = async (rewardShare) => {
try {
setIsLoading(true);
const privateRewardShare = await removeRewardShare(rewardShare);
} catch (error) {
setInfo({
type: 'error',
message: error?.message || 'Unable to remove reward share',
});
setOpenSnack(true);
} finally {
setIsLoading(false);
}
};
const createRewardShareForPotentialMinter = async (receiver) => {
try {
setIsLoading(true);
const confirmReceiver = await getNameOrAddress(receiver);
if (confirmReceiver.error)
throw new Error('Invalid receiver address or name');
const isInMinterGroup = await checkIfMinterGroup(confirmReceiver);
if (!isInMinterGroup) throw new Error('Account not in Minter Group');
const publicKey = await getPublicKeyFromAddress(confirmReceiver);
const findRewardShare = rewardShares?.find(
(item) =>
item?.recipient === confirmReceiver &&
item?.mintingAccount === myAddress
);
if (findRewardShare) {
const privateRewardShare = await getRewardSharePrivateKey(publicKey);
setRewardsharekey(privateRewardShare);
} else {
await createRewardShare(publicKey, confirmReceiver);
const privateRewardShare = await getRewardSharePrivateKey(publicKey);
setRewardsharekey(privateRewardShare);
}
} catch (error) {
setInfo({
type: 'error',
message: error?.message || 'Unable to create reward share',
});
setOpenSnack(true);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
getNodeInfos();
getMintingAccounts();
}, []);
useEffect(() => {
if (!myAddress) return;
getRewardShares(myAddress);
getAccountInfo(myAddress);
}, [myAddress]);
const _blocksNeed = () => {
if (accountInfo?.level === 0) {
return 7200;
} else if (accountInfo?.level === 1) {
return 72000;
} else if (accountInfo?.level === 2) {
return 201600;
} else if (accountInfo?.level === 3) {
return 374400;
} else if (accountInfo?.level === 4) {
return 618400;
} else if (accountInfo?.level === 5) {
return 964000;
} else if (accountInfo?.level === 6) {
return 1482400;
} else if (accountInfo?.level === 7) {
return 2173600;
} else if (accountInfo?.level === 8) {
return 3037600;
} else if (accountInfo?.level === 9) {
return 4074400;
}
};
const handleClose = () => {
setOpenSnack(false);
setTimeout(() => {
setInfo(null);
}, 250);
};
const _levelUpBlocks = () => {
if (
accountInfo?.blocksMinted === undefined ||
nodeInfos?.height === undefined
)
return null;
let countBlocks =
_blocksNeed() -
(accountInfo?.blocksMinted + accountInfo?.blocksMintedAdjustment);
let countBlocksString = countBlocks.toString();
return '' + countBlocksString;
};
return (
<Dialog
open={true}
maxWidth="lg"
fullWidth
fullScreen
sx={{
'& .MuiDialog-paper': {
margin: 0,
maxWidth: '100%',
width: '100%',
height: '100vh',
overflow: 'hidden', // Prevent scrollbars
},
}}
>
<DialogTitle id="alert-dialog-title">{'Manage your minting'}</DialogTitle>
<IconButton
sx={{
position: 'absolute',
right: 8,
top: 8,
}}
color="inherit"
onClick={() => setIsOpenMinting(false)}
aria-label="close"
>
<CloseIcon />
</IconButton>
<DialogContent
sx={{
position: 'relative',
}}
>
{isLoading && (
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<FidgetSpinner
visible={true}
height="80"
width="80"
ariaLabel="fidget-spinner-loading"
wrapperStyle={{}}
wrapperClass="fidget-spinner-wrapper"
/>
</Box>
)}
<Card
sx={{
backgroundColor: 'var(--bg-2)',
padding: '10px',
}}
>
<Typography>Account: {handleNames(accountInfo?.address)}</Typography>
<Typography>Level: {accountInfo?.level}</Typography>
<Typography>
blocks remaining until next level: {_levelUpBlocks()}
</Typography>
<Typography>
This node is minting: {nodeInfos?.isMintingPossible?.toString()}
</Typography>
</Card>
<Spacer height="10px" />
{isPartOfMintingGroup && !accountIsMinting && (
<Box
sx={{
display: 'flex',
gap: '5px',
flexDirection: 'column',
width: '100%',
alignItems: 'center',
}}
>
<Button
size="small"
onClick={() => {
startMinting();
}}
disabled={mintingAccounts?.length > 1}
sx={{
backgroundColor: 'var(--green)',
color: 'black',
fontWeight: 'bold',
opacity: 0.7,
maxWidth: '90%',
width: '200px',
'&:hover': {
backgroundColor: 'var(--green)',
color: 'black',
opacity: 1,
},
}}
variant="contained"
>
Start minting
</Button>
{mintingAccounts?.length > 1 && (
<Typography>
Only 2 minting keys are allowed per node. Please remove one if
you would like to mint with this account.
</Typography>
)}
</Box>
)}
<Spacer height="10px" />
{mintingAccounts?.length > 0 && (
<Typography>Node's minting accounts</Typography>
)}
<Card
sx={{
backgroundColor: 'var(--bg-2)',
padding: '10px',
}}
>
{accountIsMinting && (
<Box
sx={{
display: 'flex',
gap: '5px',
flexDirection: 'column',
}}
>
<Typography>
You currently have a minting key for this account attached to
this node
</Typography>
</Box>
)}
<Spacer height="10px" />
{mintingAccounts?.map((acct) => (
<Box
key={acct?.mintingAccount}
sx={{
display: 'flex',
gap: '10px',
flexDirection: 'column',
}}
>
<Typography>
Minting account: {handleNames(acct?.mintingAccount)}
</Typography>
<Button
size="small"
sx={{
backgroundColor: 'var(--danger)',
color: 'black',
fontWeight: 'bold',
opacity: 0.7,
maxWidth: '90%',
width: '200px',
'&:hover': {
backgroundColor: 'var(--danger)',
color: 'black',
opacity: 1,
},
}}
onClick={() => {
removeMintingAccount(acct.publicKey, acct);
}}
variant="contained"
>
Remove minting account
</Button>
<Divider />
<Spacer height="10px" />
</Box>
))}
{mintingAccounts?.length > 1 && (
<Typography>
Only 2 minting keys are allowed per node. Please remove one if you
would like to add a different account.
</Typography>
)}
</Card>
<Spacer height="20px" />
{!isPartOfMintingGroup && (
<Card
sx={{
backgroundColor: 'var(--bg-2)',
padding: '10px',
}}
>
<Box
sx={{
display: 'flex',
gap: '5px',
flexDirection: 'column',
width: '100%',
alignItems: 'center',
}}
>
<Typography>
You are currently not part of the MINTER group
</Typography>
<Typography>
Visit the Q-Mintership app to apply to be a minter
</Typography>
<Spacer height="10px" />
<Button
size="small"
sx={{
backgroundColor: 'var(--green)',
color: 'black',
fontWeight: 'bold',
opacity: 0.7,
'&:hover': {
backgroundColor: 'var(--green)',
color: 'black',
opacity: 1,
},
}}
onClick={() => {
executeEvent('addTab', {
data: { service: 'APP', name: 'q-mintership' },
});
executeEvent('open-apps-mode', {});
setIsOpenMinting(false);
}}
variant="contained"
>
Visit Q-Mintership
</Button>
</Box>
</Card>
)}
{showWaitDialog && (
<Dialog
open={showWaitDialog}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{isShowNext ? 'Confirmed' : 'Please Wait'}
</DialogTitle>
<DialogContent>
{!isShowNext && (
<Typography>
Confirming creation of rewardshare on chain. Please be
patient, this could take up to 90 seconds.
</Typography>
)}
{isShowNext && (
<Typography>
Rewardshare confirmed. Please click Next.
</Typography>
)}
</DialogContent>
<DialogActions>
<Button
disabled={!isShowNext}
variant="contained"
onClick={onOk}
autoFocus
>
Next
</Button>
</DialogActions>
</Dialog>
)}
</DialogContent>
<DialogActions>
<Button
// disabled={isLoadingPublish}
variant="contained"
onClick={() => setIsOpenMinting(false)}
>
Close
</Button>
</DialogActions>
<Snackbar
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
open={openSnack}
autoHideDuration={6000}
onClose={handleClose}
>
<Alert
onClose={handleClose}
severity={info?.type}
variant="filled"
sx={{ width: '100%' }}
>
{info?.message}
</Alert>
</Snackbar>
</Dialog>
);
};