import { Alert, Box, Button, Card, Dialog, DialogActions, DialogContent, DialogTitle, Divider, IconButton, Snackbar, Typography, useTheme, } from '@mui/material'; import { useCallback, useEffect, useMemo, useState } from 'react'; import CloseIcon from '@mui/icons-material/Close'; import { getBaseApiReact } from '../../App'; import { executeEvent, subscribeToEvent, unsubscribeFromEvent, } from '../../utils/events'; import { getFee } from '../../background'; import { Spacer } from '../../common/Spacer'; import { FidgetSpinner } from 'react-loader-spinner'; import { useModal } from '../../common/useModal'; import { useAtom, useSetAtom } from 'jotai'; import { memberGroupsAtom, txListAtom } from '../../atoms/global'; import { useTranslation } from 'react-i18next'; export const Minting = ({ setIsOpenMinting, myAddress, show }) => { const setTxList = useSetAtom(txListAtom); const [groups] = useAtom(memberGroupsAtom); 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 theme = useTheme(); const { t } = useTranslation(['auth', 'core', 'group']); 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) { console.log(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) { console.log(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) { console.log(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) { console.log(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 || t('core:message.error.generic', { postProcess: 'capitalizeFirstChar', }), }); }); }); } catch (error) { setInfo({ type: 'error', message: error?.message || t('core:message.error.minting_account_add', { postProcess: 'capitalizeFirstChar', }), }); 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 || t('core:message.error.generic', { postProcess: 'capitalizeFirstChar', }), }); }); }); } catch (error) { setInfo({ type: 'error', message: error?.message || t('core:message.error.minting_account_remove', { postProcess: 'capitalizeFirstChar', }), }); setOpenSnack(true); } finally { setIsLoading(false); } }, []); const createRewardShare = useCallback(async (publicKey, recipient) => { const fee = await getFee('REWARD_SHARE'); await show({ message: t('core:message.question.perform_transaction', { action: 'REWARD_SHARE', postProcess: 'capitalizeFirstChar', }), 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: t('group:message.success.rewardshare_add', { postProcess: 'capitalizeFirstChar', }), labelDone: t('group:message.success.rewardshare_add_label', { postProcess: 'capitalizeFirstChar', }), done: false, }, ...prev, ]); res(response); return; } rej({ message: response.error }); }) .catch((error) => { rej({ message: error.message || t('core:message.error.generic', { postProcess: 'capitalizeFirstChar', }), }); }); }); }, []); 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 || t('core:message.error.generic', { postProcess: 'capitalizeFirstChar', }), }); }); }); }, []); 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( t('group:message.error.timeout_reward', { postProcess: 'capitalizeFirstChar', }) ); }; 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 || t('group:message.error.unable_minting', { postProcess: 'capitalizeFirstChar', }), }); 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: t('group:message.success.rewardshare_remove', { postProcess: 'capitalizeFirstChar', }), labelDone: t('group:message.success.rewardshare_remove_label', { postProcess: 'capitalizeFirstChar', }), done: false, }, ...prev, ]); return; } rej({ message: response.error }); }) .catch((error) => { rej({ message: error.message || t('core:message.error.generic', { postProcess: 'capitalizeFirstChar', }), }); }); }); }, []); useEffect(() => { getNodeInfos(); getMintingAccounts(); }, []); useEffect(() => { if (!myAddress) return; getRewardShares(myAddress); getAccountInfo(myAddress); }, [myAddress]); const _blocksNeed = () => { if (accountInfo?.level === 0) { return 7200; // TODO manage these magic numbers in a proper location } 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 ( {t('group:message.generic.manage_minting', { postProcess: 'capitalizeFirstChar', })} setIsOpenMinting(false)} aria-label="close" > {isLoading && ( )} {t('auth:account.account_one', { postProcess: 'capitalizeFirstChar', })} : {handleNames(accountInfo?.address)} {t('core:level', { postProcess: 'capitalizeFirstChar', })} : {accountInfo?.level} {t('group:message.generic.next_level', { postProcess: 'capitalizeFirstChar', })}{' '} {_levelUpBlocks()} {t('group:message.generic.node_minting', { postProcess: 'capitalizeFirstChar', })}{' '} {nodeInfos?.isMintingPossible?.toString()} {isPartOfMintingGroup && !accountIsMinting && ( {mintingAccounts?.length > 1 && ( {t('group:message.generic.minting_keys_per_node', { postProcess: 'capitalizeFirstChar', })} )} )} {mintingAccounts?.length > 0 && ( {t('group:message.generic.node_minting_account', { postProcess: 'capitalizeFirstChar', })} )} {accountIsMinting && ( {t('group:message.generic.node_minting_key', { postProcess: 'capitalizeFirstChar', })} )} {mintingAccounts?.map((acct) => ( {t('group:message.generic.minting_account', { postProcess: 'capitalizeFirstChar', })}{' '} {handleNames(acct?.mintingAccount)} ))} {mintingAccounts?.length > 1 && ( {t('group:message.generic.minting_keys_per_node_different', { postProcess: 'capitalizeFirstChar', })} )} {!isPartOfMintingGroup && ( {t('group:message.generic.minter_group', { postProcess: 'capitalizeFirstChar', })} {t('group:message.generic.mintership_app', { postProcess: 'capitalizeFirstChar', })} )} {showWaitDialog && ( {isShowNext ? 'Confirmed' : 'Please Wait'} {!isShowNext && ( {t('group:message.success.rewardshare_creation', { postProcess: 'capitalizeFirstChar', })} )} {isShowNext && ( {t('group:message.success.rewardshare_confirmed', { postProcess: 'capitalizeFirstChar', })} )} )} {info?.message} ); };