/*
 * last modified---
 * 	08-17-26 add text indicating MVO Pool membership features may not work
 * 			 without a paid API key
 * 	06-10-26 add DOM ids to support browser automation
 * 	10-29-25 add correct MVO registration link
 * 	10-20-25 tolerate getPastEvents() failures in fetchMVOPools()
 *  11-20-24 debugging
 * 	10-11-24 retool to use stakeWithPermit() instead of stake(), since the
 * 			 EnshroudToken contract supports EIP-2612
 * 	10-09-24 allow additional withdrawals of never-locked staked tokens too
 * 	10-04-24 complete
 * 	08-15-24 new (placeholder)
 *
 * purpose---
 * 	provide UI/UX to manage an MVO node and its staking
 */

import React, { useState, Fragment } from 'react';
import useEth from '../EthContext/useEth';
import Table from 'react-bootstrap/Table';
import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';
import Accordion from 'react-bootstrap/Accordion';
import Card from 'react-bootstrap/Card';
import ListGroup from 'react-bootstrap/ListGroup';
import { MVOStaking } from '../MVOs.js';
import LoadingButton from '../LoadingButton.jsx';
const BigNumber = require("bignumber.js");


/* method to obtain the MVO Id and staking record user is administering
 * @param props.mvoId the MVOId being worked with (empty to start)
 * @param props.setMVOId method to register entered MVOId
 * @param props.setStaking method to register newly retrieved MVO staking record
 * @param props.setAuth method to set whether user can modify MVO staking record
 * @param props.setUserTokens method to set user's $ENSHROUD token balance
 * @param props.setMVOTimelock method to set user's MVOStaking Timelock
 * @param props.mgrTimelock current TimelockManager Timelock, if any
 * @param props.setTMTimelock method to set user's TimelockManager Timelock
 * @param props.withdrawable unlocked tokens in TimelockManager Timelock
 * @param props.setUnlocked method to set user's unlocked Timelock tokens
 * @param props.minStaking minimum staking quantum for this chain
 * @param props.setMinStaking method to set the minStake for this chain
 * @param props.setMVOPools method to register freshly fetched MVO Pool list
 */
function GetMVOStaking(props) {
	// enable use of our contracts and accounts
	const { state: { contracts, accounts, chainConn } } = useEth();
	const userAcct = accounts[0];
	// NB: tokenGenesis is a lower-bound for starting to look for events
	var startBlock = chainConn.chainConfig.tokenGenesis;
	if (startBlock === undefined) {
		console.error("No tokenGenesis found for MVOStaking on chain Id "
						+ chainConn.chainConfig.chainId);
		startBlock = "earliest";
	}

	// contracts we need to access
	const mvoStakingContract = contracts["MVOStaking"];
	const TimelockMgr = contracts["TimelockManager"];
	const enshTokenContract = contracts["EnshroudToken"];

	// properties shorthands
	var mvoId = props.mvoId;

	// process a MVO Id value change
	const handleMVOIdEntry = e => {
		const id = e.target.value.toString();
		if (id === '') {
			props.setMVOId('');
		}
		else if (/[MVO\-0-9]+/.test(id)) {
			props.setMVOId(id);
		}
	};

	/* determine whether this user (accounts[0]) owns the selected staking
	 * @param staking the MVO staking record
	 */
	function authToUpdate(staking) {
		let stakingAddr = "";
		if (staking !== undefined) stakingAddr = staking.stakingAddress;
		const isAuth = stakingAddr === userAcct;
		props.setAuth(isAuth);
	};

	// method to obtain value of MVOStaking.minStake on current chain
	const getMinStake = async () => {
		// obtain value of public variable 'minStake'
		const chainMinStake = await mvoStakingContract.methods.minStake()
													.call({ from: userAcct });
		props.setMinStaking(chainMinStake);
	};

	// obtain user's current balance of $ENSHROUD tokens (in wei)
	const fetchUserBalance = async () => {
		let userTokens = await enshTokenContract.methods.balanceOf(userAcct)
													.call( { from: userAcct });
		props.setUserTokens(userTokens);
	};

	// obtain user's MVOStaking timelock balance (if any)
	const fetchUserTimelock = async () => {
		let timelock = await mvoStakingContract.methods.timelocks(userAcct)
													.call({ from: userAcct });
		if (timelock !== undefined) {
			props.setMVOTimelock(timelock);
		}
	};

	// obtain manager's timelock (if any)
	var fetchedTL = props.mgrTimelock;
	const fetchMgrTimelock = async () => {
		let tLock = await TimelockMgr.methods.timelocks(userAcct).call(
															{ from: userAcct });
		if (tLock !== undefined) {
			props.setTMTimelock(tLock);
			fetchedTL = tLock;
		}
	};
	
	// obtain user's withdrawable balance of unlocked tokens (in wei, poss. 0)
	const fetchWithdrawable = async () => {
		let withAmt = "0";
		const tLock = fetchedTL;
		if (tLock.remainingAmount !== "0") {
			// see how much of the remaining amount is withdrawable
			withAmt = await TimelockMgr.methods.getWithdrawable(userAcct).call(
															{ from: userAcct });
			if (!props.withdrawable.eq(withAmt)) {
				let withBN = new BigNumber(withAmt);
				props.setUnlocked(withBN);
			}
		}
	};

	/* Obtain a list of all MVOPoolCreated and MVOPoolDeleted events since
	 * genesis.  For each of these, determine whether the pool still exists,
	 * and if it does add it to the list of available pools.
	 */
	const fetchMVOPools = async () => {
		// list of creations
		var creList = await mvoStakingContract.getPastEvents('MVOPoolCreated',
		{
			fromBlock: startBlock,
		})
		.catch(err => {
			alert("MVOPoolCreated event fetch error: code " + err.code + ", "
				+ err.message);
		});

		// list of deletions
		var delList = await mvoStakingContract.getPastEvents('MVOPoolDeleted',
		{
			fromBlock: startBlock,
		})
		.catch(err => {
			alert("MVOPoolDeleted event fetch error: code " + err.code + ", "
				+ err.message);
		});
		// sometimes past event fetches fail but other blockchain accesses work
		if (creList === undefined || creList === null
			|| delList === undefined || delList === null)
		{
			alert("Warning: MVO Pool membership could not be determined.\n"
				+ "You may need a JSON-RPC endpoint with a paid API key in "
				+ "order to retrieve events this far in the past.");
		}

		// parse event lists into arrays
		var poolList = [];
		var deletedList = [];
		// count number of times we see creations of a given poolId
		var idCreCounts = new Map();
		if (creList !== undefined && creList !== null) {
			for (const creEvent of creList) {
				const pId = creEvent.returnValues.poolId;
				poolList.push({	poolId: pId,
								dues: creEvent.returnValues.dues,
								creator: creEvent.returnValues.creatorMvoID });
				if (!idCreCounts.has(pId)) {
					idCreCounts.set(pId, 1);
				}
				else {
					const cnt = idCreCounts.get(pId);
					idCreCounts.set(pId, cnt+1);
				}
			}
		}

		// count number of times we see deletions of a given poolId
		var idDelCounts = new Map();
		if (delList !== undefined && delList !== null) {
			for (const delEvent of delList) {
				const pId = delEvent.returnValues.poolId;
				deletedList.push({	poolId: pId,
								creator: delEvent.returnValues.creatorMvoID });
				if (!idDelCounts.has(pId)) {
					idDelCounts.set(pId, 1);
				}
				else {
					const cnt = idDelCounts.get(pId);
					idDelCounts.set(pId, cnt+1);
				}
				// find the event that created pool (consistency double-check)
				let creEv = poolList.find((elt) => elt.poolId === pId);
				if (creEv === undefined) {
					// the smart contract should make this impossible
					console.error("No matching MVOPoolCreated event for "
								+ "MVOPoolDeleted for poolId " + pId);
				}
			}
		}

		// merge lists
		var undeletedList = [];
		for (const creEv of poolList) {
			/* NB: while poolIDs must be unique, it's possible they may get
			 * recycled.  So we have to allow for more creations than deletes,
			 * as well as the simple case where it was created and not deleted.
			 */
			const pId = creEv.poolId;
			if (!deletedList.some((elt) => elt.poolId === pId)) {
				// this poolId was created but never deleted
				undeletedList.push(creEv);
			}
			else {
				// check whether we have more creations than deletions
				const creCnt = idCreCounts.get(pId);
				const delCnt = idDelCounts.get(pId);
				if (creCnt > delCnt) {
					// this poolId has been recycled at least once
					undeletedList.push(creEv);
				}
			}
		}
		props.setMVOPools(undeletedList);
		return undeletedList;
	};

	// perform actual lookup of MVO staking record for this Id
	const fetchMVOrecord = async () => {
		const mvoRec = await mvoStakingContract.methods.idToMVO(mvoId).call(
															{ from: userAcct });
		if (mvoRec !== undefined && mvoRec.signingAddress
							!== '0x0000000000000000000000000000000000000000')
		{
			const stakingRec = new MVOStaking();
			stakingRec.setMVOId(mvoId);
			stakingRec.config(mvoRec);

			// if we haven't yet done so, get value of minStake on chain
			if (props.minStaking === 0) {
				await getMinStake();
			}

			// determine whether this user is allowed to update this staking
			authToUpdate(stakingRec);
			props.setStaking(stakingRec);

			// obtain user's current balance of $ENSHROUD tokens
			await fetchUserBalance();

			// fetch user's Timelock in MVOStaking, if any
			await fetchUserTimelock();

			// fetch user's Timelock in TimelockManager, if any
			await fetchMgrTimelock();

			// fetch withdrawable unlocked tokens in TimelockManager, if any
			await fetchWithdrawable();

			if (stakingRec.stakingAddress === userAcct) {
				// also fetch list of MVO pools currently defined on this chain
				const allPools = await fetchMVOPools();

				// if pool shown isn't in the undeleted pools list, override
				// (this is done to prevent errors accessing non-existent pools)
				const poolShown = allPools.find((pool) =>
									pool.poolId === stakingRec.memberPoolId);
				if (poolShown === undefined) {
					stakingRec.memberPoolId = 0;
				}
			}
		} else {
			alert("No MVO Staking record found for MVOId : " + mvoId);
		}
	};

	// form to query the MVO Id wanted
	const queryMVOId =
		<>
			<Form>
				<Form.Group className="mb-3">
					<Form.Label>Enter your MVO Id:</Form.Label>
					<Form.Control type="text" placeholder="MVO-nnn" id="mvoID"
						value={props.mvoId} onChange={handleMVOIdEntry}
					/>
				</Form.Group>
				<Button variant="info" className="m-3" id="getMVOdata"
					onClick={() => fetchMVOrecord()}
				>
					Get Record
				</Button>
			</Form>
		</>;

	// render enter MVO Id form
	return (
		<div id="enterId">
			{ queryMVOId }
		</div>
	);
}

/* render an MVOStaking record as a Nx2 form of name/value pairs (read-only)
 * @param props.mvoStaking the retrieved staking record (a MVOStaking)
 * @param props.user the owning account (matches the .stakingAddress property)
 */
function MVOStakingRenderer(props) {
	const staking = props.mvoStaking;
	const { state: { web3 } } = useEth();

	// error message for staking not found for given Id
	var mvoID = "(unset)";
	if (staking !== undefined) mvoID = staking.mvoId;
	const stakingNotFound = 
		<>
			<p className="text-danger">
				No MVOStaking record found for MVO Id {mvoID}.
			</p>
			<br/>
			<p>
				If you need to register your MVO to create your staking, you
				may do so using this link:<br/>
				<a href="https://enshroud.info/registerMVO.html"
					target="_blank" rel="noreferrer noopener">
					https://enshroud.info/registerMVO.html
				</a>
			</p>
		</>;

	// rendering of staking record details, when found
	var stakingRendering = stakingNotFound;
	var stakingDetails = '';
	if (staking !== undefined &&
		staking.stakingAddress !== '0x0000000000000000000000000000000000000000')
	{
		stakingDetails =
			<>
				<hr/>
				<br/>
				<h4>Details of Staking Record ({mvoID})</h4>
				<br/>
				<Form>
					{ /* MVO Id echo */ }
					<Form.Group className="mb-3" controlId="mvoID">
						<Form.Label>MVO Id:</Form.Label>
						<Form.Control type="text" readOnly value={mvoID} />
					</Form.Group>
					{ /* staking address (owner) */ }
					<Form.Group className="mb-3" controlId="stakingAddr">
						<Form.Label>Staking Address:</Form.Label>
						<Form.Control type="text" readOnly
							value={staking.stakingAddress}
						/>
					</Form.Group>
					{ /* staking amount */ }
					<Form.Group className="mb-3" controlId="stakingAmt">
						<Form.Label>Current Staking Amount:</Form.Label>
						<Form.Control type="text" readOnly
							value={web3.utils.fromWei(staking.stakingQuantum)}
						/>
					</Form.Group>
					{ /* signing address */ }
					<Form.Group className="mb-3" controlId="signingAddr">
						<Form.Label>Signing Address:</Form.Label>
						<Form.Control type="text" readOnly
							value={staking.signingAddress}
						/>
					</Form.Group>
					{ /* active */ }
					<Form.Group className="mb-3" controlId="active">
						<Form.Label>Active (set by admin):</Form.Label>
						<Form.Control type="text" readOnly
							value={staking.active}
							className={staking.active ? "text-success" : "text-warning"}
						/>
					</Form.Group>
					{ /* pool membership Id */ }
					<Form.Group className="mb-3" controlId="poolId">
						<Form.Label>
							Member of Pool Id (0 means solo MVO):
						</Form.Label>
						<Form.Control type="text" readOnly
							value={staking.memberPoolId}
						/>
					</Form.Group>
					{ /* encryption pubkey */ }
					<Form.Group className="mb-3" controlId="pubKey">
						<Form.Label>Encryption pubkey:</Form.Label>
						<Form.Control type="text" readOnly
							value={staking.encrPubkey}
						/>
					</Form.Group>

				</Form>
			</>;
		stakingRendering = stakingDetails;
	}

	// render display of staking items
	return (
		<div id="stakingDetails">
			{ stakingRendering }
		</div>
	);
}

/* show UI to add to an MVO staking
 * @param props.mvoStaking the retrieved staking record (a MVOStaking)
 * @param props.isAuth whether user owns the staking record
 * @param props.userBal quantity of $ENSHROUD the user has available
 * @param props.setUserTokens method for setting/updating user token balance
 * @param props.minStake minimum number of $ENSHROUD for an active staking (wei)
 * @param props.setStaking configure the staking record for an MVO Id
 */
function ActionAddToStaking(props) {
	const staking = props.mvoStaking;
	// enable use of our contracts and wallet accounts
	const { state: { accounts, contracts, web3, chainConn } } = useEth();

	const MVOStakingContract = contracts["MVOStaking"];
	const mvoStakingAddr = MVOStakingContract.options.address;
	const EnshroudTokenContract = contracts["EnshroudToken"];
	const tokenAddr = EnshroudTokenContract.options.address;
	const userAcct = accounts[0];
	const chId = chainConn.chainConfig.chainId;
	const minStaking
		= web3.utils.fromWei(new BigNumber(props.minStake).toFixed());

	// local state for addition to state
	const [stakeIncrement, setStateIncrement] = useState(new BigNumber(0));

	// process a change to staking increment
	const handleStakingChange = e => {
		var amt = new BigNumber(0.0);
		let inpVal = e.target.value.toString();
		if (inpVal === '') inpVal = "0.0";
		if (isNaN(inpVal)) inpVal = "0.0";
		let stakeAmtStr = inpVal;
		let stakeAmt = new BigNumber(stakeAmtStr);
		if (stakeAmt.isNegative()) {
			stakeAmtStr = "0.0";
			stakeAmt = amt;
		}
		amt = amt.plus(stakeAmt);
		const stakeWei = amt.times("1e18");
		if (stakeWei.gte(0)) {
			setStateIncrement(stakeWei);
		}
	};
	const minStake = "Required: " + minStaking + " or more";

	// get the nonce value for EnshroudToken contract (EIP-2612 compatible)
	const getNonce = async () => {
		var nonce = 0;
		const callData = web3.eth.abi.encodeFunctionCall({
			name: 'nonces',
			type: 'function',
			constant: 'true',
			inputs: [{
				type: 'address',
				name: ''
			}],
			outputs: [{
				type: 'uint256'
			}]
		}, [userAcct]);
		await web3.eth.call({
			to: tokenAddr,	// erc20/erc777/erc4626 contract address
			from: userAcct,
			data: callData	// function call encoding
		})
			.then(nonceVal => {
				if (nonceVal === undefined || nonceVal === '0x') {
					nonce = -1;
				} else {
					nonce = +nonceVal;
				}
			}).catch(err => {
				console.error("error fetching nonce at " + tokenAddr + ": "
							+ err);
			});
		return nonce;
	};

	// obtain the deadline as last block timestamp + 3 minutes for time to sign
	const getDeadline = async () => {
		var deadline = 0;
		await web3.eth.getBlock("pending")
			.then(block => {
				const tStamp = block.timestamp + 180;
				/* NB: on some test chains (such as Ganache) new blocks are not
				 * generated absent new transactions, so this timestamp might
				 * be in the past.  If so take current time + 3 minutes instead.
				 */
				const nowSecs = new Date().getTime() / 1000;
				const altTstamp = parseInt(nowSecs) + 180;
				// return whichever is later
				deadline = Math.max(tStamp, altTstamp);
			})
		return deadline;
	};

	// submit additional stake
	async function addToStaking(resolve, reject) {
		if (!props.isAuth) {
			let authErr = new Error("This MVO staking record does not belong "
									+ "to you, cannot add tokens to it");
			alert(authErr.message);
			reject(authErr);
			return false;
		}

		// determine whether anything changed
		if (stakeIncrement.lte(0)) {
			let errRet = new Error("No staking increment to add, enter a "
									+ "positive number");
			alert(errRet.message);
			reject(errRet);
			return false;
		}

		// determine if it's more than they have
		if (stakeIncrement.gt(props.userBal)) {
			let amtErr = new Error("More than you have available, max "
									+ web3.utils.fromWei(props.userBal));
			alert(amtErr.message);
			reject(amtErr);
			return false;
		}

		// obtain the user's nonce on EnshroudToken contract
		const userNonce = await getNonce();

		// obtain the deadline as last block timestamp (+180 seconds of slop)
		const deadline = await getDeadline();

		// prepare EIP-712 signature for permit
		var userSig = '';
		// define eth_signTypedData_v4 parameters
		var msgParams = JSON.stringify({
			types: {
				// the domain the contract is hosted on
				EIP712Domain: [
					{ name: 'name', type: 'string' },
					{ name: 'version', type: 'string' },
					{ name: 'chainId', type: 'uint256' },
					{ name: 'verifyingContract', type: 'address' },
				],
				// refer to primaryType
				Permit: [
					{ name: 'owner', type: 'address' },
					{ name: 'spender', type: 'address' },
					{ name: 'value', type: 'uint256' },
					{ name: 'nonce', type: 'uint256' },
					{ name: 'deadline', type: 'uint256' },
				],
			},
			primaryType: 'Permit',
			// EIP-712 domain info
			domain: {
				name: 'Enshroud Token',
				version: '1',
				chainId: chId,
				verifyingContract: tokenAddr,
			},
			// descriptive info on what's being signed and for whom
			message: {
				owner: userAcct,
				spender: mvoStakingAddr,
				value: stakeIncrement.toFixed(),
				nonce: userNonce,
				deadline: deadline,
			},
		});

		// now obtain signature on params in a EIP-712 compatible way
		const method = 'eth_signTypedData_v4';
		var params = [userAcct, msgParams];
		await web3.currentProvider.sendAsync(
			{
				method,
				params,
				from: userAcct,
			},
			async function (err, result) {
				if (err) console.dir(err);
				if (result.error) alert(result.error.message);
				if (result.error) console.error('ERROR', result.error);
				userSig = result.result;
				if (userSig === undefined) {
					let sigErr
						= new Error("Error building EIP712 signature");
					alert(sigErr.message);
					reject(sigErr);
					return false;
				}

				// split signature
				const r = userSig.slice(0, 66);
				const s = '0x' + userSig.slice(66, 130);
				const v = web3.utils.hexToNumber('0x'
												+ userSig.slice(130, 132));

				// invoke MVOStaking.stakeWithPermit()
				await MVOStakingContract.methods.stakeWithPermit(
													staking.mvoId,
													stakeIncrement.toFixed(),
													deadline,
													v,
													r,
													s)
						.send({ from: userAcct })
					.then(tx => {
						// tell the React context update worked
						const newTot
							= stakeIncrement.plus(staking.stakingQuantum);
						const stakingRec = new MVOStaking();
						stakingRec.setMVOId(staking.mvoId);
						stakingRec.config(staking);
						stakingRec.stakingQuantum = newTot.toFixed();
						props.setStaking(stakingRec);
						let tokenBal = new BigNumber(props.userBal)
										.minus(stakeIncrement);
						props.setUserTokens(tokenBal.toFixed());
						setStateIncrement(new BigNumber(0));
						resolve(true);
					})
					.catch(err => {
						alert("Error: code " + err.code + ", " + err.message);
						reject(err);
						return false;
					});
				resolve(true);
			}
		);

	/*
		// alt: perform on-chain action using ERC20 approve/transfer
		await MVOStakingContract.methods.stake(staking.mvoId,
				stakeIncrement.toFixed()).send({ from: userAcct })
			.then(tx => {
				// tell the React context update worked
				let newTot = stakeIncrement.plus(staking.stakingQuantum);
				const stakingRec = new MVOStaking();
				stakingRec.setMVOId(staking.mvoId);
				stakingRec.config(staking);
				stakingRec.stakingQuantum = newTot.toFixed();
				props.setStaking(stakingRec);
				let tokenBal = new BigNumber(props.userBal)
								.minus(stakeIncrement);
				props.setUserTokens(tokenBal.toFixed());
				setStateIncrement(new BigNumber(0));
				resolve(true);
			})
			.catch(err => {
				alert("Error: code " + err.code + ", " + err.message);
				reject(err);
				return false;
			});
	 */
		return true;
	};

	// render add staking action form
	const dispIncr = stakeIncrement.eq(0) ? ""
					: web3.utils.fromWei(stakeIncrement.toFixed());
	return (
		<Form>
			<Form.Group className="mb-3" controlId="stakingAmt">
				<Form.Label>
					Current Staking Amount (min. {minStaking}):
				</Form.Label>
				<Form.Control type="text" readOnly title={minStake}
					value={web3.utils.fromWei(staking.stakingQuantum)}
				/>
			</Form.Group>
			<Form.Group className="mb-3" controlId="tokenBal">
				<Form.Label>Tokens Available:</Form.Label>
				<Form.Control type="text" readOnly
					value={web3.utils.fromWei(props.userBal)}
				/>
			</Form.Group>
			<Form.Group className="mb-3">
				<Form.Label>
					Amount to add to stake using EIP-2612 permit:
				</Form.Label>
				<Form.Control type="text" size="8"
					onChange={handleStakingChange} placeholder="0"
					value={dispIncr} id="stakeAmount"
				/>
			</Form.Group>
			<LoadingButton variant="success"
				buttonStyle="m-3"
				buttonText="Increase Staking"
				buttonTitle="Add the increment to your staking using permit"
				buttonIcon="images/plus-lg.svg"
				netMethod={(resolve, reject) => addToStaking(resolve, reject)}
			/>
		</Form>
	);
}

/* show UI to add all locked tokens to an MVO staking
 * @param props.mvoStaking the retrieved staking record (a MVOStaking)
 * @param props.minStake minimum number of $ENSHROUD for an active staking
 * @param props.setStaking configure the staking record for an MVO Id
 * @param props.mgrTimelock the Timelock in the TimelockManager contract, if any
 * @param props.setTMTimelock configure the TimelockManager Timelock record
 * @param props.withdrawable the amount withdrawable (unlocked) from the
 * @param props.setUnlocked method to set user's unlocked Timelock tokens
 * Timelock in the TimelockManager contract, if any
 * @param props.userBal quantity of $ENSHROUD the user has available
 * @param props.setUserTokens method for setting/updating user token balance
 * @param props.setMVOTimelock set the MVOStaking contract Timelock
 */
function ActionStakeLocked(props) {
	const staking = props.mvoStaking;
	// enable use of our contracts and wallet accounts
	const { state: { accounts, contracts, web3 } } = useEth();
	const TimelockMgr = contracts["TimelockManager"];
	const userAcct = accounts[0];
	const minStaking
		= web3.utils.fromWei(new BigNumber(props.minStake).toFixed());

	// compute net total still locked
	var totalLocked = new BigNumber(props.mgrTimelock.remainingAmount)
									.minus(props.withdrawable);
	const isTransfer = userAcct !== staking.stakingAddress;
	const destAddress
		= isTransfer ? staking.stakingAddress + " (not your account!)"
					: staking.stakingAddress;
	const minStake = "Required: " + minStaking + " or more";

	// submit additional stake
	async function stakeLocked(resolve, reject) {
		// determine whether anything can be moved
		if (totalLocked.lte(0)) {
			let errRet = new Error("No locked tokens available to transfer "
									+ "to MVOStaking");
			alert(errRet.message);
			reject(errRet);
			return false;
		}

		// perform on-chain action
		await TimelockMgr.methods.withdrawTimelockToMVOStaking(
				staking.stakingAddress, staking.mvoId).send({ from: userAcct })
			.then(receipt => {
				/* Because one or more blocks may have elapsed since they were
				 * computed, we cannot trust the quantities we have.  Thus we
				 * pull the withdrawn amount from an event with the receipt.
				 * This event exists iff some unlocked tokens were withdrawn.
				 */
				var withdrawn = "0";
				const withdrawnEv = receipt.events["Withdrawn"];
				if (withdrawnEv !== undefined) {
					withdrawn = withdrawnEv.returnValues.withdrawable;
				}
				var newTLTot = new BigNumber(props.mgrTimelock.remainingAmount)
								.minus(withdrawn);
				var newStake = newTLTot.plus(staking.stakingQuantum).toFixed();

				// tell the React context update worked
				// 1. user's $ENSHROUD balance was credited with withdrawn amt
				const newBal = new BigNumber(props.userBal).plus(withdrawn);
				if (!newBal.eq(props.userBal)) {
					props.setUserTokens(newBal.toFixed());
				}
				// 2. MVO's staking now has newStake staking quantity (+TL)
				const stakingRec = new MVOStaking();
				stakingRec.setMVOId(staking.mvoId);
				stakingRec.config(staking);
				stakingRec.stakingQuantum = newStake;
				props.setStaking(stakingRec);
				// 3. configure new MVOStaking Timelock
				const newMVOTimelock = {
					totalAmount: newTLTot.toFixed(),
					remainingAmount: newTLTot.toFixed(),
					releaseStart: props.mgrTimelock.releaseStart,
					releaseEnd: props.mgrTimelock.releaseEnd
				};
				props.setMVOTimelock(newMVOTimelock);
				// 4. previous TimelockManager Timelock was deleted
				const newMgrTimelock = {
					totalAmount: "0",
					remainingAmount: "0",
					releaseStart: "0",
					releaseEnd: "0"
				};
				props.setTMTimelock(newMgrTimelock);
				// 5. withdrawable tokens likewise becomes 0
				props.setUnlocked(new BigNumber(0));
				totalLocked = new BigNumber(0);
				resolve(true);
			})
			.catch(err => {
				alert("Error: code " + err.code + ", " + err.message);
				reject(err);
				return false;
			});
		return true;
	};

	// render add locked action form
	return (
		<>
		<Card>
			<Card.Body>
				<Card.Title>Staking an MVO Using Locked Tokens</Card.Title>
				<Card.Subtitle>Rules and Implications</Card.Subtitle>
				<Card.Text>
					<br/>
					Locked $ENSHROUD tokens held in
					the <i>TimelockManager</i> contract can be staked to
					support an MVO.  This involves transferring the
					Timelock and all locked tokens to
					the <i>MVOStaking</i> contract, according to the
					following rules.
					<br/><br/>
					<ListGroup numbered>
						<ListGroup.Item>
							The number of withdrawable (unlocked) tokens in
							your Timelock is shown below.  These tokens
							will <b>NOT</b> be staked during this operation,
							but will instead get withdrawn to your address's
							$ENSHROUD balance.  (You can then add them to your
							staking separately later.)  The amount shown is
							estimated as of the current block; the actual final
							amount may differ.
						</ListGroup.Item>
						<ListGroup.Item>
							The remaining tokens which are locked will all be
							transferred to the <i>MVOStaking</i> contract,
							where they will remain locked, and continue to
							unlock according to the published linear unlocking
							schedule.  However, once transferred they will
							automatically be added to
							the staking for your MVO.  <b>All</b> of your
							locked tokens will be transferred and staked.
						</ListGroup.Item>
						<ListGroup.Item>
							You may withdraw tokens from your MVO staking as
							they unlock, provided you do not draw your total
							staking down below the minimum of {minStaking}.
						</ListGroup.Item>
						<ListGroup.Item>
							If your Timelock is owned by a different address
							than your MVO's staking address (i.e. your current
							address <i>[msg.sender]</i> is a separate account),
							then <b>be aware that this effectively
							transfers ownership of <i>ALL</i> of your locked
							$ENSHROUD to the MVO staking address.</b>  Make
							sure that you also control that staking address, or
							else are okay with giving up ownership of these
							locked tokens to whomever does control it.  Note
							that a transfer of locked tokens can only be done
							once per source address, because the entire
							Timelock is transferred, not just the tokens.
						</ListGroup.Item>
					</ListGroup>
				</Card.Text>
			</Card.Body>
		</Card>
		<br/>
		<Form>
			<Form.Group className="mb-3" controlId="stakingAmt">
				<Form.Label>
					Current Staking Amount (min. {minStaking}):
				</Form.Label>
				<Form.Control type="text" readOnly title={minStake}
					value={web3.utils.fromWei(staking.stakingQuantum)}
				/>
			</Form.Group>
			<Form.Group className="mb-3" controlId="tokenBal">
				<Form.Label>Locked Tokens Available:</Form.Label>
				<Form.Control type="text" readOnly
					value={web3.utils.fromWei(totalLocked.toFixed())}
				/>
			</Form.Group>
			<Form.Group className="mb-3" controlId="tokenWithdraw">
				<Form.Label>Unlocked Tokens to Withdraw:</Form.Label>
				<Form.Control type="text" readOnly
					value={web3.utils.fromWei(props.withdrawable.toFixed())}
				/>
			</Form.Group>
			<Form.Group className="mb-3" controlId="beneficiaryAddr">
				<Form.Label>
					MVO Staking Address to Receive Timelock:
				</Form.Label>
				<Form.Control type="text" readOnly value={destAddress} />
			</Form.Group>
			<LoadingButton variant="success"
				buttonStyle="m-3"
				buttonText="Stake All Locked"
				buttonTitle="Move Timelock and all locked tokens to staking address"
				buttonIcon="images/file-plus.svg"
				netMethod={(resolve, reject) => stakeLocked(resolve, reject)}
			/>
		</Form>
		</>
	);
}

/* show UI to reduce staking
 * @param props.mvoStaking the retrieved staking record (a MVOStaking)
 * @param props.isAuth whether user owns the staking record
 * @param props.minStake minimum number of $ENSHROUD for an active staking
 * @param props.timeLock the user's MVOStaking.Timelock record, if they have one
 * @param props.setMVOTimelock set the MVOStaking contract Timelock
 * @param props.setStaking configure the staking record for an MVO Id
 */
function ActionReduceStaking(props) {
	const staking = props.mvoStaking;
	// enable use of our contracts and wallet accounts
	const { state: { accounts, contracts, web3 } } = useEth();
	const MVOStakingContract = contracts["MVOStaking"];
	const userAcct = accounts[0];
	const minStaking
		= web3.utils.fromWei(new BigNumber(props.minStake).toFixed());
	const minStake = "Required: " + minStaking + " or more";

	// local state for decrement amount
	const [stakeDecrement, setStateDecrement] = useState(new BigNumber(0));

	// local state for current block number
	const [currBlock, setCurrBlock] = useState(0);

	// get the next block
	const getCurrBlock = async () => {
		var blockNum = 0;
		await web3.eth.getBlock("latest")
			.then(block => { blockNum = block.number })
		blockNum += 1;
		setCurrBlock(blockNum);
	};
	getCurrBlock();

	// process a change to staking increment
	const handleStakingChange = e => {
		var amt = new BigNumber(0.0);
		let inpVal = e.target.value.toString();
		if (inpVal === '') inpVal = "0.0";
		if (isNaN(inpVal)) inpVal = "0.0";
		let stakeAmtStr = inpVal;
		let stakeEthers = new BigNumber(stakeAmtStr);
		if (stakeEthers.isNegative()) {
			stakeAmtStr = "0.0";
			stakeEthers = amt;
		}
		amt = amt.plus(stakeEthers);
		const stakeWei = new BigNumber(web3.utils.toWei(amt.toString()));
		if (stakeWei.gte(0)) {
			setStateDecrement(stakeWei);
		}
	};

	/* If they have a Timelock in the MVOStaking contract, then their
	 * withdrawal is limited to the unlocked amount of tokens, as of the
	 * current block, plus any never-locked tokens they may have added using
	 * <ActionAddToStaking/>.  In this case, we need to compute the number
	 * of unlocked tokens in their Timelock, as of the current block, in the
	 * same manner that the private contract method MVOStaking._getUnlocked()
	 * does.  Otherwise they'll get a revert.
	 */
	var availUnlocked = new BigNumber(0);
	var neverLocked = new BigNumber(staking.stakingQuantum);
	const timelock = props.timeLock;
	if (timelock !== undefined && currBlock > 0) {
		if (currBlock <= timelock.releaseStart) {
			// none; availUnlocked = 0;
		}
		else if (currBlock >= timelock.releaseEnd) {
			// all
			availUnlocked = new BigNumber(timelock.totalAmount)
							.minus(timelock.remainingAmount);
		} else {
			// unlocked = (total * elapsed blocks) / total timelock duration
			let total = new BigNumber(timelock.totalAmount);
			let accrued = total.times(currBlock - timelock.releaseStart)
							.idiv(timelock.releaseEnd - timelock.releaseStart);
			// subtract any already withdrawn
			let alreadyPulled = total.minus(timelock.remainingAmount);
			availUnlocked = accrued.minus(alreadyPulled);
		}

		// this difference can only represent unlocked tokens staked separately
		neverLocked = neverLocked.minus(timelock.remainingAmount);
	}

	// submit stake removal
	async function rmFromStaking(resolve, reject) {
		if (!props.isAuth) {
			let authErr = new Error("This MVO staking record does not belong "
									+ "to you, cannot remove tokens from it");
			alert(authErr.message);
			reject(authErr);
			return false;
		}

		// determine whether anything changed
		if (stakeDecrement.lte(0)) {
			let errRet = new Error("No staking decrement to withdraw, enter a "
									+ "positive number");
			alert(errRet.message);
			reject(errRet);
			return false;
		}

		// ensure this isn't more than they have (would revert anyway)
		if (stakeDecrement.gt(staking.stakingQuantum)) {
			let amtErr = new Error("You do not have that many tokens staked");
			alert(amtErr.message);
			reject(amtErr);
			return false;
		}

		// check they aren't withdrawing unlocked (would also revert)
		const totWithdrawAvail = availUnlocked.plus(neverLocked);
		if (totWithdrawAvail.lt(stakeDecrement)) {
			let lockErr = new Error("You can only withdraw a max of "
								+ web3.utils.fromWei(totWithdrawAvail.toFixed())
									+ " unlocked tokens from your staking");
			alert(lockErr.message);
			reject(lockErr);
			return false;
		}

		// also warn if it goes below minimum
		const newTot
			= new BigNumber(staking.stakingQuantum).minus(stakeDecrement);
		if (newTot.lt(props.minStake)) {
			alert("Warning: you are reducing your staking below the minimum "
				+ "of " + minStaking + " tokens.  This will prevent your "
				+ "MVO from being utilized by users.");
			// (however, must allow it)
		}

		// perform on-chain action
		await MVOStakingContract.methods.withdraw(staking.mvoId,
												  stakeDecrement.toFixed())
				.send({ from: userAcct })
			.then(tx => {
				// tell the React context update worked
				staking.stakingQuantum = newTot.toFixed();
				const stakingRec = new MVOStaking();
				stakingRec.setMVOId(staking.mvoId);
				stakingRec.config(staking);
				stakingRec.stakingQuantum = newTot.toFixed();
				// also update MVOStaking timelock so new numbers will display
				const rmFromTL = BigNumber.min(stakeDecrement, availUnlocked);
				const newRemaining = new BigNumber(timelock.remainingAmount)
									.minus(rmFromTL);
				const updTimelock = {
					totalAmount: timelock.totalAmount,
					remainingAmount: newRemaining.toFixed(),
					releaseStart: timelock.releaseStart,
					releaseEnd: timelock.releaseEnd
				};
				props.setStaking(stakingRec);
				props.setMVOTimelock(updTimelock);
				setStateDecrement(new BigNumber(0));
				resolve(true);
			})
			.catch(err => {
				alert("Error: code " + err.code + ", " + err.message);
				reject(err);
				return false;
			});
		return true;
	};

	// render reduce staking action form
	const dispDecr = stakeDecrement.eq(0) ? ""
					: web3.utils.fromWei(stakeDecrement.toFixed());
	return (
		<Form>
			<Form.Group className="mb-3" controlId="stakingAmt">
				<Form.Label>
					Current Staking Amount (min. {minStaking}):
				</Form.Label>
				<Form.Control type="text" readOnly title={minStake}
					value={web3.utils.fromWei(staking.stakingQuantum)}
				/>
			</Form.Group>
			<Form.Group className="mb-3" controlId="unlockedAvail">
				<Form.Label>
					Withdrawable Unlocked (outside of Timelock):
				</Form.Label>
				<Form.Control type="text" readOnly
					title="Never-locked tokens available to withdraw"
					value={web3.utils.fromWei(neverLocked.toFixed())}
				/>
			</Form.Group>
			<Form.Group className="mb-3" controlId="timelockAvail">
				<Form.Label>
					Max Withdrawable From Timelock (as of block {currBlock}):
				</Form.Label>
				<Form.Control type="text" readOnly
					title="Timelocked tokens now unlocked and withdrawable"
					value={web3.utils.fromWei(availUnlocked.toFixed())}
				/>
			</Form.Group>
			<Form.Group className="mb-3" controlId="timelockCaution">
				<Form.Label>
					<b>Notice:</b> If you have timelocked tokens staked, not
					all of your displayed Current Staking Amount may be
					withdrawable at the present time.  Tokens will be withdrawn
					first from your Timelock, if any unlocked tokens are
					available.  Tokens which were never locked will then be
					withdrawn to satisfy your request.
				</Form.Label>
			</Form.Group>
			<Form.Group className="mb-3">
				<Form.Label>Amount to withdraw from stake:</Form.Label>
				<Form.Control type="text" size="8"
					onChange={handleStakingChange} placeholder="0"
					value={dispDecr} id="unstakeAmount"
				/>
			</Form.Group>
			<LoadingButton variant="warning"
				buttonStyle="m-3"
				buttonText="Reduce Staking"
				buttonTitle="Withdraw the amount from your staking"
				buttonIcon="images/patch-minus.svg"
				netMethod={(resolve, reject) => rmFromStaking(resolve, reject)}
			/>
		</Form>
	);
}

/* show UI to retrieve the value of estimated points accrued since last claim,
 * and to claim points if desired
 * @param props.mvoStaking the retrieved staking record (a MVOStaking)
 * @param props.isAuth whether user owns the staking record
 * @param props.setStaking configure the staking record for an MVO Id
 */
function ActionClaimPoints(props) {
	const staking = props.mvoStaking;
	// enable use of our contracts and wallet accounts
	const { state: { accounts, contracts, web3 } } = useEth();
	const MVOStakingContract = contracts["MVOStaking"];
	const userAcct = accounts[0];

	// local state for current data related to claimable points and values
	const [claimData, setClaimData] = useState({
		genesisBlock: 0,
		blockNumber: 0,
		availPoints: 0,
		totalPoints: 0,
		remainingYearTokens: new BigNumber(0),
		poolDuesRate: 0
	});

	// determine how many MVO points and total points exist
	const fetchPoints = async () => {
		// obtain the number of points this MVO has accrued
		let points = 0;
		if (staking !== undefined) {
			points = await MVOStakingContract.methods.pointsBalance(
														staking.stakingAddress)
													.call({ from: userAcct });
		}
		if (points !== claimData.availPoints) {
			setClaimData({...claimData, availPoints: points});
		}

		// obtain the total unclaimed points of all MVOs
		let totPoints
			= await MVOStakingContract.methods.totalPoints()
													.call({ from: userAcct });
		if (totPoints !== claimData.totalPoints) {
			setClaimData({...claimData, totalPoints: totPoints});
		}
	};
	fetchPoints();

	// get the genesis block for the MVOStaking contract
	const fetchGenesis = async () => {
		// obtain block where this contract was originally deployed (immutable)
		let genesis = await MVOStakingContract.methods.genesisBlock()
													.call({ from: userAcct });
		if (genesis !== claimData.genesisBlock) {
			setClaimData({...claimData, genesisBlock: genesis});
		}

	};
	fetchGenesis();

	// get the next block
	const getCurrBlock = async () => {
		var blockNum = 0;
		await web3.eth.getBlock("latest")
			.then(block => { blockNum = block.number })
		blockNum += 1;
		if (blockNum !== claimData.blockNumber) {
			setClaimData({...claimData, blockNumber: blockNum});
		}
	};
	getCurrBlock();

	// get the number of tokens not yet minted for claims in the current year
	const getYearTokens = async (year) => {
		let yToks = await MVOStakingContract.methods.yearToTokensRemaining(year)
													.call({ from: userAcct });
		let remainingForYear = new BigNumber(yToks);
		if (!remainingForYear.eq(claimData.remainingYearTokens)) {
			setClaimData({...claimData, remainingYearTokens: remainingForYear});
		}
	};

	// get the dues for an MVO pool (if this MVO staking is a member of one)
	const getPoolDues = async (poolId) => {
		var poolDues = 0;
		let mvoPool = await MVOStakingContract.methods.idToMVOPool(poolId)
													.call({ from: userAcct });
		// make sure pool has not been deleted
		if (mvoPool !== undefined) {
			poolDues = mvoPool.dues;
			/* don't count a pool where the beneficial owner is the user's own
			 * staking address, as we'll do two separate mints in this case
			 */
			if (mvoPool.poolAddress !== staking.stakingAddress
				&& poolDues !== claimData.poolDuesRate)
			{
				setClaimData({...claimData, poolDuesRate: poolDues});
			}
		}
	};

	/* If there are claimable points, we need to estimate how many $ENSHROUD
	 * tokens these points will be worth when claimed below.  To do this we
	 * utilize the same mechanism MVOStaking.claim() does, including dues.
	 */
	var tokenEstimate = new BigNumber(0);
	// estimate of blocks per year (assuming 7150 blocks per day on EVM chain)
	const BLOCKS_PER_YEAR = 2609750;

	// bail if some async fetches haven't finished yet
	if (claimData.genesisBlock === 0 || claimData.blockNumber === 0) {
		return;
	}

	// see if user has anything to claim
	if (claimData.availPoints > 0) {
		// compute number of blocks since contract deployment
		const blockDiff
			= Math.max(0, claimData.blockNumber - claimData.genesisBlock);

		// compute which year this is in (1 - 5 are valid, 0 is not)
		var yearIndex = 0;
		if (blockDiff > BLOCKS_PER_YEAR * 5) {
			alert("Mint period for tokens has expired, points unclaimable");
			yearIndex = -1;
		}
		else if (blockDiff <= BLOCKS_PER_YEAR) {
			yearIndex = 1;	// 5 million total in this year
		}
		else if (blockDiff > BLOCKS_PER_YEAR
				 && blockDiff <= BLOCKS_PER_YEAR * 2)
		{
			yearIndex = 2;	// 4 million total in this year
		}
		else if (blockDiff > BLOCKS_PER_YEAR * 2
				 && blockDiff <= BLOCKS_PER_YEAR * 3)
		{
			yearIndex = 3;	// 3 million total in this year
		}
		else if (blockDiff > BLOCKS_PER_YEAR * 3
				 && blockDiff <= BLOCKS_PER_YEAR * 4)
		{
			yearIndex = 4;	// 2 million total in this year
		}
		else if (blockDiff > BLOCKS_PER_YEAR * 4
				 && blockDiff <= BLOCKS_PER_YEAR * 5)
		{
			yearIndex = 5;	// 1 million total in this year
		}

		// compute the number of tokens which would be minted at this time
		if (yearIndex > 0 && yearIndex <= 5) {
			// NB: this algorithm incentivizes earlier claiming
			getYearTokens(yearIndex);
		/*
			console.debug("remainingForYear for year " + yearIndex + " = "
						+ claimData.remainingYearTokens.toFixed()
						+ " with blockDiff = " + blockDiff);
		 */
			let availToMint = claimData.remainingYearTokens.times(blockDiff)
											.idiv(BLOCKS_PER_YEAR * yearIndex);
			tokenEstimate = availToMint.times(claimData.availPoints)
												.idiv(claimData.totalPoints);

			// deal with the dues the MVO owes to their pool, if they're in one
			if (staking.memberPoolId !== 0) {
				getPoolDues(staking.memberPoolId);
			}
			if (claimData.poolDuesRate > 0) {
				let dues
					= tokenEstimate.idiv("1e18").times(claimData.poolDuesRate);
				tokenEstimate = tokenEstimate.minus(dues);
			}
		}
	}

	// method to actually claim the accrued points for the MVO
	async function claimPoints(resolve, reject) {
		if (!props.isAuth) {
			let authErr = new Error("This MVO staking record does not belong "
									+ "to you, cannot claim points");
			alert(authErr.message);
			reject(authErr);
			return false;
		}

		// determine whether anything changed
		if (claimData.availPoints <= 0 || tokenEstimate.lte(0)) {
			let errRet = new Error("Estimated value of claim is zero");
			alert(errRet.message);
			reject(errRet);
			return false;
		}

		// perform on-chain action
		await MVOStakingContract.methods.claim(staking.mvoId)
				.send({ from: userAcct })
			.then(tx => {
				// tell the React context update worked (force repaint)
				const stakingRec = new MVOStaking();
				stakingRec.setMVOId(staking.mvoId);
				stakingRec.config(staking);
				props.setStaking(stakingRec);
				resolve(true);
			})
			.catch(err => {
				alert("Error: code " + err.code + ", " + err.message);
				reject(err);
				return false;
			});
		return true;
	}

	// calculate some display values
	var tokenEstString = "0";
	if (!tokenEstimate.isNaN() && tokenEstimate.isFinite()) {
		tokenEstString = tokenEstimate.toFixed();
	}
	var duesRatePct = "0";
	if (claimData.poolDuesRate > 0) {
		let rate = new BigNumber(claimData.poolDuesRate).div("1e16");
		duesRatePct = rate.toString();
	}
	var pctOfPts = 0;
	if (claimData.totalPoints > 0) {
		let ratio = new BigNumber(claimData.availPoints)
									.div(claimData.totalPoints).times(100.0);
		pctOfPts = ratio.toString();
	}

	// render estimate point value form plus claim form
	return (
		<Form>
			<Form.Group className="mb-3" controlId="currPoints">
				<Form.Label>Current Points Accrued by Your MVO:</Form.Label>
				<Form.Control type="text" readOnly
					title="Earned from participating in user transactions"
					value={claimData.availPoints}
				/>
			</Form.Group>
			<Form.Group className="mb-3" controlId="currPoints">
				<Form.Label>Total Points Accrued by All MVOs:</Form.Label>
				<Form.Control type="text" readOnly
					title={"Your percentage: " + pctOfPts.toString() + "%"}
					value={claimData.totalPoints}
				/>
			</Form.Group>
			<Form.Group className="mb-3" controlId="pointsValue">
				<Form.Label>Estimated $ENSHROUD From Claim:</Form.Label>
				<Form.Control type="text" readOnly
					title={"As of block " + claimData.blockNumber + "; accounts for Pool dues of " + duesRatePct + "%"}
					value={web3.utils.fromWei(tokenEstString)}
				/>
			</Form.Group>
			<LoadingButton variant="success"
				buttonStyle="m-3"
				buttonText="Claim Points"
				buttonTitle="Claim these points for the estimated tokens"
				buttonIcon="images/file-arrow-down.svg"
				netMethod={(resolve, reject) => claimPoints(resolve, reject)}
			/>
		</Form>
	);
}

/* method to display a table row for a MVO mining pool
 * @param props.pool the pool's descriptor
 * @param props.selector method to indicate selection
 * @param props.currPool the pool Id the user is currently wishing to join
 */
function PoolItem(props) {
	const id = props.pool.poolId;
	const dispDues = new BigNumber(props.pool.dues).dividedBy("1e16");
	return (
		<tr align="center" valign="middle">
			<td title="select this Pool to join">
				<Form.Check id={id} name="poolRadio" type="radio"
					className="mx-auto p-2" aria-label={"select Pool Id " + id}
					onChange={(poolId) => props.selector(id)}
					selected={id === props.currPool}
				/>
			</td>
			<td>{id}</td>
			<td>{dispDues.toString()} %</td>
			<td>{props.pool.creator}</td>
		</tr>
	);
}

/* show UI to join and leave MVO mining pools
 * @param props.mvoStaking the retrieved staking record (a MVOStaking)
 * @param props.isAuth whether user owns the staking record
 * @param props.minStake minimum number of $ENSHROUD for an active staking
 * @param props.mvoPools all existing MVO pools
 * @param props.setStaking configure the staking record for an MVO Id
 */
function ActionPoolMembership(props) {
	const staking = props.mvoStaking;
	// enable use of our contracts and wallet accounts
	const { state: { accounts, contracts, web3 } } = useEth();
	const MVOStakingContract = contracts["MVOStaking"];
	const userAcct = accounts[0];
	const minStaking
		= web3.utils.fromWei(new BigNumber(props.minStake).toFixed());

	// pool user wants to join
	const [desiredPoolId, setDesiredPoolId] = useState(0);

	// prepare narrative of pool they're currently in, if any
	const inAPool = staking.memberPoolId > 0;
	const poolData
		= props.mvoPools.find((pool) => pool.poolId === staking.memberPoolId);
	var poolMembership = 'you are not a member of an MVO Pool';
	if (inAPool && poolData !== undefined) {
		const duesRate = new BigNumber(poolData.dues).dividedBy("1e16") + "%";
		poolMembership = `Pool ID ${poolData.poolId}: dues = ${duesRate.toString()}, created by ${poolData.creator}`;
	}

	// method to leave the current pool and possibly join a new one
	async function joinPool(resolve, reject) {
		if (!props.isAuth) {
			let authErr = new Error("This MVO staking record does not belong "
									+ "to you, cannot change Pool membership");
			alert(authErr.message);
			reject(authErr);
			return false;
		}

		// determine whether anything changed
		if (desiredPoolId !== 0 && desiredPoolId === staking.memberPoolId) {
			let errRet = new Error("Already a member of Pool ID "
									+ desiredPoolId);
			alert(errRet.message);
			// make the Leave button reappear
			selectPool(0);
			reject(errRet);
			return false;
		}
		if (!inAPool && desiredPoolId === 0) {
			// NB: technically they could have poolId != 0 which was deleted
			let errRet = new Error("You are not a member of a pool");
			alert(errRet.message);
			reject(errRet);
			return false;
		}

		// must be a valid active MVO with sufficient staking
		var currentlyStaked = new BigNumber(staking.stakingQuantum);
		if (currentlyStaked.lt(props.minStake) || !staking.active) {
			let checkFail = new Error("You must be an active MVO with at least " + minStaking + " tokens staked to change Pool membership");
			alert(checkFail.message);
			reject(checkFail);
			return false;
		}

		// perform on-chain action
		await MVOStakingContract.methods.updateMVOPoolMembership(desiredPoolId,
				staking.mvoId).send({ from: userAcct })
			.then(tx => {
				// tell the React context update worked (force repaint)
				const stakingRec = new MVOStaking();
				stakingRec.setMVOId(staking.mvoId);
				stakingRec.config(staking);
				stakingRec.memberPoolId = desiredPoolId;
				props.setStaking(stakingRec);
				// make the Leave button reappear
				selectPool(0);
				resolve(true);
			})
			.catch(err => {
				alert("Error: code " + err.code + ", " + err.message);
				reject(err);
				return false;
			});
		return true;
	}

	/* method to  record user's selected pool Id
	 * @param poolId the ID of the pool the user wants to join
	 */
	function selectPool(poolId) {
		if (poolId !== desiredPoolId) {
			setDesiredPoolId(poolId);
		}
	}

	var leaveButton = '';
	// show Leave button if appropriate
	if (inAPool && desiredPoolId === 0) {
		leaveButton = 
			<LoadingButton variant="warning"
				buttonStyle="m-3"
				buttonText="Leave Pool"
				buttonTitle="Depart from your current Pool without joining another one"
				buttonIcon="images/x-circle.svg"
				netMethod={(resolve, reject) => joinPool(resolve, reject)}
			/>;
	}

	// render form to leave or join pools
	return (
		<>
		<p className="text-lead">
			<b>Notice:</b> in order to deal with MVO Pool membership, you must
			be able to list all existing Pools.  This requires searching for
			Pool creation and deletion events which may be millions of blocks in
			the past.  Free public JSON-RPC endpoints (such
			as <i>mainnet.infura.io</i>) no
			longer support searching for events more than ~10000 blocks old.
			<br/><br/>
			If you do not see any pools listed here (or got errors pulling up
			your MVO staking data), then you will need to acquire access to a
			JSON-RPC endpoint with a paid API key and configure it into your
			wallet software for this blockchain, in order to use this function.
			We apologize for the inconvenience, but free access isn't what it
			used to be.  (All other MVO management functions should work fine.)
		</p>
		<br/>
		<Form>
			<Form.Group className="mb-3" controlId="poolData">
				<Form.Label>Current Pool Membership:</Form.Label>
				<Form.Control type="text" readOnly value={poolMembership} />
			</Form.Group>
			{leaveButton}
			<br/><br/>
			<Form.Group className="mb-3" controlId="newPoolId">
				<Form.Label>Specify Pool to Join:</Form.Label>
				<Table striped bordered hover responsive>
					<caption className="caption-top">
						MVO Pools Available
					</caption>
					<thead>
						<tr align="center" valign="middle" key="poolHdr">
							<th scope="col" title="Select this Pool to join">
								Select
							</th>
							<th scope="col" title="Unique Pool ID">
								Pool ID
							</th>
							<th scope="col"
								title="Dues charged by this pool, in percent">
								Dues (%)
							</th>
							<th scope="col"
								title="MVO Id that created this MVO pool">
								Pool Created By
							</th>
						</tr>
					</thead>
					<tbody>
						{props.mvoPools.map((pool) =>
							<Fragment key={"pool" + pool.poolId}>
								<PoolItem
									pool={pool}
									selector={(poolId) => selectPool(poolId)}
									currPool={desiredPoolId}
								/>
							</Fragment>
						)}
					</tbody>
				</Table>
			</Form.Group>
			<LoadingButton variant="success"
				buttonStyle="m-3"
				buttonText="Join Pool"
				buttonTitle="Leave any existing Pool and join selected one"
				buttonIcon="images/plus-circle.svg"
				netMethod={(resolve, reject) => joinPool(resolve, reject)}
			/>
		</Form>
		</>
	);
}

/* show UI to create a new MVO mining pool
 * @param props.mvoStaking the retrieved staking record (a MVOStaking)
 * @param props.isAuth whether user owns the staking record
 * @param props.minStake minimum number of $ENSHROUD for an active staking
 * @param props.mvoPools all existing MVO pools
 * @param props.setStaking configure the staking record for an MVO Id
 * @param props.setMVOPools method to register freshly fetched MVO Pool list
 */
function ActionPoolCreate(props) {
	const staking = props.mvoStaking;
	// enable use of our contracts and wallet accounts
	const { state: { accounts, contracts, web3 } } = useEth();
	const MVOStakingContract = contracts["MVOStaking"];
	const userAcct = accounts[0];
	const minStake
		= web3.utils.fromWei(new BigNumber(props.minStake).toFixed());

	// the pool Id to create
	const [crePoolId, setCrePoolId] = useState(0);

	// the dues rate for the pool
	const [poolDues, setPoolDues] = useState(new BigNumber(0));

	// process entered poolId to be created
	const handlePoolInput = e => {
		let poolId = e.target.value.toString();
		if (poolId === '') {
			poolId = '0';
		}
		if (/^[0-9]+/.test(poolId)) {
			setCrePoolId(poolId);
		}
	};

	// process entered dues rate
	const handleDuesInput = e => {
		let dues = e.target.value.toString();
		if (dues === '') dues = "0.0";
		if (isNaN(dues)) dues = "0.0";
		const weiDues = new BigNumber(dues).times("1e16");
		setPoolDues(weiDues);
	};

	// process attempt to create pool and perform on-chain action
	async function createPool(resolve, reject) {
		if (!props.isAuth) {
			let authErr = new Error("This MVO staking record does not belong "
									+ "to you, cannot create Pool");
			alert(authErr.message);
			reject(authErr);
			return false;
		}

		// make sure pool ID is valid
		if (crePoolId <= 0) {
			let badID = new Error("Illegal Pool ID, " + crePoolId);
			alert(badID.message);
			reject(badID);
			return false;
		}

		// make sure this pool ID doesn't already exist
		const existPool
			= props.mvoPools.find((pool) => pool.poolId === crePoolId);
		if (existPool !== undefined) {
			let existErr
				= new Error("Pool ID " + crePoolId + " already exists");
			alert(existErr.message);
			reject(existErr);
			return false;
		}

		// make sure dues are valid
		if (poolDues.lte(0) || poolDues.gte("5e17")) {
			let badDues = new Error("Dues must be greater than 0 and less than "
									+ "50 percent");
			alert(badDues.message);
			reject(badDues);
			return false;
		}

		// must be a valid active MVO with sufficient staking
		var currentlyStaked = new BigNumber(staking.stakingQuantum);
		if (currentlyStaked.lt(props.minStake) || !staking.active) {
			let checkFail = new Error("You must be an active MVO with at least " + minStake + " tokens staked to create an MVO Pool");
			alert(checkFail.message);
			reject(checkFail);
			return false;
		}

		// perform on-chain action
		await MVOStakingContract.methods.createMVOPool(crePoolId,
				poolDues.toFixed(), staking.mvoId).send({ from: userAcct })
			.then(tx => {
				// tell the React context update worked (change membership)
				const stakingRec = new MVOStaking();
				stakingRec.setMVOId(staking.mvoId);
				stakingRec.config(staking);
				stakingRec.memberPoolId = crePoolId;
				const newList = [].concat(props.mvoPools);
				const newPoolRec = { poolId: crePoolId,
									 dues: poolDues.toFixed(),
									 creator: staking.mvoId };
				newList.push(newPoolRec);
				props.setStaking(stakingRec);
				props.setMVOPools(newList);
				resolve(true);
			})
			.catch(err => {
				alert("Error: code " + err.code + ", " + err.message);
				reject(err);
				return false;
			});
		return true;
	}

	// render form to create a pool
	return (
		<>
		<p className="text-lead">
			This will initialize a new MVO Pool controlled by you, with your
			own MVO as the sole initial member.  Other MVO operators will then
			be able to join it, and dues paid by them during points claims will
			be paid to your MVO staking address.
			<br/><br/>
			Your MVO must be active with at least {minStake} tokens
			staked in order to create a Pool.  The number chosen must be unique.
			(See the list of existing pools in the MVO Pool Membership section.)
		</p>
		<br/>
		<Form>
			<Form.Group className="mb-3">
				<Form.Label>Desired Pool ID:</Form.Label>
				<Form.Control type="text" size="8" maxLength="12" id="poolId"
					title="Must be >0 and unique (not found in MVO Pool Membership list above)"
					onChange={handlePoolInput} value={crePoolId}
				/>
			</Form.Group>
			<Form.Group className="mb-3">
				<Form.Label>Dues Charged to Members (0-50%):</Form.Label>
				<Form.Control type="text" size="8" maxLength="12"
					title="In percent, must be > 0% and < 50%"
					value={poolDues.div("1e16").toString()}
					onChange={handleDuesInput} id="poolDues"
				/>
			</Form.Group>
			<LoadingButton variant="success"
				buttonStyle="m-3"
				buttonText="Create Pool"
				buttonTitle="Create the specified pool"
				buttonIcon="images/file-plus.svg"
				netMethod={(resolve, reject) => createPool(resolve, reject)}
			/>
		</Form>
		</>
	);
}

/* show UI to remove an existing MVO mining pool
 * @param props.mvoStaking the retrieved staking record (a MVOStaking)
 * @param props.setStaking configure the staking record for an MVO Id
 * @param props.isAuth whether user owns the staking record
 * @param props.mvoPools all existing MVO pools
 * @param props.setMVOPools method to register freshly fetched MVO Pool list
 */
function ActionPoolDelete(props) {
	const staking = props.mvoStaking;
	// enable use of our contracts and wallet accounts
	const { state: { accounts, contracts } } = useEth();
	const MVOStakingContract = contracts["MVOStaking"];
	const userAcct = accounts[0];

	// the pool Id to delete
	const [delPoolId, setDelPoolId] = useState(0);

	// process entered poolId to be created
	const handlePoolInput = e => {
		let poolId = e.target.value.toString();
		if (poolId === '') {
			poolId = '0';
		}
		if (/^[0-9]+/.test(poolId)) {
			setDelPoolId(poolId);
		}
	};

	// process attempt to delete pool and perform on-chain action
	async function deletePool(resolve, reject) {
		if (!props.isAuth) {
			let authErr = new Error("This MVO staking record does not belong "
									+ "to you, cannot delete Pool");
			alert(authErr.message);
			reject(authErr);
			return false;
		}

		// make sure this pool ID already exists
		const existPool
			= props.mvoPools.find((pool) => pool.poolId === delPoolId);
		if (existPool === undefined) {
			let existErr
				= new Error("Pool ID " + delPoolId + " does not exist");
			alert(existErr.message);
			reject(existErr);
			return false;
		}

		// make sure user owns this pool as well as the staking
		if (existPool.creator !== staking.mvoId) {
			let notOwner = new Error("Pool ID " + delPoolId
									+ " not owned by you, cannot delete");
			alert(notOwner.message);
			reject(notOwner);
			return false;
		}

		// perform on-chain action
		await MVOStakingContract.methods.deleteMVOPool(delPoolId, staking.mvoId)
				.send({ from: userAcct })
			.then(tx => {
				// tell the React context update worked (trigger repaint)
				const stakingRec = new MVOStaking();
				stakingRec.setMVOId(staking.mvoId);
				stakingRec.config(staking);
				// NB: caller pool membership doesn't actually change on-chain
				stakingRec.memberPoolId = 0;
				props.setStaking(stakingRec);
				// remove the deleted pool from the list
				const purgedList = props.mvoPools.filter((pool) =>
													pool.poolId !== delPoolId);
				props.setMVOPools(purgedList);
				resolve(true);
			})
			.catch(err => {
				alert("Error: code " + err.code + ", " + err.message);
				reject(err);
				return false;
			});
		return true;
	}

	// render form to delete a pool
	return (
		<>
		<p className="text-lead">
			This will delete an MVO Pool controlled by you.  After this, dues
			will no longer be paid by members when claiming points.  Enter a
			Pool ID shown in the list in the table in the MVO Pool Membership
			section above.  (You must have created the Pool in order to delete
			it.)
		</p>
		<br/>
		<Form>
			<Form.Group className="mb-3">
				<Form.Label>Pool ID to delete:</Form.Label>
				<Form.Control type="text" size="8" maxLength="12" id="delPoolId"
					title="Must be found in MVO Pool Membership list above"
					onChange={handlePoolInput} value={delPoolId}
				/>
			</Form.Group>
			<LoadingButton variant="success"
				buttonStyle="m-3"
				buttonText="Delete Pool"
				buttonTitle="Remove the indicated Pool"
				buttonIcon="images/file-x.svg"
				netMethod={(resolve, reject) => deletePool(resolve, reject)}
			/>
		</Form>
		</>
	);
}

/* show the action options for the user (MVO owner) in an accordion
 * @param props.mvoStaking the retrieved staking record (a MVOStaking)
 * @param props.isAuth whether user owns the staking record
 * @param props.userBal quantity of $ENSHROUD the user has available
 * @param props.setUserTokens method for setting/updating user token balance
 * @param props.minStake minimum number of $ENSHROUD for an active staking (wei)
 * @param props.setStaking configure the staking record for an MVO Id
 * @param props.timeLock the Timelock record of the user in MVOStaking, if any
 * @param props.setMVOTimelock set the MVOStaking contract Timelock
 * @param props.mgrTimelock the Timelock for the user in TimelockManager
 * @param props.setTMTimelock set the Timelock for the user in TimelockManager
 * @param props.withdrawable number of unlocked tokens in TimelockManager (wei)
 * @param props.setUnlocked method to set user's unlocked Timelock tokens
 * @param props.mvoPools the array of MVO pools created but never deleted
 * @param props.setMVOPools method to register freshly fetched MVO Pool list
 */
function MVOActions(props) {
	const staking = props.mvoStaking;

	// show according iff we have valid staking
	var actionsList = <br/>;
	if (staking !== undefined && staking.signingAddress
							!== '0x0000000000000000000000000000000000000000')
	{
		if (!props.isAuth) {
			actionsList =
			<>
				<hr/>
				<h3>Possible Staking Actions</h3>
				<br/>
				<h4>You do not own this staking record, actions restricted</h4>
				<br/>
				<Accordion>
					<Accordion.Item eventKey="0">
						<Accordion.Header>
							Stake All Locked Tokens
						</Accordion.Header>
						<Accordion.Body>
							<ActionStakeLocked
								mvoStaking={staking}
								minStake={props.minStake}
								setStaking={props.setStaking}
								mgrTimelock={props.mgrTimelock}
								setTMTimelock={props.setTMTimelock}
								withdrawable={props.withdrawable}
								setUnlocked={props.setUnlocked}
								userBal={props.userBal}
								setUserTokens={props.setUserTokens}
								setMVOTimelock={props.setMVOTimelock}
							/>
						</Accordion.Body>
					</Accordion.Item>
				</Accordion>
				<br/>
			</>;
		}
		else {
			actionsList =
			<>
				<hr/>
				<br/>
				<h3>Possible Staking Actions</h3>
				<h4>Expand Desired Section Below</h4>
				<br/>
				<Accordion alwaysOpen>
					<Accordion.Item eventKey="0">
						<Accordion.Header>Add to Staking</Accordion.Header>
						<Accordion.Body>
							<p className="text-muted">
								(unlocked whole tokens only)
							</p>
							<br/>
							<ActionAddToStaking
								mvoStaking={staking}
								isAuth={props.isAuth}
								userBal={props.userBal}
								setUserTokens={props.setUserTokens}
								minStake={props.minStake}
								setStaking={props.setStaking}
							/>
						</Accordion.Body>
					</Accordion.Item>
					<Accordion.Item eventKey="1">
						<Accordion.Header>
							Stake All Locked Tokens
						</Accordion.Header>
						<Accordion.Body>
							<ActionStakeLocked
								mvoStaking={staking}
								minStake={props.minStake}
								setStaking={props.setStaking}
								mgrTimelock={props.mgrTimelock}
								setTMTimelock={props.setTMTimelock}
								withdrawable={props.withdrawable}
								setUnlocked={props.setUnlocked}
								userBal={props.userBal}
								setUserTokens={props.setUserTokens}
								setMVOTimelock={props.setMVOTimelock}
							/>
						</Accordion.Body>
					</Accordion.Item>
					<Accordion.Item eventKey="2">
						<Accordion.Header>Reduce Staking</Accordion.Header>
						<Accordion.Body>
							<ActionReduceStaking
								mvoStaking={staking}
								isAuth={props.isAuth}
								minStake={props.minStake}
								timeLock={props.timeLock}
								setMVOTimelock={props.setMVOTimelock}
								setStaking={props.setStaking}
							/>
						</Accordion.Body>
					</Accordion.Item>
					<Accordion.Item eventKey="3">
						<Accordion.Header>
							View and Claim Accrued Points
						</Accordion.Header>
						<Accordion.Body>
							<ActionClaimPoints
								mvoStaking={staking}
								isAuth={props.isAuth}
								setStaking={props.setStaking}
							/>
						</Accordion.Body>
					</Accordion.Item>
					<Accordion.Item eventKey="4">
						<Accordion.Header>MVO Pool Membership</Accordion.Header>
						<Accordion.Body>
							<ActionPoolMembership
								mvoStaking={staking}
								isAuth={props.isAuth}
								minStake={props.minStake}
								mvoPools={props.mvoPools}
								setStaking={props.setStaking}
							/>
						</Accordion.Body>
					</Accordion.Item>
					<Accordion.Item eventKey="5">
						<Accordion.Header>Create MVO Pool</Accordion.Header>
						<Accordion.Body>
							<ActionPoolCreate
								mvoStaking={staking}
								isAuth={props.isAuth}
								minStake={props.minStake}
								mvoPools={props.mvoPools}
								setStaking={props.setStaking}
								setMVOPools={props.setMVOPools}
							/>
						</Accordion.Body>
					</Accordion.Item>
					<Accordion.Item eventKey="6">
						<Accordion.Header>Delete MVO Pool</Accordion.Header>
						<Accordion.Body>
							<ActionPoolDelete
								mvoStaking={staking}
								setStaking={props.setStaking}
								isAuth={props.isAuth}
								mvoPools={props.mvoPools}
								setMVOPools={props.setMVOPools}
							/>
						</Accordion.Body>
					</Accordion.Item>
				</Accordion>
				<br/>
			</>;
		}
	}

	// render all available actions
	return (
		<div id="mvoActions">
			{ actionsList }
		</div>
	);
}

/* main UI method to manage staking and other settings for a MVO node
 * @param props.userTokens the number of $ENSHROUD tokens the user owns (wei)
 * @param props.setUserTokens method for setting/updating user token balance
 * @param props.mvoStaking the retrieved staking record for the MVO Id
 * @param props.setStaking configure the retireved staking record for an MVO Id
 * @param props.isAuth whether the current user owns the staking record
 * @param props.setAuth set whether the current user is the owner
 * @param props.minStaking the minimum number of tokens for staking on chain
 * @param props.setMinStaking method to set min staking quantity
 * @param props.timeLock the Timelock for the user in MVOStaking contract
 * @param props.setMVOTimelock set the MVOStaking contract Timelock
 * @param props.mgrTimelock the Timelock for the user in TimelockManager
 * @param props.setTMTimelock set the Timelock for the user in TimelockManager
 * @param props.withdrawable number of unlocked tokens in TimelockManager (wei)
 * @param props.setUnlocked set the number of withdrawable TimelockManager toks
 */
function MVOManagement(props) {
	// enable use of our contracts and wallet accounts
	const { state: { accounts } } = useEth();
	const userAcct = accounts[0];

	// local aliases of passed properties
	var userTokens = props.userTokens;
	var mvoStaking = props.mvoStaking;
	var haveAuth = props.isAuth;

	// state storage for MVOId entry field
	const [mvoId, setMVOId] = useState('');

	// MVO Pools which may exist
	const [mvoPools, setMvoPools] = useState([]);

	/* method to record new set of fetched MVO Pool records
	 * param pools list of pools (fresh array, no need to merge)
	 */
	function handleNewPoolList(pools) {
		setMvoPools(mvoPools => ([...pools]));
	}

	/* determine whether this user (accounts[0]) owns the selected staking
	 * @param staking the MVO staking record
	 */
	function authToUpdate(staking) {
		let stakingAddr = "";
		if (mvoStaking !== undefined) stakingAddr = mvoStaking.stakingAddress;
		const isAuth = stakingAddr === userAcct;
		if (isAuth !== props.isAuth) {
			haveAuth = isAuth;
		}
	};
	authToUpdate();

	// do actual rendering of sections for possible actions
	return (
		<div className="enshMVOManagement">
			<h2 align="center">
				MVO Staking Management (for L2 node owners/operators)
			</h2>
			<br/><br/>
			<h3 align="center">Managing Your MVO Staking</h3>
			<GetMVOStaking
				mvoId={mvoId}
				setMVOId={setMVOId}
				setStaking={props.setStaking}
				setAuth={props.setAuth}
				setUserTokens={props.setUserTokens}
				setMVOTimelock={props.setMVOTimelock}
				mgrTimelock={props.mgrTimelock}
				setTMTimelock={props.setTMTimelock}
				withdrawable={props.withdrawable}
				setUnlocked={props.setUnlocked}
				minStaking={props.minStaking}
				setMinStaking={props.setMinStaking}
				setMVOPools={handleNewPoolList}
			/>
			<br/>
			<MVOStakingRenderer
				mvoStaking={mvoStaking}
				user={userAcct}
			/>
			<br/>
			<MVOActions
				mvoStaking={mvoStaking}
				isAuth={haveAuth}
				userBal={userTokens}
				setUserTokens={props.setUserTokens}
				minStake={props.minStaking}
				setStaking={props.setStaking}
				timeLock={props.timeLock}
				setMVOTimelock={props.setMVOTimelock}
				mgrTimelock={props.mgrTimelock}
				setTMTimelock={props.setTMTimelock}
				withdrawable={props.withdrawable}
				setUnlocked={props.setUnlocked}
				mvoPools={mvoPools}
				setMVOPools={handleNewPoolList}
			/>
			<br/>
		</div>
	);

}

export default MVOManagement;
