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 ( {'Manage your minting'} setIsOpenMinting(false)} aria-label="close" > {isLoading && ( )} Account: {handleNames(accountInfo?.address)} Level: {accountInfo?.level} blocks remaining until next level: {_levelUpBlocks()} This node is minting: {nodeInfos?.isMintingPossible?.toString()} {isPartOfMintingGroup && !accountIsMinting && ( {mintingAccounts?.length > 1 && ( Only 2 minting keys are allowed per node. Please remove one if you would like to mint with this account. )} )} {mintingAccounts?.length > 0 && ( Node's minting accounts )} {accountIsMinting && ( You currently have a minting key for this account attached to this node )} {mintingAccounts?.map((acct) => ( Minting account: {handleNames(acct?.mintingAccount)} ))} {mintingAccounts?.length > 1 && ( Only 2 minting keys are allowed per node. Please remove one if you would like to add a different account. )} {!isPartOfMintingGroup && ( You are currently not part of the MINTER group Visit the Q-Mintership app to apply to be a minter )} {showWaitDialog && ( {isShowNext ? 'Confirmed' : 'Please Wait'} {!isShowNext && ( Confirming creation of rewardshare on chain. Please be patient, this could take up to 90 seconds. )} {isShowNext && ( Rewardshare confirmed. Please click Next. )} )} {info?.message} ); };