import React, { useState, useEffect } from 'react'; import { trackPromise } from 'react-promise-tracker'; import { toast } from 'react-toastify'; import { AccountType, OperationStatus, ProxyCalls, TransactionType } from '../../util/enum'; import { bech32ToChecksum, computeGasFees, convertBase16ToBech32, getTruncatedAddress, getZillionExplorerLink, isDigits, isRespOk, showWalletsPrompt, validateBalance } from '../../util/utils'; import Alert from '../alert'; import { toBech32Address, fromBech32Address } from '@zilliqa-js/crypto'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import IconWalletTransferLong from '../icons/wallet-transfer-long'; import IconQuestionCircle from '../icons/question-circle'; import IconArrowDown from '../icons/arrow-down'; import IconEditBox from '../icons/edit-box-line'; import ModalPending from '../contract-calls-modal/modal-pending'; import ModalSent from '../contract-calls-modal/modal-sent'; import ReactTooltip from 'react-tooltip'; import { computeDelegRewards } from '../../util/reward-calculator'; import SwapImg from "../../static/swap_img0.png"; import SwapImg1 from "../../static/swap_img1.png"; import SwapImg2 from "../../static/swap_img2.png"; import SwapImg3 from "../../static/swap_img3.png"; import SwapImg4 from "../../static/swap_img4.png"; import SwapImg5 from "../../static/swap_img5.png"; import { isStorageAvailable } from '../../util/use-local-storage'; import { useAppSelector } from '../../store/hooks'; import { SwapDelegModalData } from '../../util/interface'; import { ZilSigner } from '../../zilliqa-signer'; import { ZilSdk } from '../../zilliqa-api'; import { units } from '@zilliqa-js/zilliqa'; import BigNumber from 'bignumber.js'; import GasSettings from './gas-settings'; const { BN, validation } = require('@zilliqa-js/util'); function SwapDelegModal(props: any) { const {updateData, updateRecentTransactions} = props; const swapDelegModalData: SwapDelegModalData = useAppSelector(state => state.user.swap_deleg_modal_data); const proxy = useAppSelector(state => state.blockchain.proxy); const impl = useAppSelector(state => state.blockchain.impl); const networkURL = useAppSelector(state => state.blockchain.blockchain); const wallet = useAppSelector(state => state.user.address_base16); const accountType = useAppSelector(state => state.user.account_type); const ledgerIndex = useAppSelector(state => state.user.ledger_index); const userAddress = useAppSelector(state => state.user.address_bech32); const ssnstatsList = useAppSelector(state => state.staking.ssn_list); const proxyChecksum = bech32ToChecksum(proxy); // transfer all stakes to this new deleg const [newDelegAddr, setNewDelegAddr] = useState(''); // bech32 const [selectedDelegAddr, setSelectedDelegAddr] = useState(''); // base16; used in incoming requests tab to track the address being accepted or rejected const [txnId, setTxnId] = useState(''); const [txnType, setTxnType] = useState(''); const [tabIndex, setTabIndex] = useState(0); const [isPending, setIsPending] = useState(''); const [isEdit, setIsEdit] = useState(false); const [showHelpBox, setShowHelpBox] = useState(false); const [showConfirmSendRequestBox, setShowConfirmSendRequestBox] = useState(false); const [showConfirmRevokeBox, setShowConfirmRevokeBox] = useState(false); const [showConfirmSwapBox, setShowConfirmSwapBox] = useState(false); const [showConfirmRejectBox, setShowConfirmRejectBox] = useState(false); const [tutorialStep, setTutorialStep] = useState(0); // for the help guide const defaultGasPrice = ZilSigner.getDefaultGasPrice(); const defaultGasLimit = ZilSigner.getDefaultGasLimit(); const [gasPrice, setGasPrice] = useState(defaultGasPrice); const [gasLimit, setGasLimit] = useState(defaultGasLimit); const [gasOption, setGasOption] = useState(false); const cleanUp = () => { setNewDelegAddr(''); setSelectedDelegAddr(''); setTabIndex(0); setIsPending(''); setIsEdit(false); setTxnId(''); setTxnType(''); setShowHelpBox(false); setShowConfirmSendRequestBox(false); setShowConfirmRevokeBox(false); setShowConfirmSwapBox(false); setShowConfirmRejectBox(false); setTutorialStep(0); setGasOption(false); setGasPrice(defaultGasPrice); setGasLimit(defaultGasLimit); } const validateAddress = (address: string) => { if (!address || address === "" || (!validation.isAddress(address) && !validation.isBech32(address)) ) { return false; } return true; } // checks if user entered ssn address // address zil format const isSsnAddress = (address: string) => { for (let ssn of ssnstatsList) { if (address === ssn.address) { return true; } } return false; } const handleNewDelegAddr = (e : any) => { let address = e.target.value; if (!address || address === null) { address = ''; } else if (address && (validation.isAddress(address) || validation.isBech32(address)) ) { address = toBech32Address(bech32ToChecksum(address)); if (isSsnAddress(address)) { Alert('error', "Invalid Address", `Do not enter a node address. If you want to transfer your stake from one node to another node, go to "Staking Portfolio" > "Manage" > "Transfer Stake" `); address = ''; } } setNewDelegAddr(address); } const decreaseTutorialStep = () => { let newTutorialStep = tutorialStep - 1; setTutorialStep(newTutorialStep); } const incrementTutorialStep = () => { let newTutorialStep = tutorialStep + 1; setTutorialStep(newTutorialStep); } // validate the input target recipient before showing the confirm send box const toggleConfirmSendRequestBox = () => { let targetRecipientAddr = newDelegAddr; if (!validateAddress(targetRecipientAddr)) { Alert('error', "Invalid Address", "Wallet address should be bech32 or checksum format."); return null; } setShowConfirmSendRequestBox(true); } const toggleConfirmSwapBox = (address : string) => { setSelectedDelegAddr(address); setShowConfirmSwapBox(true); } const toggleRejectSwapBox = (address : string) => { setSelectedDelegAddr(address); setShowConfirmRejectBox(true); } const toggleRevokeSwapBox = () => { // hide the new owner address form when clicked on revoke setIsEdit(false); setShowConfirmRevokeBox(true); } const handleClose = () => { // txn success // invoke dashboard methods if (txnId) { let convertedTxnType; switch (txnType) { case TransactionType.REQUEST_DELEG_SWAP.toString(): convertedTxnType = TransactionType.REQUEST_DELEG_SWAP; break; case TransactionType.REVOKE_DELEG_SWAP.toString(): convertedTxnType = TransactionType.REVOKE_DELEG_SWAP; break; case TransactionType.CONFIRM_DELEG_SWAP.toString(): convertedTxnType = TransactionType.CONFIRM_DELEG_SWAP; break; case TransactionType.REJECT_DELEG_SWAP.toString(): convertedTxnType = TransactionType.REJECT_DELEG_SWAP; break; } updateRecentTransactions(convertedTxnType, txnId); updateData(); } // reset state // timeout to wait for modal to fade out before clearing // so that the animation is smoother toast.dismiss(); setTimeout(() => { cleanUp(); }, 150); } // reset the tutorial const handleCloseTutorial = () => { setShowHelpBox(false); setTutorialStep(0); } const sendTxn = async (txnType: TransactionType, txParams: any) => { if (await validateBalance(wallet) === false) { const gasFees = computeGasFees(gasPrice, gasLimit); Alert('error', "Insufficient Balance", "Insufficient balance in wallet to pay for the gas fee."); Alert('error', "Gas Fee Estimation", "Current gas fee is around " + units.fromQa(gasFees, units.Units.Zil) + " ZIL."); setIsPending(''); return null; } // set txn type to store in cookie setTxnType(txnType.toString()); setIsPending(OperationStatus.PENDING); showWalletsPrompt(accountType); trackPromise(ZilSigner.sign(accountType as AccountType, txParams, ledgerIndex) .then((result) => { if (result === OperationStatus.ERROR) { Alert('error', "Transaction Error", "Please try again."); } else { setTxnId(result) } }).finally(() => { setIsPending(''); }) ); } const confirmDelegSwap = async (requestorAddr: string) => { setIsPending(OperationStatus.PENDING); const requestorHasBuffOrRewards = await hasBufferedOrRewards("ConfirmSwap", requestorAddr); if (requestorHasBuffOrRewards) { // requestor has buffered deposits or rewards setIsPending(''); return null; } // userAddress here is the new delegator waiting to receive const newDelegHasBuffOrRewards = await hasBufferedOrRewards("ConfirmSwap", userAddress); if (newDelegHasBuffOrRewards) { setIsPending(''); return null; } let txParams = { toAddr: proxyChecksum, amount: new BN(0), code: "", data: JSON.stringify({ _tag: ProxyCalls.CONFIRM_DELEG_SWAP, params: [ { vname: 'requestor', type: 'ByStr20', value: `${requestorAddr}`, } ] }), gasPrice: gasPrice, gasLimit: gasLimit, }; await sendTxn(TransactionType.CONFIRM_DELEG_SWAP, txParams); } const rejectDelegSwap = async (requestorAddr: string) => { let txParams = { toAddr: proxyChecksum, amount: new BN(0), code: "", data: JSON.stringify({ _tag: ProxyCalls.REJECT_DELEG_SWAP, params: [ { vname: 'requestor', type: 'ByStr20', value: `${requestorAddr}`, } ] }), gasPrice: gasPrice, gasLimit: gasLimit, }; await sendTxn(TransactionType.REJECT_DELEG_SWAP, txParams); } // returns true if address is ok // otherwise returns false // @param swapAddr: bech32 format const isValidSwap = (swapAddr: string) => { // check if it is self swap if (userAddress === swapAddr) { Alert('error', "Invalid New Owner", "Please enter another wallet address other than the connected wallet."); return false; } // check if it is cyclic, i.e. if B -> X, where X == A exists let byStr20SwapAddr = fromBech32Address(swapAddr).toLowerCase(); if (swapDelegModalData.requestorList.includes(byStr20SwapAddr)) { let msg = `There is an existing request from ${getTruncatedAddress(swapAddr)}. Please accept or reject the incoming request first.`; Alert('error', "Invalid New Owner", msg); return false; } return true; } // check if address has buffered deposits or unwithdrawn rewards // returns true if the address has buffered deposits or rewards, otherwise returns false // @param invokerMethod: a string to determine if it is coming from RequestSwap or ConfirmSwap to change the message display // @param address: bech32 format const hasBufferedOrRewards = async (invokerMethod: string = "", address: string) => { let wallet = bech32ToChecksum(address).toLowerCase(); let displayAddr = getTruncatedAddress(address); let targetName = address === userAddress ? "Your wallet" : invokerMethod === "RequestSwap" ? "The recipient" : "The requestor" const lrc = await ZilSdk.getSmartContractSubState(impl, "lastrewardcycle"); const lbdc = await ZilSdk.getSmartContractSubState(impl, "last_buf_deposit_cycle_deleg", [wallet]); const deposit_amt_deleg_map = await ZilSdk.getSmartContractSubState(impl, "deposit_amt_deleg", [wallet]); const buff_deposit_deleg_map = await ZilSdk.getSmartContractSubState(impl, "buff_deposit_deleg", [wallet]); if (!isRespOk(lrc)) { return false; } if (!isRespOk(lbdc)) { return false; } if (!isRespOk(deposit_amt_deleg_map)) { // delegator has not delegate with any ssn // no need to perform further checks return false; } let ssnlist = deposit_amt_deleg_map["deposit_amt_deleg"][wallet]; for (let ssnAddress in ssnlist) { // check rewards const rewards = new BN(await computeDelegRewards(impl, ssnAddress, wallet)); if (rewards.gt(new BN(0))) { let msg = `${targetName} ${displayAddr} has unwithdrawn rewards. Please withdraw or wait until the user has withdrawn the rewards before continuing.` Alert('info', "Unwithdrawn Rewards Found", msg); return true; } const lrc_o = parseInt(lrc["lastrewardcycle"]); // check buffered deposits if (lbdc["last_buf_deposit_cycle_deleg"][wallet].hasOwnProperty(ssnAddress)) { const ldcd = parseInt(lbdc["last_buf_deposit_cycle_deleg"][wallet][ssnAddress]); if (lrc_o <= ldcd) { let msg = `${targetName} ${displayAddr} has buffered deposits. Please wait for the next cycle before continuing.` Alert('info', "Buffered Deposits Found", msg); return true; } } // check buffered deposits (lrc-1) let lrc_o_minus = lrc_o - 1 if (lrc_o_minus >= 0) { if (isRespOk(buff_deposit_deleg_map) && buff_deposit_deleg_map["buff_deposit_deleg"][wallet].hasOwnProperty(ssnAddress)) { if (buff_deposit_deleg_map["buff_deposit_deleg"][wallet][ssnAddress].hasOwnProperty(lrc_o_minus)) { let msg = `${targetName} ${displayAddr} has buffered deposits. Please wait for the next cycle before continuing.` Alert('info', "Buffered Deposits Found", msg); return true; } } } } return false; } // check if address has staked with some ssn // returns false if address has not stake; otherwise returns true const hasStaked = async (address: string) => { let wallet = fromBech32Address(address).toLowerCase(); const deposit_amt_deleg_map = await ZilSdk.getSmartContractSubState(impl, "deposit_amt_deleg", [wallet]); if (!isRespOk(deposit_amt_deleg_map)) { return false; } return true; } // check if address has pending withdrawal // returns false if address has no pending withdrawal const hasPendingWithdrawal = async (address: string) => { let wallet = fromBech32Address(address).toLowerCase(); const pending_withdrawal_map = await ZilSdk.getSmartContractSubState(impl, "withdrawal_pending", [wallet]); if (!isRespOk(pending_withdrawal_map)) { return false; } return true; } const requestDelegSwap = async () => { if (!validateAddress(newDelegAddr)) { Alert('error', "Invalid Address", "Wallet address should be bech32 or checksum format."); return null; } if (!isValidSwap(newDelegAddr)) { return null; } setIsPending(OperationStatus.PENDING); const userHasStaked = await hasStaked(userAddress); const userHasPendingWithdrawal = await hasPendingWithdrawal(userAddress); if (!userHasStaked && !userHasPendingWithdrawal) { setIsPending(''); Alert('info', "User Has No Stake", `You have not staked with any operators.`); return null; } const userHasBuffOrRewards = await hasBufferedOrRewards("RequestSwap", userAddress); if (userHasBuffOrRewards) { // user has buffered deposits or rewards setIsPending(''); return null; } const newDelegHasBuffOrRewards = await hasBufferedOrRewards("RequestSwap", newDelegAddr); if (newDelegHasBuffOrRewards) { setIsPending(''); return null; } // gas price, gas limit declared in account.ts let txParams = { toAddr: proxyChecksum, amount: new BN(0), code: "", data: JSON.stringify({ _tag: ProxyCalls.REQUEST_DELEG_SWAP, params: [ { vname: 'new_deleg_addr', type: 'ByStr20', value: `${fromBech32Address(newDelegAddr)}`, } ] }), gasPrice: gasPrice, gasLimit: gasLimit, }; await sendTxn(TransactionType.REQUEST_DELEG_SWAP, txParams); } const revokeDelegSwap = async () => { // gas price, gas limit declared in account.ts let txParams = { toAddr: proxyChecksum, amount: new BN(0), code: "", data: JSON.stringify({ _tag: ProxyCalls.REVOKE_DELEG_SWAP, params: [] }), gasPrice: gasPrice, gasLimit: gasLimit, }; await sendTxn(TransactionType.REVOKE_DELEG_SWAP, txParams); } const onBlurGasPrice = () => { if (gasPrice === '' || new BigNumber(gasPrice).lt(new BigNumber(defaultGasPrice))) { setGasPrice(defaultGasPrice); Alert("Info", "Minimum Gas Price Required", "Gas price should not be lowered than default blockchain requirement."); } } const onGasPriceChange = (e: React.ChangeEvent) => { let input = e.target.value; if (input === '' || isDigits(input)) { setGasPrice(input); } } const onBlurGasLimit = () => { if (gasLimit === '' || new BigNumber(gasLimit).lt(50)) { setGasLimit(defaultGasLimit); } } const onGasLimitChange = (e: React.ChangeEvent) => { let input = e.target.value; if (input === '' || isDigits(input)) { setGasLimit(input); } } const onSelectTabs = (index: number) => { // reset the advanced gas settings display on change tabs setTabIndex(index); setGasOption(false); setGasPrice(defaultGasPrice); setGasLimit(defaultGasLimit); } useEffect(() => { // show the tutorial if this is the first time // user clicks on the change stake ownership button if (isStorageAvailable('localStorage')) { const storedValue: any = window.localStorage.getItem("show-swap-help"); if (storedValue === null) { setShowHelpBox(true); window.localStorage.setItem("show-swap-help", JSON.stringify(false)); } } }, []) return ( ); } export default SwapDelegModal;