import React, { useState, useMemo, useEffect, useCallback } from 'react';
import { useTable, useSortBy } from 'react-table';
import ReactTooltip from 'react-tooltip';
import { toast } from 'react-toastify';
import { trackPromise } from 'react-promise-tracker';
import ModalPending from '../contract-calls-modal/modal-pending';
import ModalSent from '../contract-calls-modal/modal-sent';
import Alert from '../alert';
import { bech32ToChecksum, convertZilToQa, convertQaToCommaStr, convertToProperCommRate, getTruncatedAddress, showWalletsPrompt, convertQaToZilFull, isDigits, computeGasFees, isRespOk, validateBalance } from '../../util/utils';
import { ProxyCalls, OperationStatus, TransactionType, AccountType } from '../../util/enum';
import { computeDelegRewards } from '../../util/reward-calculator';
import { useAppSelector } from '../../store/hooks';
import { StakeModalData } from '../../util/interface';
import { ZilSdk } from '../../zilliqa-api';
import { ZilSigner } from '../../zilliqa-signer';
import GasSettings from './gas-settings';
import BigNumber from 'bignumber.js';
import { logger } from '../../util/logger';
const { BN, units } = require('@zilliqa-js/util');
// hide the data that contains the sender address
// no point to transfer to same person
function Table({ columns, data, tableId, senderAddress, handleNodeSelect, hiddenColumns }: any) {
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable(
{
columns,
data,
initialState : {
hiddenColumns : hiddenColumns,
sortBy: [
{
id: "name",
desc: false
},
]
}
}, useSortBy);
return (
{headerGroups.map(headerGroup => (
{headerGroup.headers.map(column => (
| {column.render('Header')} |
))}
))}
{rows.map((row, i) => {
prepareRow(row)
return (
handleNodeSelect(row.original)}>
{
(row.original as any).address !== senderAddress &&
row.cells.map(cell => {
return | {cell.render('Cell')} |
})
}
)
})}
);
}
function ReDelegateStakeModal(props: any) {
const { updateData, updateRecentTransactions } = props;
const proxy = useAppSelector(state => state.blockchain.proxy);
const impl = useAppSelector(state => state.blockchain.impl);
const networkURL = useAppSelector(state => state.blockchain.blockchain);
const userBase16Address = useAppSelector(state => state.user.address_base16);
const ledgerIndex = useAppSelector(state => state.user.ledger_index);
const accountType = useAppSelector(state => state.user.account_type);
const minDelegStake = useAppSelector(state => state.staking.min_deleg_stake);
const nodeSelectorOptions = useAppSelector(state => state.staking.ssn_dropdown_list);
const stakeModalData: StakeModalData = useAppSelector(state => state.user.stake_modal_data);
const minDelegStakeDisplay = units.fromQa(new BN(minDelegStake), units.Units.Zil);
const fromSsn = stakeModalData.ssnAddress; // bech32
const [toSsn, setToSsn] = useState('');
const [toSsnName, setToSsnName] = useState('');
const [delegAmt, setDelegAmt] = useState('0'); // in ZIL
const [txnId, setTxnId] = useState('');
const [isPending, setIsPending] = useState('');
const [showNodeSelector, setShowNodeSelector] = useState(false);
const defaultGasPrice = ZilSigner.getDefaultGasPrice();
const defaultGasLimit = ZilSigner.getDefaultGasLimit();
const [gasPrice, setGasPrice] = useState(defaultGasPrice);
const [gasLimit, setGasLimit] = useState(defaultGasLimit);
const [gasOption, setGasOption] = useState(false);
// checks if there are any unwithdrawn rewards or buffered deposit
const hasRewardToWithdraw = async () => {
const ssnChecksumAddress = bech32ToChecksum(fromSsn).toLowerCase();
const last_reward_cycle_json = await ZilSdk.getSmartContractSubState(impl, "lastrewardcycle");
const last_buf_deposit_cycle_deleg_json = await ZilSdk.getSmartContractSubState(impl, "last_buf_deposit_cycle_deleg", [userBase16Address]);
if (!isRespOk(last_reward_cycle_json)) {
return false;
}
if (!isRespOk(last_buf_deposit_cycle_deleg_json)) {
return false;
}
// compute rewards
const delegRewards = new BN(await computeDelegRewards(impl, ssnChecksumAddress, userBase16Address));
if (delegRewards.gt(new BN(0))) {
Alert('info', "Unwithdrawn Rewards Found", "Please withdraw the rewards before transferring.");
return true;
}
// secondary buffered deposits check
// different map
// check if user has buffered deposits
if (last_buf_deposit_cycle_deleg_json.last_buf_deposit_cycle_deleg[userBase16Address].hasOwnProperty(ssnChecksumAddress)) {
const lastDepositCycleDeleg = parseInt(last_buf_deposit_cycle_deleg_json.last_buf_deposit_cycle_deleg[userBase16Address][ssnChecksumAddress]);
const lastRewardCycle = parseInt(last_reward_cycle_json.lastrewardcycle);
if (lastRewardCycle <= lastDepositCycleDeleg) {
Alert('info', "Buffered Deposits Found", "Please wait for the next cycle before transferring.");
return true;
}
}
// corner case check
// if user has buffered deposits
// happens if user first time deposit
// reward is zero but contract side warn has unwithdrawn rewards
// user cannot withdraw zero rewards from UI
// if (contract.buff_deposit_deleg.hasOwnProperty(userBase16Address) &&
// contract.buff_deposit_deleg[userBase16Address].hasOwnProperty(ssnChecksumAddress)) {
// const buffDepositMap: any = contract.buff_deposit_deleg[userBase16Address][ssnChecksumAddress];
// const lastCycleDelegNum = Object.keys(buffDepositMap).sort().pop() || '0';
// const lastRewardCycle = parseInt(contract.lastrewardcycle);
// if (lastRewardCycle < parseInt(lastCycleDelegNum + 2)) {
// // deposit still in buffer
// // have to wait for 2 cycles to receive rewards to clear buffer
// Alert('info', "Buffered Deposits Found", "Please wait for 2 more cycles for your rewards to be issued before transferring.");
// return true;
// }
// }
return false;
}
const redeleg = async () => {
let delegAmtQa;
if (!fromSsn || !toSsn) {
Alert('error', "Invalid Node", "Please select a node.");
return null;
}
if (!delegAmt) {
Alert('error', "Invalid Transfer Amount", "Transfer amount cannot be empty.");
return null;
} else {
try {
delegAmtQa = convertZilToQa(delegAmt);
} catch (err) {
// user input is malformed
// cannot convert input zil amount to qa
Alert('error', "Invalid Transfer Amount", "Please check your transfer amount again.");
return null;
}
}
if (await validateBalance(userBase16Address) === 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.");
return null;
}
setIsPending(OperationStatus.PENDING);
// check if deleg has unwithdrawn rewards or buffered deposits for the from ssn address
const hasRewards = await hasRewardToWithdraw();
if (hasRewards) {
setIsPending('');
return null;
}
// create tx params
// toAddr: proxy address
const proxyChecksum = bech32ToChecksum(proxy);
const fromSsnChecksumAddress = bech32ToChecksum(fromSsn).toLowerCase();
const toSsnChecksumAddress = bech32ToChecksum(toSsn).toLowerCase();
const currentAmtQa = stakeModalData.delegAmt;
const leftOverQa = new BN(currentAmtQa).sub(new BN(delegAmtQa));
// check if redeleg more than current deleg amount
if (new BN(delegAmtQa).gt(new BN(currentAmtQa))) {
Alert('info', "Invalid Transfer Amount", "You only have " + convertQaToCommaStr(currentAmtQa) + " ZIL to transfer." );
setIsPending('');
return null;
} else if (!leftOverQa.isZero() && leftOverQa.lt(new BN(minDelegStake))) {
// check leftover amount
// if less than min stake amount
Alert('info', "Invalid Transfer Amount", "Please leave at least " + minDelegStakeDisplay + " ZIL (min. stake amount) or transfer ALL.");
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.REDELEGATE_STAKE,
params: [
{
vname: 'ssnaddr',
type: 'ByStr20',
value: `${fromSsnChecksumAddress}`,
},
{
vname: 'to_ssn',
type: 'ByStr20',
value: `${toSsnChecksumAddress}`,
},
{
vname: 'amount',
type: 'Uint128',
value: `${delegAmtQa}`,
}
]
}),
gasPrice: gasPrice,
gasLimit: gasLimit,
};
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('');
})
);
}
// set default transfer amt to current stake amt
const setDefaultDelegAmt = useCallback(() => {
if (stakeModalData.delegAmt) {
const tempDelegAmt = convertQaToZilFull(stakeModalData.delegAmt);
setDelegAmt(tempDelegAmt);
} else {
setDelegAmt('0');
}
}, [stakeModalData.delegAmt]);
const handleClose = () => {
// txn success
// invoke dashbaord methods
if (txnId) {
updateRecentTransactions(TransactionType.TRANSFER_STAKE, txnId);
updateData();
}
// reset state
// timeout to wait for modal to fade out before clearing
// so that the animation is smoother
toast.dismiss();
setTimeout(() => {
setToSsn('');
setToSsnName('');
setTxnId('');
setDefaultDelegAmt();
setShowNodeSelector(false);
setGasOption(false);
setGasPrice(defaultGasPrice);
setGasLimit(defaultGasLimit);
}, 150);
}
// row contains a json from react-table, similar to the react-table header declaration
const handleNodeSelect = (row: any) => {
setToSsn(row.address);
setToSsnName(row.name);
// reset the view
toggleNodeSelector();
}
const handleDelegAmt = (e: any) => {
setDelegAmt(e.target.value);
}
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 toggleNodeSelector = () => {
logger("toggle node selector: %o", showNodeSelector);
setShowNodeSelector(!showNodeSelector);
}
const columns = useMemo(
() => [
{
Header: 'name',
accessor: 'name'
},
{
Header: 'address',
accessor: 'address',
Cell: ({ row }: any) =>
<>
{getTruncatedAddress(row.original.address)}
>
},
{
Header: 'Delegators',
accessor: 'delegNum',
},
{
Header: 'Stake Amount (ZIL)',
accessor: 'stakeAmt',
Cell: ({ row }: any) =>
<>
{convertQaToCommaStr(row.original.stakeAmt)}
>
},
{
Header: 'Comm. Rate (%)',
accessor: 'commRate',
Cell: ({ row }: any) =>
{convertToProperCommRate(row.original.commRate).toFixed(2)}
}
// eslint-disable-next-line
], []
)
const getHiddenColumns = () => {
let hiddenColumns = ["address"];
return hiddenColumns;
}
useEffect(() => {
setDefaultDelegAmt();
}, [setDefaultDelegAmt]);
return (
{
isPending ?
:
txnId ?
:
<>
Transfer Stake
{
!showNodeSelector &&
<>
{/* sender */}
From
{stakeModalData.ssnName}
{stakeModalData.ssnAddress}
Current Deposit
{convertQaToCommaStr(stakeModalData.delegAmt)} ZIL
{/* recipient*/}
To
{!toSsn &&
}
{ toSsn &&
<>
{toSsnName}
{toSsn}
Enter transfer amount
>
}
>
}
{
showNodeSelector &&
<>
Select a node to transfer to
>
}
>
}
);
}
export default ReDelegateStakeModal;