/*
 * last modified---
 * 	05-07-25 TEMP: add call to DAOPool.importFees() for testing
 * 	04-17-25 prevent claim attempts when Epoch requirement is not met
 * 	04-14-25 show USD value in <SelectableClaim/> if we have a price feed avail
 *  01-15-24 debugging
 * 	12-30-24 complete draft
 * 	12-04-24 new (skeleton)
 *
 * purpose---
 *	provide UI/UX to manage a user's staking in the DAOPool contract
 */

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 InputGroup from 'react-bootstrap/InputGroup';
import Image from 'react-bootstrap/Image';
import Badge from 'react-bootstrap/Badge';
import Container from 'react-bootstrap/Container';
import LoadingButton from '../LoadingButton.jsx';
import ChainLinkPriceFeed from '../priceFeeds.js';
const BigNumber = require("bignumber.js");


/* method to obtain the user's deposited and staked shares, timelocks, etc.
 * @param props.setUserTokens method for setting/updating user token balance
 * @param props.setUserData method for setting/updating user's User{} struct
 * @param props.setDAOTimelock set the DAOPool 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
 * @param props.setStakedShares set the number of staked shares
 * @param props.setTotalStake set the number of total staked tokens
 * @param props.setEpochLength set number of blocks needed to fulfill an Epoch
 */
function GetDAOStaking(props) {
	// enable use of our contracts and accounts
	const { state: { contracts, accounts, web3 } } = useEth();
	const userAcct = accounts[0];

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

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

	// method to 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);
	};

	// method to obtain user's User{} struct from contract, .deposited etc.
	const fetchUserStats = async () => {
		let userStruct = await DaoPoolContract.methods.users(userAcct)
													.call( { from: userAcct });
		if (userStruct !== undefined) {
			// might be all 0
			props.setUserData(userStruct);
		}
	};

	// obtain user's DAOStaking timelock balance (if any)
	const fetchUserTimelock = async () => {
		let timelock = await DaoPoolContract.methods.userToTimelock(userAcct)
													.call( { from: userAcct });
		if (timelock !== undefined) {
			// might be all zeros
			props.setDAOTimelock(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) {
			// might be all zeros
			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 user's shares staked in DAOPool (if any), and total shares
	const fetchShares = async () => {
		// need latest block first
		const blockNow = await getCurrBlock();
		const userShares = await DaoPoolContract.methods.userSharesAt(userAcct,
																	  blockNow)
			.call({ from: userAcct });
		if (userShares !== undefined) {
			let uShares = new BigNumber(userShares);
			props.setStakedShares(uShares);
		}
		const totalShares
			= await DaoPoolContract.methods.totalSharesAt(blockNow)
													.call({ from: userAcct });
		if (totalShares !== undefined) {
			let totShares = new BigNumber(totalShares);
			props.setTotalShares(totShares);
		}
	};

	// obtain total staked $ENSHROUD in DAOPool
	const fetchStaked = async () => {
		const totStaked = await DaoPoolContract.methods.totalStake()
			.call({ from: userAcct });
		if (totStaked !== undefined) {
			// deduct the 1 wei seeded by the DAOPool constructor
			let totStake = new BigNumber(totStaked).minus(1);
			props.setTotalStake(totStake);
		}
	};

	// obtain number of blocks that constitute an Epoch on this blockchain
	const fetchEpoch = async () => {
		const epochLen = await DaoPoolContract.methods.BLOCKS_PER_EPOCH()
			.call({ from: userAcct });
		if (epochLen !== undefined) {
			props.setEpochLength(epochLen);
		}
	};

	// perform actual lookup of DAOPool data for user
	const fetchDAOrecords = async () => {
		// obtain user's current balance of $ENSHROUD tokens
		await fetchUserBalance();

		// obtain user's User{} data, if any
		await fetchUserStats();

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

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

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

		// fetch user's staked shares and total shares in DAOPool + total shares
		await fetchShares();

		// fetch total $ENSHROUD tokens staked in DAOPool
		await fetchStaked();

		// fetch length of an Epoch
		await fetchEpoch();
	};

	// button to trigger fetches
	const daoFetcher = 
		<>
			<br/><br/>
			<Form>
				<Form.Group className="mb-3" controlId="daoData">
					<Form.Label>Click to refresh your data:</Form.Label>
					<Button variant="info" className="m-3"
						title="Obtain the latest data on your DAOPool information"
						onClick={() => fetchDAOrecords()}
					>
						Get/Refresh DAO Pool Info
					</Button>
				</Form.Group>
			</Form>
			<p className="text-muted" align="left">
				(Do this before attempting any of the Possible Actions below.)
			</p>
		</>;

	// render button
	return (
		<div id="daoGet">
			{ daoFetcher }
		</div>
	);
}

/* render the DAOPool staking data for the user as a Nx2 form of name/value
 * pairs (read-only)
 * @param props.user the owning account
 * @param props.userTokens total balance of $ENSHROUD tokens user has
 * @param props.userData the user's User{} struct data from DAOPool
 * @param props.setUserData method for setting/updating user's User{} struct
 * @param props.timeLock Timelock record of user in DAOPool (if any)
 * @param props.setDAOTimelock set the DAOPool contract Timelock
 * @param props.mgrTimelock Timelock record of user in TimelockManager (if any)
 * @param props.withdrawable number of tokens user can withdraw from TimelockMgr
 * @param props.stakedShares number of tokens user has staked in DAOPool
 * @param props.totalShares number of tokens user has staked in DAOPool
 * @param props.unlockableTokens number of tokens unlockable in DAOPool Timelock
 * @param props.setUnlockableTokens set unlockable DAOPool Timelock tokens
 */
function DAOStakingRenderer(props) {
	const { state: { contracts, accounts, web3 } } = useEth();
	const timelock = props.timeLock;
	const DaoPoolContract = contracts["DAOPool"];
	const userAcct = accounts[0];
	const uData = props.userData;

	// 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();

	// method to run updateTimelockStatus() in DAOPool contract
	async function updateTimelock(resolve, reject) {
		// make sure there are some tokens to unlock
		if (props.unlockableTokens.lte(0)) {
			let noUnlock = new Error("You don't have any tokens which would "
									+ "unlock in your Timelock at this time");
			alert(noUnlock.message);
			reject(noUnlock);
			return false;
		}

		// invoke contract method
		await DaoPoolContract.methods.updateTimelockStatus(userAcct)
				.send( { from: userAcct })
			.then(receipt => {
				/* Adjust timelock stats.  We'll try to pull the
				 * UpdatedTimelock event from the transaction receipt and
				 * reset the timelock.remainingAmount based on the 3rd param.
				 * Failing this, we'll calculate it based on the estimate.
				 * (The user could also click the "Get/Refresh DAO Pool Info"
				 * button above to correct any discrepancy.)
				 */
				let remainingAmt = "0";
				const newRemaining = receipt.events["UpdatedTimelock"]
									.returnValues.userUnlocking;
				if (newRemaining === undefined) {
					// compute estimate based on existing data
					let updRemaining = new BigNumber(timelock.remainingAmount)
										.minus(props.unlockableTokens);
					remainingAmt = updRemaining.toFixed();
				} else {
					remainingAmt = newRemaining;
				}
				const updTimelock = {
					totalAmount: timelock.totalAmount,
					remainingAmount: remainingAmt,
					releaseStart: timelock.releaseStart,
					releaseEnd: timelock.releaseEnd
				};
				props.setDAOTimelock(updTimelock);

				// also adjust deposited balance in User{}
				const amtUnlocked = receipt.events["UpdatedTimelock"]
									.returnValues.amount;
				const newDepTot
					= new BigNumber(amtUnlocked).plus(uData.deposited);
				const updUser = {
					shares: uData.shares,
					delegatedTo: uData.delegatedTo,
					delegates: uData.delegates,
					deposited: newDepTot.toFixed(),
					unstakeAmount: uData.unstakeAmount,
					unstakeShares: uData.unstakeShares,
					unstakeScheduledFor: uData.unstakeScheduledFor,
					lastDelegationUpdateTimestamp:
						uData.lastDelegationUpdateTimestamp
				};
				props.setUserData(updUser);
				resolve(true);
			})
			.catch (err => {
				alert("Error: code " + err.code + ", " + err.message);
				reject(err);
				return false;
			});
		return true;
	};

	// build section for scheduled unstaking
	var unstakeDetails = <br/>;
	if (uData.unstakeScheduledFor > 0) {
		const vestDate = new Date(+uData.unstakeScheduledFor * 1000).toString();
		const shareAmt = new BigNumber(web3.utils.fromWei(uData.unstakeShares));
		const tokenAmt = new BigNumber(web3.utils.fromWei(uData.unstakeAmount));
		unstakeDetails = 
		<>
			{ /* shares scheduled for unstaking */ }
			<Form.Group className="mb-3" controlId="schedShares">
				<Form.Label>Shares scheduled to unstake:</Form.Label>
				<Form.Control type="text" readOnly
					value={shareAmt.sd(6).toFixed()}
				/>
			</Form.Group>
			{ /* est. tokens scheduled for unstaking */ }
			<Form.Group className="mb-3" controlId="schedTokens">
				<Form.Label>
					Tokens scheduled to unstake (estimated):
				</Form.Label>
				<Form.Control type="text" readOnly
					value={tokenAmt.sd(6).toFixed()}
				/>
			</Form.Group>
			{ /* timestamp at which scheduled unstake becomes valid */ }
			<Form.Group className="mb-3" controlId="schedDate">
				<Form.Label>
					Earliest date at which unstake becomes available:
				</Form.Label>
				<Form.Control type="text" readOnly value={vestDate} />
			</Form.Group>
		</>;
	}

	// build section for DAOPool Timelock details
	var timelockDetails = '';
	if (timelock.remainingAmount > 0) {
		// estimate unlocking amount (see DAOPool.updateTimelockStatus() logic)
		var unlocking = "0";
		if (currBlock > timelock.releaseStart) {
			if (currBlock > timelock.releaseEnd) {
				unlocking = timelock.remainingAmount;
			} else {
				let passedBlocks = currBlock - timelock.releaseStart;
				let totalBlocks = timelock.releaseEnd - timelock.releaseStart;
				let blockPct = new BigNumber(passedBlocks).div(totalBlocks);
				let totUnlockedNow = new BigNumber(timelock.totalAmount)
										.times(blockPct);
				let pastUnlocked = new BigNumber(timelock.totalAmount)
										.minus(timelock.remainingAmount);
				let newlyUnlocked = totUnlockedNow.minus(pastUnlocked);
				unlocking = newlyUnlocked.sd(6).toFixed();
			}
		}
		// NB: doing it this way allows us to update as blocks pass
		if (!props.unlockableTokens.eq(unlocking)) {
			props.setUnlockableTokens(new BigNumber(unlocking));
		}

		// fill in actual DAOPool Timelock details
		timelockDetails =
		<>
			{ /* locked $ENSHROUD */ }
			<Form.Group className="mb-3" controlId="locked">
				<Form.Label>
					Timelocked $ENSHROUD in DAOPool (cannot be staked):
				</Form.Label>
				<Form.Control type="text" readOnly
					value={web3.utils.fromWei(timelock.remainingAmount)}
				/>
			</Form.Group>
			{ /* approximate amount that would unlock as of current block */ }
			<Form.Group className="mb-3" controlId="unlocked">
				<Form.Label>
					Unlockable $ENSHROUD as of next block, {currBlock} (can be moved to deposited and then staked):
				</Form.Label>
				<Form.Control type="text" readOnly
					value={web3.utils.fromWei(unlocking)}
				/>
			</Form.Group>

			{ /* supply button to call updateTimelockStatus() */ }
			<LoadingButton variant="primary"
				buttonStyle="m-3"
				buttonText="Update Timelock (Unlock Tokens)"
				buttonTitle="Unlock available tokens and move them to deposited total"
				netMethod={(resolve, reject) => updateTimelock(resolve, reject)}
				buttonIcon="images/key.svg"
			/>
		</>;
	}

	// build section for TimelockManager Timelock details
	var mgrTimelockDetails = '';
	if (props.mgrTimelock.remainingAmount > 0) {
		// compute amount of tokens that will still be locked
		let tlLocked = new BigNumber(props.mgrTimelock.remainingAmount)
						.minus(props.withdrawable).sd(6);

		// fill in actual TimelockManager Timelock details
		mgrTimelockDetails =
		<>
			{ /* amount of currently locked tokens */ }
			<Form.Group className="mb-3" controlId="stillLocked">
				<Form.Label>
					Timelocked $ENSHROUD in TimelockManager (cannot be staked):
				</Form.Label>
				<Form.Control type="text" readOnly
					value={web3.utils.fromWei(tlLocked.toFixed())}
				/>
			</Form.Group>
			{ /* amount of currently withdrawable tokens */ }
			<Form.Group className="mb-3" controlId="withdrawable">
				<Form.Label>
					Unlocked $ENSHROUD (can be Withdrawn or added to Deposited):
				</Form.Label>
				<Form.Control type="text" readOnly
					value={web3.utils.fromWei(props.withdrawable.toFixed())}
				/>
			</Form.Group>
		</>;
	}

	// calculate percentage user's shares represents out of pool total
	var sharePct = props.stakedShares.dividedBy(props.totalShares).times(100);

	// build set of all details (either Timelock can exist, but not both)
	var userDetails =
		<>
			<hr/>
			<br/>
			<h4>Your DAOPool Staking Details</h4>
			<h5>
				Estimated As of Next Block ({currBlock})<br/>
				(Click Get/Refresh button above to update.)
			</h5>
			<br/>
			<Form>
				{ /* user EOA echo */ }
				<Form.Group className="mb-3" controlId="user">
					<Form.Label>Account:</Form.Label>
					<Form.Control type="text" readOnly value={props.user} />
				</Form.Group>
				{ /* total $ENSHROUD balance */ }
				<Form.Group className="mb-3" controlId="balance">
					<Form.Label>$ENSHROUD Balance:</Form.Label>
					<Form.Control type="text" readOnly
						value={web3.utils.fromWei(props.userTokens)}
					/>
				</Form.Group>
				{ /* $ENSHROUD deposited in DAOPool */ }
				<Form.Group className="mb-3" controlId="deposited">
					<Form.Label>$ENSHROUD Deposited with DAOPool:</Form.Label>
					<Form.Control type="text" readOnly
						value={web3.utils.fromWei(uData.deposited)}
					/>
				</Form.Group>

				{ /* include TimelockManager Timelock details, if any */ }
				{ mgrTimelockDetails }

				{ /* include DAOPool Timelock details, if any */ }
				{ timelockDetails }

				{ /* shares staked by user */ }
				<Form.Group className="mb-3" controlId="shares">
					<Form.Label>Your staked shares in pool:</Form.Label>
					<Form.Control type="text" readOnly
						value={web3.utils.fromWei(props.stakedShares.sd(6).toFixed())}
					/>
				</Form.Group>

				{ /* total shares in pool */ }
				<Form.Group className="mb-3" controlId="totShares">
					<Form.Label>Total shares in pool:</Form.Label>
					<Form.Control type="text" readOnly
						value={web3.utils.fromWei(props.totalShares.sd(6).toFixed())}
					/>
				</Form.Group>

				{ /* percentage user has in pool */ }
				<Form.Group className="mb-3" controlId="pctShares">
					<Form.Label>Your share percentage:</Form.Label>
					<Form.Control type="text" readOnly
						value={sharePct.toString() + "%"}
					/>
				</Form.Group>

				{ /* include scheduled unstaking details, if any */ }
				{ unstakeDetails }
			</Form>
		</>;

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

/* show UI to deposit more tokens into DAOPool
 * @param props.userTokens number of $ENSHROUD tokens user has available
 * @param props.setUserTokens update the number of tokens available
 * @param props.userData the User{} struct, including .deposited value
 * @param props.setUserData method for setting/updating user deposited balance
 */
function ActionDeposit(props) {
	// enable use of contracts and wallet accounts
	const { state: { accounts, contracts, web3, chainConn } } = useEth();

	const userAcct = accounts[0];
	const DaoPoolContract = contracts["DAOPool"];
	const daoPoolAddress = DaoPoolContract.options.address;
	const EnshroudTokenContract = contracts["EnshroudToken"];
	const tokenAddr = EnshroudTokenContract.options.address;
	const chId = chainConn.chainConfig.chainId;

	// local state for deposit amount
	const [depositIncrement, setDepositIncrement] = useState(new BigNumber(0));

	// process a change to deposit increment
	const handleDepositChange = 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 depAmtStr = inpVal;
		let depAmt = new BigNumber(depAmtStr);
		if (depAmt.isNegative()) {
			depAmtStr = "0.0";
			depAmt = amt;
		}
		amt = amt.plus(depAmt);
		const depWei = amt.times("1e18");
		if (depWei.gte(0)) {
			setDepositIncrement(depWei);
		}
	};

	// 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 deposit
	async function depositToPool(resolve, reject) {
		// determine whether anything changed
		if (depositIncrement.lte(0)) {
			let errRet = new Error("No deposit increment to add, enter a "
									+ "positive number");
			alert(errRet.message);
			reject(errRet);
			return false;
		}

		// determine if it's more than they have
		if (depositIncrement.gt(props.userTokens)) {
			let amtErr = new Error("More than you have available, max "
									+ web3.utils.fromWei(props.userTokens));
			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: daoPoolAddress,
				value: depositIncrement.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 DAOPool.depositWithPermit()
				await DaoPoolContract.methods.depositWithPermit(
													depositIncrement.toFixed(),
													deadline,
													v,
													r,
													s)
						.send({ from: userAcct })
					.then(tx => {
						// provide user with immediate feedback
						alert("Successfully deposited "
							+ web3.utils.fromWei(depositIncrement.toFixed())
							+ " $ENSHROUD");
						// tell the React context update worked
						const oldUser = props.userData;
						const newTot = depositIncrement.plus(oldUser.deposited);
						let updUser = {
							// replicate everything except the .deposited value
							shares: oldUser.shares,
							delegatedTo: oldUser.delegatedTo,
							delegates: oldUser.delegates,
							deposited: newTot.toFixed(),
							unstakeAmount: oldUser.unstakeAmount,
							unstakeShares: oldUser.unstakeShares,
							unstakeScheduledFor: oldUser.unstakeScheduledFor,
							lastDelegationUpdateTimestamp:
								oldUser.lastDelegationUpdateTimestamp
						};
						props.setUserData(updUser);
						let tokenBal = new BigNumber(props.userTokens)
										.minus(depositIncrement);
						props.setUserTokens(tokenBal.toFixed());
						setDepositIncrement(new BigNumber(0));
						resolve(true);
					})
					.catch(err => {
						alert("Error: code " + err.code + ", " + err.message);
						reject(err);
						return false;
					});
				resolve(true);
			}
		);
		return true;
	}

	// render add to deposit action form
	const dispValue = depositIncrement.eq(0) ? ""
					: web3.utils.fromWei(depositIncrement.toFixed());
	return (
		<Form>
			<Form.Group className="mb-3" controlId="increment">
				<Form.Label>
					Amount of $ENSHROUD to deposit using EIP-2612 permit:
				</Form.Label>
				<Form.Control type="text" size="8"
					title={"Max: " + web3.utils.fromWei(props.userTokens)}
					onChange={handleDepositChange} placeholder="0"
					value={dispValue}
				/>
			</Form.Group>
			<LoadingButton variant="success"
				buttonStyle="m-3"
				buttonText="Increase Deposit"
				buttonTitle="Add amount to your deposited total using permit"
				buttonIcon="images/plus-lg.svg"
				netMethod={(resolve, reject) => depositToPool(resolve, reject)}
			/>
		</Form>
	);
}

/* show UI to increase staking using deposited tokens
 * @param props.userData the User{} data, including tokens deposited in DAOPool
 * @param props.setUserData method for setting/updating user deposited balance
 * @param props.stakedShares number of tokens currently staked in DAOPool
 * @param props.setStakedShares set the number of staked shares
 * @param props.totalShares total number of shares currently staked in DAOPool
 * @param props.setTotalShares set the number of total staked shares
 */
function ActionStakeDeposited(props) {
	// enable use of contracts and wallet accounts
	const { state: { accounts, contracts, web3 } } = useEth();

	const userAcct = accounts[0];
	const DaoPoolContract = contracts["DAOPool"];
	const uData = props.userData;	// shorthand

	// local state for staking amount
	const [stakeIncrement, setStakeIncrement] = useState(new BigNumber(0));

	// process a change to deposit increment
	const handleStakeChange = 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)) {
			setStakeIncrement(stakeWei);
		}
	};

	// submit additional staking
	async function stakeInPool(resolve, reject) {
		// determine whether anything changed
		if (stakeIncrement.lte(0)) {
			let errRet = new Error("No stake 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(uData.deposited)) {
			let amtErr = new Error("More than you have available, max "
									+ web3.utils.fromWei(uData.deposited));
			alert(amtErr.message);
			reject(amtErr);
			return false;
		}

		// invoke DAOPool.stake()
		await DaoPoolContract.methods.stake(stakeIncrement.toFixed(), userAcct)
				.send({ from: userAcct })
			.then(receipt => {
				/* From the Staked event (sole event emitted), extract the
				 * number of shares minted (3rd arg), new deposited total (4th),
				 * user's new total of shares (5th), and new total shares (6th).
				 */
				const stakedEventVals = receipt.events["Staked"].returnValues;
				const mintedShares = stakedEventVals.mintedShares;
				const userUnstaked = stakedEventVals.userUnstaked;
				const userShares = stakedEventVals.userShares;
				const totalShares = stakedEventVals.totalShares;
				// provide user with immediate feedback
				alert("Successfully staked "
					+ web3.utils.fromWei(stakeIncrement.toFixed())
					+ " $ENSHROUD for " + web3.utils.fromWei(mintedShares)
					+ " pool shares");

				// tell the React context update worked
				let updUser = {
					// replicate everything except the .deposited value
					shares: uData.shares,	// new Checkpoint not added (unused)
					delegatedTo: uData.delegatedTo,
					delegates: uData.delegates,
					deposited: userUnstaked,
					unstakeAmount: uData.unstakeAmount,
					unstakeShares: uData.unstakeShares,
					unstakeScheduledFor: uData.unstakeScheduledFor,
					lastDelegationUpdateTimestamp:
						uData.lastDelegationUpdateTimestamp
				};
				props.setUserData(updUser);
				props.setStakedShares(userShares);
				props.setTotalShares(totalShares);
				resolve(true);
			})
			.catch(err => {
				alert("Error: code " + err.code + ", " + err.message);
				reject(err);
				return false;
			});
		// NB: call this at the end to avoid making the stake() happen twice
		setStakeIncrement(new BigNumber(0));
		return true;
	}

	// render form to increase staking
	const dispValue = stakeIncrement.eq(0) ? ""
					: web3.utils.fromWei(stakeIncrement.toFixed());
	return (
		<Form>
			<Form.Group className="mb-3" controlId="increment">
				<Form.Label>
					Amount of deposited $ENSHROUD to stake in DAO Pool:
				</Form.Label>
				<Form.Control type="text" size="8"
					title={"Max: " + web3.utils.fromWei(props.userData.deposited)}
					onChange={handleStakeChange} placeholder="0"
					value={dispValue}
				/>
			</Form.Group>
			<LoadingButton variant="success"
				buttonStyle="m-3"
				buttonText="Increase Staking"
				buttonTitle="Add amount to your staking in pool shares"
				buttonIcon="images/plus-circle.svg"
				netMethod={(resolve, reject) => stakeInPool(resolve, reject)}
			/>
		</Form>
	);
}

/* show UI to withdraw tokens from a Timelock and stake in the DAOPool contract
 * @param props.withdrawable number of unlocked tokens in TimelockManager (wei)
 * @param props.setUnlocked set the number of withdrawable TimelockManager toks
 * @param props.userData user's User{} data, including any deposited balance
 * @param props.setUserData method for setting/updating user deposited balance
 * @param props.mgrTimelock the Timelock for the user in TimelockManager
 * @param props.setDAOTimelock set the DAOPool contract Timelock
 * @param props.setTMTimelock set the Timelock for the user in TimelockManager
 */
function ActionStakeLocked(props) {
	const uData = props.userData;
	// enable use of our contracts and wallet accounts
	const { state: { accounts, contracts, web3 } } = useEth();
	const TimelockMgr = contracts["TimelockManager"];
	const DaoPoolContract = contracts["DAOPool"];
	const userAcct = accounts[0];

	// local state for beneficiary address
	const [beneficiary, setBeneficiary] = useState(userAcct);

	// process a Timelock stake beneficiary address value change
	const handleRecipientAddressChange = e => {
		const addr = e.target.value.toString();
		if (addr === '' || /^0x[0-9a-fA-F]+/.test(addr)) {
			setBeneficiary(addr);
		}
	};

	// compute total still locked in original Timelock
	const totalLocked = new BigNumber(props.mgrTimelock.remainingAmount)
									.minus(props.withdrawable);
	const isTransfer = userAcct !== beneficiary;
	const destAddress
		= isTransfer ? beneficiary + " (not your account!)" : beneficiary;

	// transfer Timelock from TimelockManager to DAOPool
	async function transferTimelock(resolve, reject) {
		// determine whether anything can be moved
		if (totalLocked.lte(0) && props.withdrawable.lte(0)) {
			let errRet = new Error("No tokens available to transfer "
								+ "to DAOPool");
			alert(errRet.message);
			reject(errRet);
			return false;
		}

		// if a transfer, verify that the recipient doesn't already have a TL
		if (isTransfer) {
			let possTL = await DaoPoolContract.methods.userToTimelock(
				beneficiary).call( { from: userAcct });
			if (possTL !== undefined) {
				// should be all zeroes
				if (possTL.totalAmount > 0) {
					let exists = new Error("The beneficiary address already "
										+ "has a Timelock in the DAOPool");
					alert(exists.message);
					reject(exists);
					return false;
				}
			}
		}

		// perform on-chain action
		await TimelockMgr.methods.withdrawToPool(beneficiary)
				.send({ from: userAcct })
			.then(receipt => {
				/* receipt processing is moot if it's a transfer, as we are not
				 * the user who's getting the deposited tokens in the Timelock
				 */
				if (isTransfer) {
					// withdrawable is now 0 (this will reset display here to 0)
					props.setUnlocked(new BigNumber(0));

					// TimelockManager Timelock is now deleted
					const nullTL = {
						totalAmount: "0",
						remainingAmount: "0",
						releaseStart: "0",
						releaseEnd: "0"
					};
					props.setTMTimelock(nullTL);

					resolve(true);
					return true;
				}

				/* Instead of trusting the pre-computed totals, pull event data.
				 * There are up to 6 events raised by this operation:
				 * [0] - from EnshroudToken for Approval (all remaining tokens)
				 * [1] - from EnshroudToken for Transfer (of unlocked tokens)
				 * [2] - from DAOPool for DepositedByTimelockManager (unlocked)
				 * [3] - from EnshroudToken for Transfer (of locked tokens)
				 * [4] - from DAOPool for DepositedUnlocking
				 * [WithdrawnToPool] from TimelockManager (umbrella event)
				 *
				 * We only need data from [2] and [4], but because these events
				 * were not raised by TimelockManager, returnValues[] is empty.
				 * We must therefore manually decode the returned raw data.
				 *
				 * In the event that there are no unlocked tokens, then the
				 * first Transfer will not occur because DAOPool.deposit() will
				 * not be called by the TimelockManager.  Also, the event
				 * DepositedByTimelockManager will not be present.
				 *
				 * In the situation that there were no locked tokens (i.e. the
				 * Timelock is past its releaseEnd block), the second Transfer
				 * and the DepositedUnlocking event will not be present.
				 *
				 * Therefore to distinguish these cases, we will examine
				 * events[] elements.  If ['4'] then we want indices [2] & [4].
				 *
				 * If no ['4'], then we have two possible situations:
				 * either there were no locked tokens, or no unlocked tokens.
				 * To distinguish between these cases, we'll examine the length
				 * of the ABI-encoded raw.data.  If it represents 3 arguments,
				 * it's a DepositedUnlocking event.  If it represents only 2
				 * arguments, it's a DepositedByTimelockManager event.
				 */
				var depositedByTimelockManagerData = [];
				var depositedUnlockingData = [];
				var newDepTot = uData.deposited;
				var newLocked = "0";
				var newStart = 0;
				if (receipt.events['4'] !== undefined) {
					// all events are present (both locked/unlocked tokens)
					const depositedByTimelockManagerABI
						= receipt.events['2'].raw.data;
					depositedByTimelockManagerData
						= web3.eth.abi.decodeParameters(['uint256', 'uint256'],
												depositedByTimelockManagerABI);
					newDepTot = depositedByTimelockManagerData['1'];
					const depositedUnlockingABI = receipt.events['4'].raw.data;
					depositedUnlockingData = web3.eth.abi.decodeParameters(
											['uint256', 'uint128', 'uint128'],
											depositedUnlockingABI);
					newLocked = depositedUnlockingData['0'];
					newStart = depositedUnlockingData['1'];
				}
				else {
					/* There are locked tokens or unlocked, but not both.
					 * Either way, our desired data will be found at index 2.
					 */
					const event2ABIdata = receipt.events['2'].raw.data;
					if (event2ABIdata.length > 130) {
						depositedUnlockingData = web3.eth.abi.decodeParameters(
							['uint256', 'uint128', 'uint128'], event2ABIdata);
						newLocked = depositedUnlockingData['0'];
						newStart = depositedUnlockingData['1'];
					}
					else {
						depositedByTimelockManagerData
							= web3.eth.abi.decodeParameters(
														['uint256', 'uint256'],
														event2ABIdata);
						newDepTot = depositedByTimelockManagerData['1'];
					}
				}

				// tell the React context update worked
				// 1. DAO's new Timelock has totalLocked locked, same stop,
				// 	  start will be greater of mined block or TLM TL end
				if (newStart > 0) {
					// add new DAOPool Timelock to local state
					const daoTimelock = {
						totalAmount: newLocked,
						remainingAmount: newLocked,
						releaseStart: newStart,
						releaseEnd: props.mgrTimelock.releaseEnd
					};
					props.setDAOTimelock(daoTimelock);
				}

				// 2. any withdrawable tokens are now deposited in User{}
				if (newDepTot !== uData.deposited) {
					// update new deposited total; all else unchanged
					const updUser = {
						shares: uData.shares,
						delegatedTo: uData.delegatedTo,
						delegates: uData.delegates,
						deposited: newDepTot,
						unstakeAmount: uData.unstakeAmount,
						unstakeShares: uData.unstakeShares,
						unstakeScheduledFor: uData.unstakeScheduledFor,
						lastDelegationUpdateTimestamp:
							uData.lastDelegationUpdateTimestamp
					};
					props.setUserData(updUser);
				}

				// 3. withdrawable is now 0 (this will reset display here to 0)
				props.setUnlocked(new BigNumber(0));

				// 4. TimelockManager Timelock is now deleted
				const nullTL = {
					totalAmount: "0",
					remainingAmount: "0",
					releaseStart: "0",
					releaseEnd: "0"
				};
				props.setTMTimelock(nullTL);
				resolve(true);
			})
			.catch(err => {
				alert("Error: code " + err.code + ", " + err.message);
				reject(err);
				return false;
			});
		return true;
	}

	// render Timelock transfer form
	return (
		<>
		<Card>
			<Card.Body>
				<Card.Title>
					Transferring Your Timelock to the DAOPool
				</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 into
					the <i>DAOPool</i> contract.  This involves transferring
					the entire Timelock and all locked tokens, according to the
					following rules.
					<br/><br/>
					<ListGroup numbered>
						<ListGroup.Item>
							The number of withdrawable (unlocked) tokens in
							your existing <i>TimelockManager</i> Timelock is
							shown below.  These tokens will <b>NOT</b> be staked
							during this operation.  Instead, they will get
							deposited as unlocked tokens in the <i>DAOPool</i>.
							(You can add them to your staking separately later
							using the "Stake Deposited Tokens" section.)
						</ListGroup.Item>
						<ListGroup.Item>
							The remaining tokens which are locked will all be
							transferred to the <i>DAOPool</i> contract, where
							they will remain locked, and continue to unlock
							according to the same regular linear unlocking
							schedule.  Basically your existing Timelock is
							transferred between contracts, with any already
							unlocked tokens pre-withdrawn from the new Timelock.
						</ListGroup.Item>
						<ListGroup.Item>
							You may update your timelock status at any time to
							unlock tokens now eligible as of the current block.
							To do this, use the <b>Update Timelock</b> button
							in the "DAOPool Staking Details" section above.
							(Note this button will not appear until after you
							have performed the Timelock transfer operation.)
							Tokens you unlock will be added to your Deposited
							tokens balance, where you can either stake or
							withdraw them.
						</ListGroup.Item>
						<ListGroup.Item>
							Note that in the <i>DAOPool</i> staking context,
							your locked tokens <b>DO NOT</b> earn you any
							yields.  You must unlock and then stake your tokens
							as they become available.  Alternatively, you can
							withdraw them as they unlock (as per above).
						</ListGroup.Item>
						<ListGroup.Item>
							It is permitted to override the beneficiary address
							to another address.  A "not your account" hover
							warning will be shown in this case.  If you do
							this, make certain that you also control this
							other address, or are otherwise okay with granting
							ownership of the tokens to whoever does control it.
						</ListGroup.Item>
						<ListGroup.Item>
							Note that all amounts shown are estimated as of the
							next block.  Actual final amounts may differ due to
							a later block number at the time of execution.
						</ListGroup.Item>
					</ListGroup>
				</Card.Text>
			</Card.Body>
		</Card>
		<br/>
		<Form>
			<Form.Group className="mb-3" controlId="lockedBal">
				<Form.Label>Locked Tokens Available:</Form.Label>
				<Form.Control type="text" readOnly
					value={web3.utils.fromWei(totalLocked.sd(6).toFixed())}
				/>
			</Form.Group>
			<Form.Group className="mb-3" controlId="unlockedBal">
				<Form.Label>Unlocked Tokens to Deposit:</Form.Label>
				<Form.Control type="text" readOnly
					value={web3.utils.fromWei(props.withdrawable.sd(6).toFixed())}
				/>
			</Form.Group>
			<Form.Group className="mb-3" controlId="beneficiaryAddr">
				<Form.Label>Address to Receive Timelock:</Form.Label>
				<Form.Control type="text" title={destAddress}
					value={beneficiary} onChange={handleRecipientAddressChange}
				/>
			</Form.Group>
			<LoadingButton variant="primary"
				buttonStyle="m-3"
				buttonText="Transfer Timelock"
				buttonTitle={"Move Timelock and all tokens to DAOPool for " + destAddress}
				buttonIcon="images/file-plus.svg"
				netMethod={(resolve, reject) => transferTimelock(resolve, reject)}
			/>
		</Form>
		</>
	);
}

/* show UI to withdraw currently unlocked tokens from a TimelockManager Timelock
 * @param props.withdrawable number of unlocked tokens in TimelockManager (wei)
 * @param props.setUnlocked set the number of withdrawable TimelockManager toks
 * @param props.mgrTimelock the Timelock for the user in TimelockManager
 * @param props.setTMTimelock set the Timelock for the user in TimelockManager
 * @param props.userTokens the number of $ENSHROUD tokens the user owns (wei)
 * @param props.setUserTokens method for setting/updating user token balance
 */
function ActionWithdrawUnlocked(props) {
	// enable use of our contracts and wallet accounts
	const { state: { accounts, contracts, web3 } } = useEth();
	const TimelockMgr = contracts["TimelockManager"];
	const userAcct = accounts[0];

	// submit withdrawal request to TimelockManager
	async function withdrawUnlocked(resolve, reject) {
		// make sure they have some
		if (props.withdrawable.lte(0)) {
			let errRet = new Error("You have no unlocked withdrawable tokens");
			alert(errRet.message);
			reject(errRet);
			return false;
		}

		// perform on-chain action
		await TimelockMgr.methods.withdraw().send({ from: userAcct })
			.then(receipt => {
				/* instead of trusting computed total (since 1 or more blocks
				 * may have elapsed), pull confirmed amount from event
				 */
				const amtWithdrawn = receipt.events["Withdrawn"]
									.returnValues.withdrawable;
				// provide user feedback
				alert("Successfully withdrew "
					+ web3.utils.fromWei(amtWithdrawn) + " unlocked $ENSHROUD "
					+ "from TimelockManager");

				// tell React context about updates
				const newBal
					= new BigNumber(props.userTokens).plus(amtWithdrawn);
				// 1. Tokens become part of user's $ENSHROUD balance
				props.setUserTokens(newBal.toFixed());
				// 2. unlocked is now 0
				props.setUnlocked(new BigNumber(0));
				// 3. Timelock.remainingAmount is reduced by amount withdrawn
				const newLocked
					= new BigNumber(props.mgrTimelock.remainingAmount)
									.minus(amtWithdrawn);
				const updTimelock = {
					totalAmount: props.mgrTimelock.totalAmount,
					remainingAmount: newLocked.toFixed(),
					releaseStart: props.mgrTimelock.releaseStart,
					releaseEnd: props.mgrTimelock.releaseEnd
				};
				props.setTMTimelock(updTimelock);
				resolve(true);
			})
			.catch(err => {
				alert("Error: code " + err.code + ", " + err.message);
				reject(err);
				return false;
			});
		return true;
	}

	// render form to withdraw from TM Timelock
	return (
		<>
		<Card>
			<Card.Body>
				<Card.Title>
					Removing Unlocked Tokens from Your TimelockManager Timelock
				</Card.Title>
				<Card.Subtitle>Explanation</Card.Subtitle>
				<Card.Text>
					<br/>
					Any tokens in
					your <i>TimelockManager</i> Timelock which are now unlocked
					according to the unlocking schedule, can be withdrawn
					directly to your $ENSHROUD wallet balance (without staking
					them anywhere).  If this is what you want to do,
					you're in the right place!
				</Card.Text>
				<Card.Text>
					<br/>
					Any <b>locked</b> $ENSHROUD tokens held in
					the <i>TimelockManager</i> contract can also be transferred
					into the <i>DAOPool</i> contract.  To do this, use the
					"Transfer Timelock to DAOPool" section above.
				</Card.Text>
				<Card.Text>
					<br/>
					NB: A Timelock in
					the <i>TimelockManager</i> contract can instead be
					transferred to the <i>MVOStaking</i> contract, if you are
					an MVO operator and wish to your stake locked tokens on
					your MVO.  To do this, use the "Staking an MVO Using
					Locked Tokens" section under
					the <b>Staking/MVO Staking</b> menu option.
					<br/><br/>
					<ListGroup numbered>
						<ListGroup.Item>
							The estimated number of withdrawable (unlocked)
							tokens in your
							existing <i>TimelockManager</i> Timelock (if
							any) is shown below.
						</ListGroup.Item>
						<ListGroup.Item>
							Use the <b>Withdraw Unlocked</b> button to 
							trigger this withdrawal. (All unlocked tokens will
							be withdrawn; you cannot specify a lesser amount.)
						</ListGroup.Item>
						<ListGroup.Item>
							Note that the amount shown is estimated as of the
							current block when you last refreshed your DAOPool
							Info.  The actual amount may differ due
							to a later block number at the time of execution.
						</ListGroup.Item>
					</ListGroup>
				</Card.Text>
			</Card.Body>
		</Card>
		<br/>
		<Form>
			<Form.Group className="mb-3" controlId="lockedBal">
				<Form.Label>Unlocked Tokens Available:</Form.Label>
				<Form.Control type="text" readOnly
					value={web3.utils.fromWei(props.withdrawable.sd(6).toFixed())}
				/>
			</Form.Group>
			<LoadingButton variant="warning"
				buttonStyle="m-3"
				buttonText="Withdraw Unlocked"
				buttonTitle="Remove all unlocked tokens from Timelock"
				buttonIcon="images/file-plus.svg"
				netMethod={(resolve, reject) => withdrawUnlocked(resolve, reject)}
			/>
		</Form>
		</>
	);
}

/* show UI to reduce staking and withdraw tokens from DAOPool contract
 * @param props.stakedShares user's number of shares currently staked in DAOPool
 * @param props.setStakedShares set the user's number of staked shares
 * @param props.totalShares total number of shares currently staked in DAOPool
 * @param props.setTotalShares set the number of total staked shares
 * @param props.userData the User{} data
 * @param props.setUserData method for setting/updating User{} data
 * @param props.totalStake total amount of $ENSHROUD currently staked in DAOPool
 */
function ActionReduceStaking(props) {
	// enable use of our contracts and wallet accounts
	const { state: { accounts, contracts, web3 } } = useEth();
	const DaoPoolContract = contracts["DAOPool"];
	const userAcct = accounts[0];
	const uData = props.userData;

	// local state for number of shares to unstake
	const [unstakeShares, setUnstakeShares] = useState(new BigNumber(0));

	// process a share amount change
	const handleUnstakeAmtChange = 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 unstakeWei = amt.times("1e18");
		if (amt.gte(0)) {
			setUnstakeShares(unstakeWei);
		}
	};

	// compute the amount of tokens the entered number of shares works out to
	var unstakeAmount = props.totalStake.times(unstakeShares)
						.idiv(props.totalShares);
	const shareAmtDisp = unstakeShares.eq(0) ? ""
						: web3.utils.fromWei(unstakeShares.toFixed());
	const max = web3.utils.fromWei(props.stakedShares.toFixed());

	// submit schedule unstaking request to DAOPool
	async function scheduleUnstake(resolve, reject) {
		// check for legal share quantity
		if (unstakeShares.lte(0)) {
			let badShares = new Error("Illegal share quantity, "
								+ web3.utils.fromWei(unstakeShares.toFixed()));
			alert(badShares.message);
			reject(badShares);
			return false;
		}

		// make sure they have enough shares
		if (props.stakedShares.lt(unstakeShares)) {
			let noShares = new Error("You have only " + max
									+ " staked shares available to unstake");
			alert(noShares.message);
			reject(noShares);
			return false;
		}

		// make sure they don't have an unstake already scheduled
		if (uData.unstakeScheduledFor !== "0") {
			let schedTime = new Date();
			schedTime.setTime(+uData.unstakeScheduledFor * 1000);
			let alreadySched = new Error("You already have an unstake "
								+ "scheduled for: " + schedTime.toString());
			alert(alreadySched.message);
			reject(alreadySched);
			return false;
		}

		// perform on-chain action
		await DaoPoolContract.methods.scheduleUnstake(unstakeAmount.toFixed())
				.send({ from: userAcct })
			.then(receipt => {
				/* instead of trusting computed total (since share totals
				 * may have changed), pull confirmed amounts from event
				 */
				const schedUnstakeData
					= receipt.events["ScheduledUnstake"].returnValues;
				const amtUnstaked = schedUnstakeData.amount;
				const sharesUnstaked = schedUnstakeData.shares;
				const scheduledFor = schedUnstakeData.scheduledFor;
				const updShares = schedUnstakeData.userShares;
				const schedDt = new Date();
				schedDt.setTime(+scheduledFor * 1000);
				// provide user feedback
				alert("Successfully scheduled unstake of "
					+ web3.utils.fromWei(sharesUnstaked) + " from DAOPool "
					+ " for " + schedDt.toString());

				// tell React context about updates
				// 1. user's share total changes
				props.setStakedShares(updShares);
				// 2. User{} fields are updated
				const updUserData = {
					shares: uData.shares,	// changed, but we don't use this
					delegatedTo: uData.delegatedTo,	// changed, but unused
					delegates: uData.delegates,
					deposited: uData.deposited,
					unstakeAmount: amtUnstaked,
					unstakeShares: sharesUnstaked,
					unstakeScheduledFor: scheduledFor,
					lastDelegationUpdateTimestamp:
						uData.lastDelegationUpdateTimestamp
				};
				setUnstakeShares(new BigNumber(0));
				props.setUserData(updUserData);
				resolve(true);
			})
			.catch(err => {
				alert("Error: code " + err.code + ", " + err.message);
				reject(err);
				return false;
			});
		return true;
	}

	// calculate percentage user's shares represents out of pool total
	var sharePct = props.stakedShares.dividedBy(props.totalShares).times(100);

	// render UI form for unstaking shares
	return (
		<>
		<Card>
			<Card.Body>
				<Card.Title>
					Scheduling an Unstake of Shares
				</Card.Title>
				<Card.Subtitle>Procedure</Card.Subtitle>
				<Card.Text>
					<br/>
					When you want to reduce your staking in the Enshroud
					DAOPool, this is done by first scheduling an unstaking of
					any portion of your currently staked shares.
					An Epoch (~1 week) must then elapse before you can
					perform the actual unstaking operation.  The following
					rules apply:
					<br/><br/>
					<ListGroup numbered>
						<ListGroup.Item>
							The number of shares you have staked in
							the <i>DAOPool</i> contract is shown below.
							You may unstake any or all of them.
						</ListGroup.Item>
						<ListGroup.Item>
							One week (an "Epoch") must elapse before you can
							perform the actual unstaking.  (To do that, see the
							section "Unstake/Withdraw Tokens" below.)
						</ListGroup.Item>
						<ListGroup.Item>
							If you already have an unstaking scheduled, you
							cannot schedule a second overlapping one.  You must
							wait for the scheduled unstaking to mature, then
							unstake the tokens, before scheduling another one.
						</ListGroup.Item>
						<ListGroup.Item>
							The actual number of $ENSHROUD tokens you will
							receive for your unstaked shares is calculated as a
							pro-rated percentage of the total shares staked in
							the pool at the time you schedule your unstaking.
							However, this number can change during the Epoch
							based on new stakings or unstakings by other users,
							which affect the total number of shares and staked
							tokens.  When you actually execute your unstaking,
							you will receive either the quantity of tokens
							scheduled to unstake, or the amount of tokens your
							unstaked shares corresponds to at that time,
							whichever is smaller. (This rule ensures that there
							will always be sufficient staked tokens to fund
							all future unstakings.)
						</ListGroup.Item>
						<ListGroup.Item>
							Unstaked tokens are added to your "deposited" token
							balance within the <i>DAOPool</i> contract.  You
							can optionally withdraw them back to your wallet
							at the same time, depending upon which button you
							select in the "Unstake/Withdraw Tokens" operation.
							If you elect not to withdraw your unstaked tokens
							from the contract, leaving them in your "deposited"
							balance, you can remove them separately later (by
							using the "Withdraw Deposited Tokens" section
							below).
						</ListGroup.Item>
						<ListGroup.Item>
							Enter the number of shares you wish to unstake.
							Use the <b>Schedule Unstake</b> button to 
							establish the scheduled unstaking of your shares.
						</ListGroup.Item>
						<ListGroup.Item>
							Note that amounts shown are estimated as of the
							current block.  Actual final amounts may differ.
						</ListGroup.Item>
					</ListGroup>
				</Card.Text>
			</Card.Body>
		</Card>
		<br/>
		<Form>
			{ /* shares staked by user */ }
			<Form.Group className="mb-3" controlId="shares">
				<Form.Label>Your staked shares in pool:</Form.Label>
				<Form.Control type="text" readOnly
					value={web3.utils.fromWei(props.stakedShares.sd(6).toFixed())}
				/>
			</Form.Group>
			{ /* total shares in pool */ }
			<Form.Group className="mb-3" controlId="totShares">
				<Form.Label>Total shares in pool:</Form.Label>
				<Form.Control type="text" readOnly
					value={web3.utils.fromWei(props.totalShares.sd(6).toFixed())}
				/>
			</Form.Group>
			{ /* percentage user has in pool */ }
			<Form.Group className="mb-3" controlId="pctShares">
				<Form.Label>Your share percentage:</Form.Label>
				<Form.Control type="text" readOnly title="of total pool shares"
					value={sharePct.toString() + "%"}
				/>
			</Form.Group>
			<Form.Group className="mb-3" controlId="unstake">
				<Form.Label>Number of your shares to unstake:</Form.Label>
				<Form.Control type="text" size="8" value={shareAmtDisp}
					onChange={handleUnstakeAmtChange} placeholder="0"
					title={"Max: " + max}
				/>
			</Form.Group>
			<Form.Group className="mb-3" controlId="tokenEst">
				<Form.Label>Estimated number of tokens for shares:</Form.Label>
				<Form.Control type="text" readOnly
					value={web3.utils.fromWei(unstakeAmount.sd(6).toFixed())}
					title="based on current number of shares and staked tokens"
				/>
			</Form.Group>
			<LoadingButton variant="primary"
				buttonStyle="m-3"
				buttonText="Schedule Unstake"
				buttonTitle="Schedule unstaking of shares for 1 week from now"
				buttonIcon="images/patch-minus.svg"
				netMethod={(resolve, reject) => scheduleUnstake(resolve, reject)}
			/>
		</Form>
		</>
	);
}

/* show UI to withdraw tokens from previously scheduled unstaking, with options
 * to either withdraw to deposited or withdraw altogether
 * @param props.userData the User{} data from DAOPool for the user
 * @param props.setUserData the method to update the User{} data with changes
 * @param props.userTokens the number of $ENSHROUD tokens the user owns (wei)
 * @param props.setUserTokens method to update $ENSHROUD tokens balance
 * @param props.stakedShares number of tokens currently staked in DAOPool
 * @param props.setStakedShares set the number of staked shares
 * @param props.totalShares total number of shares currently staked in DAOPool
 * @param props.setTotalShares set the number of total staked shares
 * @param props.totalStake total amount of $ENSHROUD currently staked in DAOPool
 * @param props.setTotalStake set the number of total staked tokens
 */
function ActionWithdrawFromStaking(props) {
	// enable use of our contracts and wallet accounts
	const { state: { accounts, contracts, web3 } } = useEth();
	const DaoPoolContract = contracts["DAOPool"];
	const userAcct = accounts[0];
	const uData = props.userData;

	// compute the amount of tokens the entered number of shares works out to
	var calcUnstakeAmt = props.totalStake.times(uData.unstakeShares).div(props.totalShares);

	// user gets this or recorded quantity of tokens, whichever is less
	var unstakeTokens = BigNumber.min(calcUnstakeAmt, uData.unstakeAmount);

	// whether user's scheduled unstaking is eligible for unstaking
	const tsNow = Date.now();
	const schedMilli = uData.unstakeScheduledFor * 1000;
	const schedDate = new Date(schedMilli);
	const eligible = schedMilli <= tsNow;

	// submit DAOPool.unstake() call
	async function unstakeOnly(resolve, reject) {
		// make sure their unstake timestamp is present
		if (uData.unstakeScheduledFor === "0") {
			let noUnstake = new Error("You don't have an unstake scheduled");
			alert(noUnstake.message);
			reject(noUnstake);
			return false;
		}

		// make sure their unstake timestamp is met
		if (!eligible) {
			let notYet = new Error("You have an unstake scheduled for: "
									+ schedDate.toString()
									+ ", which is still in the future");
			alert(notYet.message);
			reject(notYet);
			return false;
		}

		// check for legal token quantity
		if (unstakeTokens.lte(0)) {
			let noToks = new Error("You don't have any unstakable tokens");
			alert(noToks.message);
			reject(noToks);
			return false;
		}

		// perform on-chain action
		await DaoPoolContract.methods.unstake(userAcct).send({ from: userAcct })
			.then(receipt => {
				// pull updated amounts from event
				const unstakedData = receipt.events["Unstaked"].returnValues;
				const amtUnstaked = unstakedData.amount;
				const userDeposited = unstakedData.userUnstaked;
				const totShares = unstakedData.totalShares;
				const totalStake = unstakedData.totalStake;
				// notify user
				alert("Successfully unstaked " + web3.utils.fromWei(amtUnstaked)
					+ " $ENSHROUD tokens from DAO Pool");

				// tell React context about updates
				// 1. user's share total changes, less shares unstaked
				let updShareTotal = props.stakedShares.minus(amtUnstaked);
				props.setStakedShares(updShareTotal);
				// 2. User{} fields are updated
				const updUserData = {
					shares: uData.shares,	// changed, but we don't use this
					delegatedTo: uData.delegatedTo,	// changed, but unused
					delegates: uData.delegates,
					deposited: userDeposited,
					unstakeAmount: "0",			// reset
					unstakeShares: "0",			// reset
					unstakeScheduledFor: "0",	// reset
					lastDelegationUpdateTimestamp:
						uData.lastDelegationUpdateTimestamp
				};
				props.setUserData(updUserData);
				// 3. total shares updates
				props.setTotalShares(totShares);
				// 4. total staked tokens updates
				props.setTotalStake(totalStake);
				resolve(true);
			})
			.catch(err => {
				alert("Error: code " + err.code + ", " + err.message);
				reject(err);
				return false;
			});
		return true;
	}

	// submit DAOPool.unstakeAndWithdraw() call
	async function unstakePlusWithdraw(resolve, reject) {
		// make sure their unstake timestamp is present
		if (uData.unstakeScheduledFor === "0") {
			let noUnstake = new Error("You don't have an unstake scheduled");
			alert(noUnstake.message);
			reject(noUnstake);
			return false;
		}

		// make sure their unstake timestamp is met
		if (!eligible) {
			let notYet = new Error("You have an unstake scheduled for: "
									+ schedDate.toString()
									+ ", which is still in the future");
			alert(notYet.message);
			reject(notYet);
			return false;
		}

		// check for legal token quantity
		if (unstakeTokens.lte(0)) {
			let noToks = new Error("You don't have any unstakable tokens");
			alert(noToks.message);
			reject(noToks);
			return false;
		}

		// perform on-chain action
		await DaoPoolContract.methods.unstakeAndWithdraw()
				.send({ from: userAcct })
			.then(receipt => {
				// pull updated amounts from events
				const unstakedData = receipt.events["Unstaked"].returnValues;
				const totShares = unstakedData.totalShares;
				const totalStake = unstakedData.totalStake;
				const withdrawnData = receipt.events["Withdrawn"].returnValues;
				// take new User.deposited value from 2nd (Withdrawn) event
				const userDeposited = withdrawnData.userUnstaked;
				const amtWithdrawn = withdrawnData.amount;
				// notify user
				alert("Successfully unstaked and withdrew "
					+ web3.utils.fromWei(amtWithdrawn)
					+ " $ENSHROUD tokens from DAO Pool");

				// tell React context about updates
				// 1. user's share total changes, less shares unstaked
				let updShareTotal
					= props.stakedShares.minus(uData.unstakeShares);
				props.setStakedShares(updShareTotal);
				// 2. User{} fields are updated
				const updUserData = {
					shares: uData.shares,	// changed, but we don't use this
					delegatedTo: uData.delegatedTo,	// changed, but unused
					delegates: uData.delegates,
					deposited: userDeposited,
					unstakeAmount: "0",			// reset
					unstakeShares: "0",			// reset
					unstakeScheduledFor: "0",	// reset
					lastDelegationUpdateTimestamp:
						uData.lastDelegationUpdateTimestamp
				};
				props.setUserData(updUserData);
				// 3. total shares updates
				props.setTotalShares(totShares);
				// 4. total staked tokens updates
				props.setTotalStake(totalStake);
				// 5. user's $ENSHROUD balance increments
				let newBal = new BigNumber(props.userTokens).plus(amtWithdrawn);
				props.setUserTokens(newBal.toFixed());
				resolve(true);
			})
			.catch(err => {
				alert("Error: code " + err.code + ", " + err.message);
				reject(err);
				return false;
			});
		return true;
	}

	// render UI form for unstaking/withdrawing tokens
	const shareAmt = new BigNumber(web3.utils.fromWei(uData.unstakeShares));
	const tokenAmt = new BigNumber(web3.utils.fromWei(uData.unstakeAmount));
	var vestDate = "(none scheduled)";
	if (uData.unstakeScheduledFor > 0) {
		vestDate = new Date(+uData.unstakeScheduledFor * 1000).toString();
	}
	return (
		<>
		<Card>
			<Card.Body>
				<Card.Title>
					Unstaking and Withdrawing Tokens
				</Card.Title>
				<Card.Subtitle>Rules and Options</Card.Subtitle>
				<Card.Text>
					<br/>
					To use this function, you must previously have scheduled an
					unstaking of some or all of your shares in the
					Enshroud <i>DAOPool</i> contract.  (See the section
					"Scheduling Unstaking of Shares" above to do this.)
					An Epoch (~1 week) must
					then elapse before you can unstake your tokens.
					<br/><br/>
					<ListGroup numbered>
						<ListGroup.Item>
							The status of your scheduled unstaking (if any) is
							shown below.  If a scheduled unstaking exists and
							the Epoch has passed, you may unstake the resulting
							tokens in one of two ways.
						</ListGroup.Item>
						<ListGroup.Item>
							Method One: unstake your tokens into your
							"deposited" balance.  That is, your shares will be
							redeemed for their equivalent $ENSHROUD tokens, but
							the tokens will remain deposited in
							the <i>DAOPool</i> contract.  Use this option if
							you plan to stake your tokens again later.
							The <b>Unstake Only</b> button accomplishes this.
						</ListGroup.Item>
						<ListGroup.Item>
							Method Two: unstake your tokens and also immediately
							withdraw them back to your account's $ENSHROUD
							token balance.  Use this option if you don't
							plan to re-stake your tokens in the <i>DAOPool</i>.
							The <b>Unstake and Withdraw</b> button does this.
						</ListGroup.Item>
						<ListGroup.Item>
							If you elect not to withdraw your unstaked tokens
							from the contract, leaving them in your "deposited"
							balance (Method One), you can withdraw them
							separately later, by using the "Withdraw Deposited
							Tokens" section below.
						</ListGroup.Item>
						<ListGroup.Item>
							The actual number of $ENSHROUD tokens you will
							receive for your unstaked shares is calculated as a
							pro-rated percentage of the total shares staked in
							the pool at the time you perform your unstaking.
							However, this number may have changed since the
							same computation was performed when you originally
							scheduled the unstaking,
							based on new stakings or unstakings by other users
							during the Epoch, which affect the total number of
							shares and staked tokens.  As a result,
							you will receive either the quantity of tokens
							scheduled to unstake, or the amount of tokens your
							unstaked shares corresponds to at that time,
							whichever is smaller. (This rule ensures that there
							will always be sufficient staked tokens to fund
							all future unstakings.)
						</ListGroup.Item>
					</ListGroup>
					<br/>
				</Card.Text>
			</Card.Body>
		</Card>
		<br/>
		<Form>
			<Form.Group className="mb-3" controlId="unstake">
				<Form.Label>Number of your shares being unstaked:</Form.Label>
				<Form.Control type="text" readOnly
					value={shareAmt.sd(6).toFixed()}
				/>
			</Form.Group>
			<Form.Group className="mb-3" controlId="tokenEst">
				<Form.Label>Estimated number of tokens to unstake:</Form.Label>
				<Form.Control type="text" readOnly
					value={tokenAmt.sd(6).toFixed()}
					title="based on current number of shares and staked tokens"
				/>
			</Form.Group>
			<Form.Group className="mb-3" controlId="schedDate">
				<Form.Label>
					Earliest date at which unstake becomes available:
				</Form.Label>
				<Form.Control type="text" readOnly value={vestDate} />
			</Form.Group>
			<LoadingButton variant="primary"
				buttonStyle="m-3"
				buttonText="Unstake Only"
				buttonTitle="Perform unstaking without withdrawing tokens"
				buttonIcon="images/file-x.svg"
				netMethod={(resolve, reject) => unstakeOnly(resolve, reject)}
			/>
			<br/>
			<LoadingButton variant="success"
				buttonStyle="m-3"
				buttonText="Unstake and Withdraw"
				buttonTitle="Perform unstaking plus withdraw tokens"
				buttonIcon="images/file-arrow-down.svg"
				netMethod={(resolve, reject) => unstakePlusWithdraw(resolve, reject)}
			/>
		</Form>
		</>
	);
}

/* show UI to withdraw deposited tokens separately
 * @param props.userData the User{} data from DAOPool for the user
 * @param props.setUserData the method to update the User{} data with changes
 * @param props.userTokens the number of $ENSHROUD tokens the user owns (wei)
 * @param props.setUserTokens method to update $ENSHROUD tokens balance
 */
function ActionWithdrawDeposited(props) {
	// enable use of our contracts and wallet accounts
	const { state: { accounts, contracts, web3 } } = useEth();
	const DaoPoolContract = contracts["DAOPool"];
	const userAcct = accounts[0];
	const uData = props.userData;

	// local state for number of tokens to withdraw
	const [withdrawAmount, setWithdrawAmount] = useState(new BigNumber(0));

	// process a token amount change
	const handleWithdrawAmtChange = 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 withAmtStr = inpVal;
		let withAmt = new BigNumber(withAmtStr);
		if (withAmt.isNegative()) {
			withAmtStr = "0.0";
			withAmt = amt;
		}
		amt = amt.plus(withAmt);
		const withWei = amt.times("1e18");
		if (withWei.gte(0)) {
			setWithdrawAmount(withWei);
		}
	};

	// submit DAOPool.withdrawRegular() call
	async function doWithdraw(resolve, reject) {
		// check for legal token quantity
		if (withdrawAmount.lte(0)) {
			let badAmt = new Error("No token withdraw amount, enter a positive "
								+ "number");
			alert(badAmt.message);
			reject(badAmt);
			return false;
		}
		if (withdrawAmount.gt(uData.deposited)) {
			let noToks = new Error("You don't have enough withdrawable tokens");
			alert(noToks.message);
			reject(noToks);
			return false;
		}

		// perform on-chain action
		await DaoPoolContract.methods.withdrawRegular(withdrawAmount.toFixed())
				.send({ from: userAcct })
			.then(receipt => {
				// pull updated amounts from event
				const withdrawnData = receipt.events["Withdrawn"].returnValues;
				const userDeposited = withdrawnData.userUnstaked;
				const amtWithdrawn = withdrawnData.amount;

				// user feedback
				alert("Successfully withdrew "
					+ web3.utils.fromWei(amtWithdrawn)
					+ " $ENSHROUD tokens from DAO Pool");

				// tell React context about updates
				// 1. user's deposited balance changes (all else remains same)
				const updUserData = {
					shares: uData.shares,
					delegatedTo: uData.delegatedTo,
					delegates: uData.delegates,
					deposited: userDeposited,
					unstakeAmount: uData.unstakeAmount,
					unstakeShares: uData.unstakeShares,
					unstakeScheduledFor: uData.unstakeScheduledFor,
					lastDelegationUpdateTimestamp:
						uData.lastDelegationUpdateTimestamp
				};
				props.setUserData(updUserData);
				// 2. user's $ENSHROUD balance increments
				let newBal = new BigNumber(props.userTokens).plus(amtWithdrawn);
				props.setUserTokens(newBal.toFixed());
				setWithdrawAmount(new BigNumber(0));
				resolve(true);
			})
			.catch(err => {
				alert("Error: code " + err.code + ", " + err.message);
				reject(err);
				return false;
			});
		return true;
	}

	// render UI form for withdrawing deposited tokens
	const dispValue = withdrawAmount.eq(0) ? ""
					: web3.utils.fromWei(withdrawAmount.toFixed());
	return (
		<Form>
			<Form.Group className="mb-3" controlId="increment">
				<Form.Label>
					Amount of deposited $ENSHROUD to withdraw from DAOPool:
				</Form.Label>
				<Form.Control type="text" size="8" value={dispValue}
					title={"Max: " + web3.utils.fromWei(uData.deposited)}
					onChange={handleWithdrawAmtChange} placeholder="0"
				/>
			</Form.Group>
			<LoadingButton variant="warning"
				buttonStyle="m-3"
				buttonText="Withdraw Tokens"
				buttonTitle="Withdraw amount from your deposited tokens"
				buttonIcon="images/x-circle.svg"
				netMethod={(resolve, reject) => doWithdraw(resolve, reject)}
			/>
		</Form>
	);
}

/* render a single claimable yield as a suitable table row
 * @param props.claimable an object containing these fields:
	contract: <address of erc20 token>,
	symbol: <symbol() of token contract>,
	priceFeed: price feed contract for this asset, if any (undefined if none)
	claimValue: <amount (wei) that user can claim (a BigNumber)>,
	lastClaim: <block number on which user last claimed yields in this asset>,
	selected: <true or false, based on whether currently selected by the user
 * @param props.selector function to toggle true or false, (claimable)
 * @param props.block the current block number
 * @param props.epochLength the number of blocks contained in an Epoch
 */
function SelectableClaim(props) {
	const { state: { chainConn, web3 } } = useEth();

	// local state for selected status
	const [itemSelected, setItemSelected] = useState(props.claimable.selected);

	// local state for market value column
	const [marketValue, setMarketValue] = useState("N/A");

	// shorthands
	const claim = props.claimable;
	const amt = web3.utils.fromWei(claim.claimValue.sd(8).toFixed());
	const aria = `select ${claim.contract} claim`;
	const address0 = "0x0000000000000000000000000000000000000000";
	var addrTitle = claim.contract;
	var truncAddr = claim.contract.substring(0,6) + '...'
					+ claim.contract.substring(38);
	var examinerLink =
		<>
		<a href={`${chainConn.chainConfig.scanURL}/address/${claim.contract}`} title="Examine token contract" target="_blank" rel="noreferrer noopener">
			{truncAddr}
		</a>&nbsp;&nbsp;
		<Image src="images/zoom-in.svg" height={20} width={20} fluid rounded />
		</>;
	// don't show link for ETH (native token)
	if (claim.contract === address0) {
		truncAddr = 'ETH';
		addrTitle = '';
		examinerLink = "(unavailable for native token)";
	}
	var blockDiff = props.block;
	if (claim.lastClaim > 0) {
		blockDiff -= claim.lastClaim;
	}
	else {
		// zero means user has never claimed this; assume Epoch has passed
		blockDiff = 0;
	}
	const available = blockDiff >= props.epochLength || blockDiff === 0;
	const blockText = blockDiff === 0 ? "0 (never)" : blockDiff;
	const badge
		= <Badge bg={available ? "success" : "danger"}>{blockText}</Badge>;
	const badgeTitle = `Green: claimable; Red: need ${props.epochLength} blocks since last claim`;

	// method to change selection status and sync with claim object
	function changeSelected(toggle) {
		// disallow claim if amount is zero
		if (toggle && amt <= 0) {
			alert("Cannot claim " + claim.symbol + ", amount is 0");
			const chk = document.getElementById(truncAddr);
			if (chk !== null) chk.checked = false;
			return;
		}
		setItemSelected(toggle);
		props.selector(claim, !itemSelected);
	}

	// if we have a price feed and a non-zero amt, calculate USD value of claim
	async function getMarketPrice() {
		var mktPrice = undefined;
		if (claim.priceFeed !== undefined && claim.priceFeed !== '') {
			const clFeed = new ChainLinkPriceFeed();
			const usdPrice = await clFeed.getPrice(web3, claim.priceFeed);
			if (usdPrice !== undefined) {
				mktPrice = usdPrice.decimalPlaces(4);
			}
			if (mktPrice !== undefined) {
				const mktValue = mktPrice.times(amt);
				setMarketValue(mktValue.decimalPlaces(4).toString());
			}
		}
	}
	if (amt > 0) {
		getMarketPrice();
	}

	// render the claimable claim as a table row
	return (
		<tr align="center" valign="middle">
			<td>
				<Form.Check type="checkbox" id={truncAddr} disabled={!available}
					name="claimSelector" selected={itemSelected}
					onClick={(claim) => changeSelected(!itemSelected)}
					value={claim.contract} aria-label={aria}
				/>
			</td>
			<td title="Token symbol">{claim.symbol}</td>
			<td title="Amount of your claim (ethers scale)">{amt}</td>
			<td title="Estimated USD value of your claim, if known">
				{marketValue}
			</td>
			<td title={addrTitle}>{examinerLink}</td>
			<td title={badgeTitle}>{badge}</td>
		</tr>
	);
}

/* show UI to claim yields, as ERC20+ETH, or as deposits to EnshroudProtocol
 * @param props.stakedShares number of tokens currently staked in DAOPool
 * @param props.totalShares total number of shares currently staked in DAOPool
 * @param props.setTotalShares set the number of total staked shares
 * @param props.totalStake total amount of $ENSHROUD currently staked in DAOPool
 * @param props.setTotalStake set the number of total staked tokens
 * @param props.epochLength the number of blocks contained in an Epoch
 */
function ActionClaimYields(props) {
	// enable use of our contracts and wallet accounts
	const { state: { accounts, contracts, chainConn, web3 } } = useEth();
	const DaoPoolContract = contracts["DAOPool"];
	const DaoAddress = DaoPoolContract.options.address;
	const ProtocolContract = contracts["EnshroudProtocol"];
	const TokenContract = contracts["EnshroudToken"];
	const EnshroudTokenAddr = TokenContract.options.address;
	const userAcct = accounts[0];

	// max number of tokens for which yield can be claimed (DAOPool.ARRAY_LIMIT)
	const ARRAY_LIMIT = 20;

	// "address" used to represent ETH (or other native token)
	const address0 = "0x0000000000000000000000000000000000000000";

	// the list of all preconfig assets on this chain (WETH will be among them)
	const protocolAssets = [].concat(chainConn.chainConfig.assetList);

	// local state for most recent stake change for this user
	const [lastStakeBlock, setLastStakeBlock] = useState(0);

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

	// local state for switch controlling regular claims vs. eNFT deposits
	const [eNFTswitch, setENFTswitch] = useState(false);

	// local state for token contracts, claimable balances, and last claim
	const [claimCandidates, setClaimCandidates] = useState([]);

	// local state for an additional token contract to claim yields on
	const [tokenAddress, setTokenAddress] = useState("");

	// process a change to the additional contract address form field
	const handleContractChange = e => {
		if (e.target.value === '') {
			setTokenAddress("");
		} else if (/^0x[0-9a-fA-F]+/.test(e.target.value)) {
			const bareAddr = web3.utils.stripHexPrefix(e.target.value);
			const addr = "0x" + bareAddr;
			if (web3.utils.isAddress(addr)) {
				setTokenAddress(web3.utils.toChecksumAddress(addr));
			} else {
				alert("Invalid token contract address, " + addr);
			}
		}
	};

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

	// obtain user's most recent update to staking
	const fetchUserLastStakeChange = async () => {
		let stakeBlock = await DaoPoolContract.methods.mostRecentStake(userAcct)
													.call( { from: userAcct });
		if (stakeBlock !== undefined) {
			// might be zero
			setLastStakeBlock(stakeBlock);
		}
	};
	// this merely needs to finish before the user clicks the Refresh button
	fetchUserLastStakeChange();

	/* obtain the balance of the DAOPool contract in a given ERC20 token
	 * @param cAddr the erc20 contract for which we want the DAOPool balance
	 */
	const fetchDAOBalance = async (cAddr) => {
		if (cAddr === undefined || cAddr === null) return "0";
		var daoBalance = "0";
		const callData = web3.eth.abi.encodeFunctionCall({
			name: 'balanceOf',
			type: 'function',
			constant: true,
			inputs: [{
					name: '',
					type: 'address'
			}],
			outputs: [{
					name: '',
					type: 'uint256'
			}],
		}, [DaoAddress]);
		await web3.eth.call({
			to: cAddr,		// erc20/erc777/erc4626 contract address
			data: callData	// function call encoding
		})
			.then(balance => {
				daoBalance = web3.eth.abi.decodeParameter('uint256', balance);
			}).catch(err => {
				console.error("error fetching DAOPool balance at " + cAddr
							+ ": " + err);
			})
		return daoBalance;
	};

	// obtain the DAOPool's current ETH balance
	const fetchDAOEthBalance = async () => {
		var ethBal = "0";
		await web3.eth.getBalance(DaoAddress)
			.then(balance => { ethBal = balance });
		return ethBal;
	};

	/* get symbol of an erc20-compatible contract
	 * @param tokenAddr the address of the contract
	 */
	const getTokenSymbol = async (tokenAddr) => {
		var symbol = '';
		const callData = web3.eth.abi.encodeFunctionCall({
			name: 'symbol',
			type: 'function',
			constant: true,
			inputs: []
		}, []);
		await web3.eth.call({
			to: tokenAddr,	// erc20/erc777/erc4626 contract address
			data: callData	// function call encoding
		})
			.then(sym => {
				symbol = web3.eth.abi.decodeParameter('string', sym);
			}).catch(err => {
				console.error("error fetching symbol at " + tokenAddr + ": "
							+ err);
			})
		return symbol;
	};

	/* get block number when user last claimed an asset
	 * @param contract the erc20 contract for which we want the last claim block
	 */
	const getLastClaim = async (contract) => {
		let claimBlock
			= await DaoPoolContract.methods.claimed(userAcct, contract)
													.call( { from: userAcct });
		return claimBlock;
	};

	/* method to toggle claimable assets for claim, subject to availability
	 * @param claimAsset the claimable asset involved
	 * @param selected whether toggle should be set to true or false
	 */
	function setSelected(claimAsset, selected) {
		if (selected) {
			if (claimAsset.selected) {
				// nothing to do
				return;
			}
			if (currBlock - claimAsset.lastClaim < props.epochLength) {
				// illegal
				console.error("attempt to select non-claimable asset "
							+ claimAsset.symbol);
				return;
			}
			claimAsset.selected = true;
		}
		else {
			if (!claimAsset.selected) {
				// nothing to do
				return;
			}
			claimAsset.selected = false;
		}
		// update the item
		setClaimCandidates(claimCandidates.map(ca => {
			if (ca.contract === claimAsset.contract) {
				return claimAsset;
			} else {
				return ca;
			}
		}));
	}

	/* Obtain a list of all DepositERC20, DepositETH, WithdrawERC20, and
	 * WithdrawETH events which have occurred in the EnshroudProtocol contract
	 * since the block number of the user's last update to their staking.
	 * NB: this won't return every token which could be claimed against, only
	 * the most recent ones.  Ones which haven't been used recently can be
	 * claimed via the user manually adding the contract address.
	 * We then check balances and entitlements, and get the last claimed block.
	 * All this data is stored in claimCandidates[] state and used to construct
	 * the table from which user will pick their tokens to claim.
	 */
	const fetchClaimList = async () => {
		// begin by getting current block as of click
		const blockNow = await getCurrBlock();

		// get list of deposited ERC20 events
		var depErcList = await ProtocolContract.getPastEvents('DepositERC20',
		{
			fromBlock: lastStakeBlock,
		})
		.catch(err => {
			console.error(err);
			alert("DepositERC20 event fetch error: code " + err.code + ", "
				+ err.message);
		});

		// NB: list of deposit/withdraw ETH events is assumed (always on list)

		// now get list of withdrawn ERC20 events
		var withErcList = await ProtocolContract.getPastEvents('WithdrawERC20',
		{
			fromBlock: lastStakeBlock,
		})
		.catch(err => {
			console.error(err);
			alert("WithdrawERC20 event fetch error: code " + err.code + ", "
				+ err.message);
		});

		/* NB: props.userShares only changes when user changes staking, and
		 * we're working after their last staking change.  So even if they just
		 * now changed it, we should already have the latest figure.
		 */

		// next, re-obtain totalShares because it could have changed
		var totalShares
			= await DaoPoolContract.methods.totalSharesAt(blockNow)
													.call({ from: userAcct });
		if (totalShares !== undefined) {
			let totShares = new BigNumber(totalShares);
			if (!totShares.eq(props.totalShares)) {
				// did change: update property too
				props.setTotalShares(totShares);
			}
		}
		else totalShares = props.totalShares;

		// likewise re-obtain totalStaked because other users could have altered
		var totStaked = await DaoPoolContract.methods.totalStake()
													.call({ from: userAcct });
		if (totStaked !== undefined) {
			// subtract 1 wei that was seeded by constructor
			let totStake = new BigNumber(totStaked).minus(1);
			if (!totStake.eq(props.totalStake)) {
				// did change: update property too
				props.setTotalStake(totStake);
			}
			totStaked = totStake.toFixed();
		}
		else totStaked = props.totalStake;

		// if no shares staked, we're done (we'll calculate NaN repeatedly)
		if (totStaked <= 0) {
			alert("No shares staked in DAOPool, no claims possible");
			return;
		}

		// we also need to know the total deposited unstaked $ENSHROUD tokens
		const depositedUnstaked
			= await DaoPoolContract.methods.depositedUnstaked()
													.call({ from: userAcct });

		// compute share for user (share of yields user is entitled to)
		const userStake = props.stakedShares.div(totalShares).times(totStaked);

		// get the DAOPool balance of WETH (or other native token wrapped asset)
		var nativeAsset = protocolAssets.find(elt => elt.method === 'native');
		var ethBal = await fetchDAOEthBalance();
		// find when user last claimed yields in native asset (address(0))
		var wrappedClaim = await getLastClaim(address0);

		var candidates = [];
		// start with ETH (or native token) which is always present on the list
		var claimable = userStake.div(totStaked).times(ethBal);
		if (!claimable.isNaN()) {
			candidates.push({	contract: address0,
								symbol: nativeAsset.symbol,
								priceFeed: nativeAsset.priceFeed,
								claimValue: claimable,
								lastClaim: wrappedClaim,
								selected: false	});
		}

		// parse deposit events into array of claim candidates
		for await (const depEvent of depErcList) {
			const contractAddr = web3.utils.toChecksumAddress(
										depEvent.returnValues.tokenContract);
			// see whether we have this one yet
			if (!candidates.some(elt => elt.contract === contractAddr)) {
				// get the symbol too
				const tokenSymbol = await getTokenSymbol(contractAddr);
				// get the balance the DAOPool has in this token
				const daoBal = await fetchDAOBalance(contractAddr);
				// see when the user last claimed this asset type
				const claimBlock = await getLastClaim(contractAddr);
				if (contractAddr === EnshroudTokenAddr) {
					// special rule allowing for staked and unstaked bals
					const adjBal = new BigNumber(daoBal)
								.minus(totStaked).minus(depositedUnstaked);
					claimable = userStake.div(totStaked).times(adjBal);
				}
				else {
					// the entire balance is claimable by stakers
					claimable = userStake.div(totStaked).times(daoBal);
				}
				if (!claimable.isNaN()) {
					// obtain price feed if we have one
					var mktPriceFeed = undefined;
					const chAsset = chainConn.chainConfig.assetList.find(
								elt => elt.contractAddress === contractAddr);
					if (chAsset !== undefined) {
						mktPriceFeed = chAsset.priceFeed;
					}
					// add asset to list
					candidates.push({	contract: contractAddr,
										symbol: tokenSymbol,
										priceFeed: mktPriceFeed,
										claimValue: claimable,
										lastClaim: claimBlock,
										selected: false	});
				}
			}
		}

		// parse withdraw events into array of claim candidates
		for await (const withEvent of withErcList) {
			const contractAddr = web3.utils.toChecksumAddress(
										withEvent.returnValues.tokenContract);
			// see whether we have this one yet
			if (!candidates.some(elt => elt.contract === contractAddr)) {
				// get the symbol too
				const tokenSymbol = await getTokenSymbol(contractAddr);
				// get the balance the DAOPool has in this token
				const daoBal = await fetchDAOBalance(contractAddr);
				// see when the user last claimed this asset type
				const claimBlock = await getLastClaim(contractAddr);
				if (contractAddr === EnshroudTokenAddr) {
					// special rule allowing for staked and unstaked bals
					const adjBal = new BigNumber(daoBal)
								.minus(totStaked).minus(depositedUnstaked);
					claimable = userStake.times(adjBal).div(totStaked);
				}
				else {
					// the entire balance is claimable by stakers
					claimable = userStake.times(daoBal).div(totStaked);
				}
				if (!claimable.isNaN()) {
					// obtain price feed if we have one
					var mrktPriceFeed = undefined;
					const chAsset = chainConn.chainConfig.assetList.find(
								elt => elt.contractAddress === contractAddr);
					if (chAsset !== undefined) {
						mrktPriceFeed = chAsset.priceFeed;
					}
					// add asset to list
					candidates.push({	contract: contractAddr,
										symbol: tokenSymbol,
										priceFeed: mrktPriceFeed,
										claimValue: claimable,
										lastClaim: claimBlock,
										selected: false	});
				}
			}
		}

		// sort array descending by claimValue (doesn't reflect economic value)
		candidates.sort((a, b) => b.claimValue - a.claimValue);

		// update state with all candidates (destructive replace)
		setClaimCandidates(candidates);
	};

	/* method to look up the claimable yield for a given token address
	 * (assumes props.totalStake, props.totalShares values are updated)
	 */
	async function checkForTokenYield(resolve, reject) {
		if (tokenAddress === '') {
			const noToken = new Error("No token address entered");
			alert(noToken.message);
			reject(noToken);
			return false;
		}

		// see if this contract is already in the list
		if (claimCandidates.some(elt => elt.contract === tokenAddress)) {
			const inList = new Error("Token already found in table");
			alert(inList.message);
			reject(inList);
			return false;
		}

		// compute share for user (portion of yields user is entitled to)
		const userStake
			= props.stakedShares.times(props.totalStake).div(props.totalShares);

		// get the symbol too
		const tokenSymbol = await getTokenSymbol(tokenAddress);
		// get the balance the DAOPool has in this token
		const daoBal = await fetchDAOBalance(tokenAddress);
		// see when the user last claimed this asset type
		const claimBlock = await getLastClaim(tokenAddress);
		var claimable = new BigNumber(0.0);

		// compute yield available, if any
		if (tokenAddress === EnshroudTokenAddr) {
			// we also need to know the total deposited unstaked $ENSHROUD
			const depositedUnstaked
				= await DaoPoolContract.methods.depositedUnstaked()
													.call({ from: userAcct });
			// special rule allowing for staked and unstaked bals
			const adjBal = new BigNumber(daoBal)
						.minus(props.totalStake).minus(depositedUnstaked);
			claimable = userStake.times(adjBal).div(props.totalStake);
		}
		else {
			// the entire balance is claimable by stakers
			claimable = userStake.times(daoBal).div(props.totalStake);
		}
		if (claimable.gt(0)) {
			// add the row to the table
			setClaimCandidates([...claimCandidates,
								{	contract: tokenAddress,
									symbol: tokenSymbol,
									claimValue: claimable,
									lastClaim: claimBlock,
									selected: false	}
								]);
			setTokenAddress("");
		}
		else {
			alert("No claimable yield for " + tokenSymbol + " at this time");
		}
		resolve(true);
		return true;
	}

	// button to trigger fetches
	const availClaimsFetcher = 
		<Form.Group className="mb-3" controlId="daoData">
			<Form.Label>Click to refresh yield data:</Form.Label>
			<Button variant="info" className="m-3"
				title="Obtain the latest data on claimable DAOPool yields"
				onClick={() => fetchClaimList()}
			>
				Get/Refresh Yield Info
			</Button>
		</Form.Group>;

	/* method to perform on-chain claim of yields.
	 * if eNFTswitch is true, call claimTokenYieldAsENFTs(), else claimYield()
	 */
	async function claimYields(resolve, reject) {
		// ensure that user has staked previously
		if (+lastStakeBlock === 0 || props.stakedShares.eq(0)) {
			const noStake = new Error("You must stake shares in the DAOPool "
									+ "before you can claim earnings.  You "
									+ "currently have "
									+ props.stakedShares.toString()
									+ " shares.");
			alert(noStake.message);
			reject(noStake);
			return false;
		}

		// ensure that an Epoch has passed since the user's last staking change
		const epochPassed = +lastStakeBlock + +props.epochLength;
		if (currBlock < epochPassed) {
			const noEpoch = new Error("You must wait for an Epoch to pass "
									+ "since your last staking change before "
									+ "you can make any claims.  Claims will "
									+ "be available after block: "
									+ epochPassed + ".");
			alert(noEpoch.message);
			reject(noEpoch);
			return false;
		}

		// build list of contract addresses to send (do not include ETH)
		const claimedContracts = [];
		const selectedClaims
			= claimCandidates.filter(elt => elt.selected === true);
		/* track whether they're manually checking ETH (always claimed if
		 * possible within contract method)
		 */
		var claimingEth = false;
		selectedClaims.forEach((elt) => {
			if (elt.contract !== address0) {
				claimedContracts.push(elt.contract);
			}
			else claimingEth = true;
		});

		// make sure we don't have too many selected
		if (claimedContracts.length > ARRAY_LIMIT) {
			const tooMany = new Error("Too many yield claims selected, max of "
									+ ARRAY_LIMIT + " per claim, plus ETH");
			alert(tooMany.message);
			reject(tooMany);
			return false;
		}

		// make sure the user selected something
		if (selectedClaims.length === 0 && !claimingEth) {
			alert("No claimable yields selected; proceeding will attempt to "
				+ "claim native tokens only");
		}

		// make sure no zero amounts were selected (smart contract will reject)
		const zeroClaims = selectedClaims.filter(elt => elt.claimValue.eq(0));
		if (zeroClaims.length > 0) {
			const zClaim = new Error(zeroClaims.length
									+ " claimable yields are zero");
			alert(zClaim.message);
			reject(zClaim);
			return false;
		}

		// special rule for claiming ETH as eNFTs only
		if (claimingEth && claimedContracts.length === 0 && eNFTswitch) {
			// call specialized method to save gas
			await DaoPoolContract.methods.claimETHYieldAsENFTs()
					.send( { from: userAcct })
				.then(receipt => {
					alert("Successfully claimed native token yields as an "
						+ "eNFT deposit");
					// clear array to force a re-Get
					setClaimCandidates([]);
				})
				.catch(err => {
					alert("Error: code " + err.code + ", " + err.message);
					reject(err);
					return false;
				});
			resolve(true);
			return true;
		}

		// build list of tokens we're trying to claim
		var claimedAssetList = claimingEth ? "ETH" : "(ETH)";
		claimedContracts.forEach((contract) => {
			const selClaim
				= selectedClaims.find((elt) => elt.contract === contract);
			claimedAssetList += (", " + selClaim.symbol);
		});

		// based on the switch setting, call the proper method
		if (!eNFTswitch) {
			// NB: this also does ETH, even if claimedContracts[] is empty
			await DaoPoolContract.methods.claimYield(claimedContracts)
					.send( { from: userAcct })
				.then(receipt => {
					// feedback to user
					alert("Successful attempt to claim: " + claimedAssetList);
					// clear array to force a re-Get
					setClaimCandidates([]);
				})
				.catch(err => {
					alert("Error: code " + err.code + ", " + err.message);
					reject(err);
					return false;
				});
		}
		else {
			// NB: this also does ETH in addition to any contracts listed
			await DaoPoolContract.methods.claimTokenYieldAsENFTs(
															claimedContracts)
					.send( { from: userAcct })
				.then(receipt => {
					// feedback to user
					alert("Successful attempt to claim as eNFT deposits: "
						+ claimedAssetList);
					// clear array to force a re-Get
					setClaimCandidates([]);
				})
				.catch(err => {
					alert("Error: code " + err.code + ", " + err.message);
					reject(err);
					return false;
				});
		}
		resolve(true);
		return true;
	}

	// render UI/UX for claiming
	return (
		<>
		{ /* show instructions */ }
		<Card>
			<Card.Body>
				<Card.Title>
					Claiming Yields on Your DAO Pool Staking
				</Card.Title>
				<Card.Subtitle>Policies and Procedures</Card.Subtitle>
				<Card.Text>
					<br/>
					<b>Notice: before using this function, you must click on
					the "Get/Refresh DAO Pool Info" button at the top of the
					page.
					</b>
					<br/><br/>
					To use this function, you must previously have staked
					$ENSHROUD in the <i>DAOPool</i> contract.  (See the section
					"Stake Deposited Tokens" above to do this.)  You can then
					claim earnings after leaving your staking in place for at
					least a week (one "Epoch", formally defined
					as {props.epochLength} blocks).
					<br/><br/>
					<ListGroup numbered>
						<ListGroup.Item>
							Whenever users deposit or withdraw tokens to/from
							the <i>EnshroudProtocol</i> contract, fees are
							extracted.  These fees are set by the EnshroudDAO,
							and can vary per asset.  A total of 95% of all
							protocol earnings are transferred into
							the <i>DAOPool</i> contract to supply yield to
							stakers.  (The remaining 5% goes to the Enshroud
							Treasury.)
						</ListGroup.Item>
						<ListGroup.Item>
							By staking $ENSHROUD, you acquire "shares" in the
							pool.  The amount of yield you can claim is
							calculated based on the ratio of your shares
							compared with the total shares in the pool, across
							all users.  For example, if you have 10 shares and
							there are 1000 shares total, you would be entitled
							to 1% of the earnings in every token asset.
						</ListGroup.Item>
						<ListGroup.Item>
							While timelocked $ENSHROUD tokens can be staked in
							the <i>DAOPool</i>, only unlocked $ENSHROUD will
							earn yields.  Unlocked tokens can be purchased with
							ETH via
							the <i>Crowdsale</i> contract on Eth Mainnet (see
							the <b>Staking/Buy $ENSHROUD</b> menu item; or
							withdrawn from a Timelock and staked as they become
							unlocked; or (possibly) obtained from other holderss
							via an AMM or other secondary market independent
							of Enshroud.
						</ListGroup.Item>
						<ListGroup.Item>
							Stakers can claim yields independently in each token
							asset where fee earnings have accrued.  All such
							tokens are ERC20-compatible, except for native
							tokens (e.g. ETH).
						</ListGroup.Item>
						<ListGroup.Item>
							An Epoch (~1 week) must elapse since your last
							staking change, in order to make claims.  An Epoch
							must also elapse between your claims
							of any particular token asset type.  That is, if
							you claimed yields in token X less than a week ago,
							you cannot claim yields in that token again until
							the next Epoch.  However, you may claim yields in
							other tokens Y and Z if you have not claimed them
							for at least one Epoch.
						</ListGroup.Item>
						<ListGroup.Item>
							To see what yields are available for you to claim
							at the present time, click on
							the <b>Get/Refresh Yield Info</b> button
							above the table.
							This will populate the table below with yields
							available for you to claim as of the current block.
							Each row provides a selector checkbox, a description
							of the token asset (both common symbol and the
							contract address), and number number of blocks since
							you last claimed yields for that token.  (A value
							of 0 means you have never done so, in which case
							you can claim now.)  The badge will be green if a
							claim is possible now, and red otherwise.
						</ListGroup.Item>
						<ListGroup.Item>
							Be aware that multiple contracts can share the same
							symbol.  To help you differentiate them, a block
							explorer link is provided for each token contract.
						</ListGroup.Item>
						<ListGroup.Item>
							The table is sorted in descending order based on
							yield amounts accrued in each token.  Please note
							that <b>this does not reflect the value</b> of the
							claimable yield, as asset prices vary with the
							market.  The native token for this blockchain (such
							as ETH) will always be shown in the table.
							<br/><br/>
							Note: if native tokens are claimable by you, a claim
							will automatically be made by the smart contract
							whether or not you selected the native token.
							You may claim the native token by itself, by
							selecting it and making no other selections.
						</ListGroup.Item>
						<ListGroup.Item>
							Please note that tokens are shown only if they
							have had fee activity in them since you last
							updated your staking.  It is possible that a yield
							could exist in a token type seldom used within the
							Enshroud system, particularly if you have adjusted
							your stake relatively recently.  To check for this
							possibility, paste the token's contract address
							into the form field for "Additional Token" and click
							the <b>Check for Token Yield</b> button.  If there
							are any claimable yields available in that token,
							it will be added to the table.
							<br/><br/>
							Caution: if you use the <b>Get/Refresh Yield
							Info</b> button again, it will overwrite any
							additional table row entries which you added
							manually.
						</ListGroup.Item>
						<ListGroup.Item>
							You may select up to {ARRAY_LIMIT} tokens at a time
							for which you want to claim your yields (not
							including ETH, which isn't a contract).  Note that
							gas costs
							will increase slightly for each token added, so be
							careful not to claim small amounts of yield in
							assets with little market value.  These are best
							left to accumulate for a later claim.  There is no
							obligation to select any given token, now or ever.
						</ListGroup.Item>
						<ListGroup.Item>
							You also have a global option to claim your yields
							in the form of Enshroud eNFTs, for greater privacy.
							By default, all tokens will be withdrawn to your
							account balance.  (Note: you might need to +Import
							the tokens into your account in order to see their
							balances appear in your wallet.)
							<br/><br/>
							By setting the slider switch on for <b>eNFTs</b>,
							you will instead receive all your yields as a
							deposit to your account address in
							the <i>EnshroudProtocol</i> contract.  Once you
							have done this, you can then mint yourself (or
							other addresses) one or more Enshroud eNFTs for
							each asset you have on deposit.  (See
							the <b>Mint</b> menu item in the top menu bar.)
							<br/><br/>
							Whichever switch setting you select, your choice
							will apply to <b>all</b> of the assets you selected
							in the table.
							You may wish to claim some yields one way, but
							others separately using the opposite switch setting.
							<br/>
						</ListGroup.Item>
						<ListGroup.Item>
							<b>
								Important: if you do not claim your yields in
								any given Epoch, some of them may get claimed by
								other users.
							</b>
							This is because each user claims a percentage of
							the available earnings based on their share ratio.
							Therefore, unclaimed earnings effectively roll over
							into the available pool for the next Epoch.
							<br/><br/>
							The algorithm generally favors users who claim
							their shares earlier, although additional fees may
							also get deposited to the contract if you wait.
							Over time, claiming earnings regularly each Epoch
							will serve to maximize your yield.
						</ListGroup.Item>
						<ListGroup.Item>
							Use the <b>Claim Yields</b> button below the table
							to claim all selected yields according to the
							payment mechanism selected via the eNFTs switch.
							<br/><br/>
							Note: because of activities in parallel by other
							users, the actual yield amounts you will receive
							may differ from the values shown in the table.
						</ListGroup.Item>
					</ListGroup>
					<br/>
				</Card.Text>
			</Card.Body>
		</Card>
		<br/>

		<Form>
			{ /* provide button to fetch claimable yields table data */ }
			{ availClaimsFetcher }

			{ /* render the table of claimable items in a Table */ }
			<br/>
			<Container fluid>
				<Table striped bordered hover responsive>
					<caption className="caption-top">
						Your Claimable Yields as of Block {currBlock}
					</caption>
					<thead>
						<tr align="center" valign="middle">
							<th scope="col" title="Select assets in which you want to make claims">
								Select
							</th>
							<th scope="col" title="Symbol for each token asset">
								Symbol
							</th>
							<th scope="col"
								title="How much yield you will claim, converted from wei for readability">
								Amount (1e-18)
							</th>
							<th scope="col"
								title="How much your claim is worth in USD, if a market price feed for this asset is known">
								USD Value
							</th>
							<th scope="col" title="Click the button link to examine the contract">
								Asset / Token Contract
							</th>
							<th scope="col" title="Blocks since you last claimed yields in this token">
								Blocks Since Claim
							</th>
						</tr>
					</thead>
					<tbody>
						{claimCandidates.map((row) =>
							<React.Fragment key={"frag_" + row.contract}>
								<SelectableClaim
									claimable={row}
									selector={(claimAsset, selected) => setSelected(row, selected)}
									block={currBlock}
									epochLength={props.epochLength}
								/>
							</React.Fragment>
						)}
					</tbody>
				</Table>
			</Container>
			<br/><br/>

			{ /* supply entry form for additional token contract address */ }
			<Form.Group className="mb-3" controlId="addlTokenInput">
				<Form.Label>
					Enter an additional token contract address:
				</Form.Label>
				<InputGroup>
					<Form.Control type="text" value={tokenAddress}
						maxLength={45}
						title="The address of an ERC20 token to check for yield"
						onChange={handleContractChange} placeholder="0x"
					/>
					<LoadingButton variant="info"
						buttonStyle="m-3"
						buttonText="Check for Token Yield"
						buttonTitle="Examine your claimable yield for a token"
						buttonIcon="images/check2.svg"
						netMethod={(resolve, reject) => checkForTokenYield(resolve, reject)}
					/>
				</InputGroup>
			</Form.Group>
			<br/>

			{ /* provide switch for claiming yields as eNFTs */ }
			<div className="form-check form-switch">
				<Form.Group className="mb-3" controlId="enftSwitch">
					<Form.Label>
						Set Switch to Claim All Selected Yields as Deposits
						for eNFTs:
					</Form.Label>
					<Form.Switch id="enftSelect"
						selected={eNFTswitch} className="mx-auto"
						onClick={(active) => setENFTswitch(!eNFTswitch)}
						aria-label="select Enft mode"
					/>
				</Form.Group>
			</div>
			<br/>

			{ /* provide button for initiating yield claim */ }
			<LoadingButton variant="primary"
				buttonStyle="m-3"
				buttonText="Claim Yields"
				buttonTitle="Initiate claim and withdrawal of selected yields"
				buttonIcon="images/send-check.svg"
				netMethod={(resolve, reject) => claimYields(resolve, reject)}
			/>
		</Form>
		</>
	);
}

/* show the action options for the user in an accordion
 * @param props.userTokens the number of $ENSHROUD tokens the user owns (wei)
 * @param props.setUserTokens method for setting/updating user token balance
 * @param props.userData the User{} struct data for user in DAOPool
 * @param props.setUserData method for setting/updating User{} struct data
 * @param props.timeLock the Timelock for the user in DAOPool contract
 * @param props.setDAOTimelock set the DAOPool 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
 * @param props.stakedShares number of tokens currently staked in DAOPool
 * @param props.setStakedShares set the number of staked shares
 * @param props.totalShares total number of shares currently staked in DAOPool
 * @param props.setTotalShares set the number of total staked shares
 * @param props.totalStake total amount of $ENSHROUD currently staked in DAOPool
 * @param props.setTotalStake set the number of total staked tokens
 * @param props.epochLength number of blocks needed to fulfill an Epoch
 */
function DAOActions(props) {
	var actionsList =
		<>
			<hr/>
			<br/>
			<h3>Possible Actions</h3>
			<h4>Expand Desired Section Below</h4>
			<br/>
			<Accordion alwaysOpen>
				<Accordion.Item eventKey="0">
					<Accordion.Header>
						Deposit Tokens for Staking
					</Accordion.Header>
					<Accordion.Body>
						<p className="text-muted">(unlocked tokens only)</p>
						<ActionDeposit
							userTokens={props.userTokens}
							setUserTokens={props.setUserTokens}
							userData={props.userData}
							setUserData={props.setUserData}
						/>
					</Accordion.Body>
				</Accordion.Item>
				<Accordion.Item eventKey="1">
					<Accordion.Header>
						Stake Deposited Tokens
					</Accordion.Header>
					<Accordion.Body>
						<ActionStakeDeposited
							userData={props.userData}
							setUserData={props.setUserData}
							stakedShares={props.stakedShares}
							setStakedShares={props.setStakedShares}
							totalShares={props.totalShares}
							setTotalShares={props.setTotalShares}
						/>
					</Accordion.Body>
				</Accordion.Item>
				<Accordion.Item eventKey="2">
					<Accordion.Header>View and Claim Yields</Accordion.Header>
					<Accordion.Body>
						<ActionClaimYields
							stakedShares={props.stakedShares}
							totalShares={props.totalShares}
							setTotalShares={props.setTotalShares}
							totalStake={props.totalStake}
							setTotalStake={props.setTotalStake}
							epochLength={props.epochLength}
						/>
					</Accordion.Body>
				</Accordion.Item>
				<Accordion.Item eventKey="3">
					<Accordion.Header>
						Schedule Unstaking of Shares
					</Accordion.Header>
					<Accordion.Body>
						<ActionReduceStaking
							stakedShares={props.stakedShares}
							setStakedShares={props.setStakedShares}
							totalShares={props.totalShares}
							setTotalShares={props.setTotalShares}
							userData={props.userData}
							setUserData={props.setUserData}
							totalStake={props.totalStake}
						/>
					</Accordion.Body>
				</Accordion.Item>
				<Accordion.Item eventKey="4">
					<Accordion.Header>
						Unstake/Withdraw Tokens
					</Accordion.Header>
					<Accordion.Body>
						<ActionWithdrawFromStaking
							userData={props.userData}
							setUserData={props.setUserData}
							userTokens={props.userTokens}
							setUserTokens={props.setUserTokens}
							stakedShares={props.stakedShares}
							setStakedShares={props.setStakedShares}
							totalShares={props.totalShares}
							setTotalShares={props.setTotalShares}
							totalStake={props.totalStake}
							setTotalStake={props.setTotalStake}
						/>
					</Accordion.Body>
				</Accordion.Item>
				<Accordion.Item eventKey="5">
					<Accordion.Header>
						Withdraw Deposited Tokens
					</Accordion.Header>
					<Accordion.Body>
						<ActionWithdrawDeposited
							userData={props.userData}
							setUserData={props.setUserData}
							userTokens={props.userTokens}
							setUserTokens={props.setUserTokens}
						/>
					</Accordion.Body>
				</Accordion.Item>
				<Accordion.Item eventKey="6">
					<Accordion.Header>
						Withdraw Unlocked Tokens from Timelock
					</Accordion.Header>
					<Accordion.Body>
						<ActionWithdrawUnlocked
							withdrawable={props.withdrawable}
							setUnlocked={props.setUnlocked}
							mgrTimelock={props.mgrTimelock}
							setTMTimelock={props.setTMTimelock}
							userTokens={props.userTokens}
							setUserTokens={props.setUserTokens}
						/>
					</Accordion.Body>
				</Accordion.Item>
				<Accordion.Item eventKey="7">
					<Accordion.Header>
						Transfer Timelock to DAOPool
					</Accordion.Header>
					<Accordion.Body>
						<ActionStakeLocked
							withdrawable={props.withdrawable}
							setUnlocked={props.setUnlocked}
							userData={props.userData}
							setUserData={props.setUserData}
							mgrTimelock={props.mgrTimelock}
							setDAOTimelock={props.setDAOTimelock}
							setTMTimelock={props.setTMTimelock}
						/>
					</Accordion.Body>
				</Accordion.Item>
			</Accordion>
		</>;

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

/* show UI for calling DAOPool.importFees() (usable only by feesBot account)
 * @param props.userTokens the number of $ENSHROUD tokens the user owns (wei)
 * @param props.setUserTokens method for setting/updating user token balance
 */
function DAOFeeImport(props) {
	// enable use of our contracts and Metamask accounts
	const { state: { contracts, accounts, web3, chainConn } } = useEth();
	const userAcct = web3.utils.toChecksumAddress(accounts[0]);
	const daoPoolContract = contracts["DAOPool"];
	const daoPoolAddr = daoPoolContract.options.address;
	const enshTokenContract = contracts["EnshroudToken"];
	const tokenAddr = enshTokenContract.options.address;

	// chainId for which these fee earnings are to be imported
	const [feesChain, setFeesChain] = useState("0");

	// amount of native tokens (ETH or equiv.) to be imported (in ethers 1e-18)
	const [ethImported, setEthImported] = useState("0.0");

	// amount of $ENSHROUD to be imported (in ethers 1e-18)
	const [enshImported, setEnshImported] = useState("0.0");

	var chainId = 0;
	// method to get the current chainId
	function getChain() {
		web3.eth.getChainId().then(chain => {
			chainId = chain;
		});
	}
	getChain();

	// process a chainId value change
	const handleChainChange = e => {
		let chId = e.target.value;
		if (chId === '') {
			chId = '0';
		}
		if (/^[0-9]+/.test(chId)) {
			setFeesChain(chId);
		}
	};

	// process a change to the ETH amount field
	const handleEthAmtChange = async (e) => {
		var amt = new BigNumber(0.0);
		var inpVal = e.target.value.toString();
		if (inpVal === '') inpVal = "0.0";
		if (isNaN(inpVal)) inpVal = "0.0";
		// in order to support trailing zeros, track string value separately
		var depAmtStr = inpVal;
		var enteredAmt = new BigNumber(depAmtStr);
		if (enteredAmt.isNegative()) {
			depAmtStr = "0.0";
			enteredAmt = amt;
		}
		amt = amt.plus(enteredAmt);
		const oldAmt = new BigNumber(ethImported.toString());
		if (!amt.eq(oldAmt)) {
			depAmtStr = amt.toString();
		}
		setEthImported(depAmtStr);
	};

	// process a change to the $ENSHROUD amount field
	const handleEnshAmtChange = async (e) => {
		var amt = new BigNumber(0.0);
		var inpVal = e.target.value.toString();
		if (inpVal === '') inpVal = "0.0";
		if (isNaN(inpVal)) inpVal = "0.0";
		// in order to support trailing zeros, track string value separately
		var depAmtStr = inpVal;
		var enteredAmt = new BigNumber(depAmtStr);
		if (enteredAmt.isNegative()) {
			depAmtStr = "0.0";
			enteredAmt = amt;
		}
		amt = amt.plus(enteredAmt);
		const oldAmt = new BigNumber(enshImported.toString());
		if (!amt.eq(oldAmt)) {
			depAmtStr = amt.toString();
		}
		setEnshImported(depAmtStr);
	};

	// obtain approval from erc-20 compatible contract for our contract
	const obtainSpendApproval = async (tokenAddr, amtWei) => {
		var appr = true;
		if (amtWei <= 0 || tokenAddr === '') {
			console.error("obtainSpendApproval: missing input");
			return false;
		}
		const callData = web3.eth.abi.encodeFunctionCall({
			name: 'approve',
			type: 'function',
			constant: false,
			inputs: [{
				type: 'address',
				name: 'spender',
			},{
				type: 'uint256',
				name: 'amount',
			}],
			outputs: [{
				type: 'bool'
			}]
		}, [daoPoolAddr, amtWei]);
		await web3.eth.sendTransaction({
			to: tokenAddr,
			from: userAcct,
			data: callData,
			chainId: chainConn.chainConfig.chainId
		})
			.then(approval => {
				appr = approval;
				if (!appr) {
					console.error("approve(" + amtWei + ") failed");
				}
			}).catch(err => {
				appr = false;
				console.error("approve() failed for " + tokenAddr + ": "
							+ err.code + ", " + err.message);
			});
		return appr;
	};

	// submit fees to the DAOPool contract
	async function importDAOfees(resolve, reject) {
		// check we have a valid chainId entered
		if (feesChain === chainId) {
			const sameCh = new Error("Cannot import fees on same chain, "
									+ chainId);
			alert(sameCh.message);
			reject(sameCh);
			return false;
		}
		else if (feesChain === "0") {
			const noCh = new Error("No chainId entered");
			alert(noCh.message);
			reject(noCh);
			return false;
		}

		/* We need to build two matching arrays: the tokenContracts[] and
		 * amounts[].  To do so we find all those asset checkboxes which are
		 * currently checked, and also have a >0 amount of fees specified.
		 */
		const tokenContracts = [];
		const amounts = [];
		const checkboxList = document.querySelectorAll('[id^="select-"]');
		var ethWei = "0";
		for (const checkbox of checkboxList) {
			const token = checkbox.value;
			if (checkbox.checked) {
				if (token === "ENSHROUD") {
					if (enshImported > 0) {
						// add this to arrays, converting to wei
						tokenContracts.push(tokenAddr);
						amounts.push(web3.utils.toWei(enshImported));
					}
				} else if (token === "ETH") {
					if (ethImported > 0) {
						// pass as msg.value
						ethWei = web3.utils.toWei(ethImported);
					}
				} else alert("Invalid token selected, " + token);
			}
		}

		// make sure something was selected
		if (amounts.length === 0 && ethWei === "0") {
			const none = new Error("No fees selected");
			alert(none.message);
			reject(none);
			return false;
		}

		// make sure we have enough $ENSHROUD or ETH
		if (amounts.length > 0) {
			if (amounts[0] > props.userTokens) {
				const lackEnsh = new Error("Insufficient $ENSHROUD available, "
											+ "max = "
										+ web3.utils.fromWei(props.userTokens));
				alert(lackEnsh.message);
				reject(lackEnsh);
				return false;
			}
			// get approval for spending this much $E
			var appr = await obtainSpendApproval(tokenAddr, amounts[0]);
			if (!appr) {
				const noApp = new Error("Approval to spend $ENSHROUD failed");
				alert(noApp.message);
				reject(noApp);
				return false;
			}
		}

		// submit to smart contract
		await daoPoolContract.methods.importFees(feesChain,
												 tokenContracts,
												 amounts)
				.send({ from: userAcct, value: ethWei })
			.then(receipt => {
				if (amounts.length > 0) {
					const newEnshBal = new BigNumber(props.userTokens)
									.minus(amounts[0]);
					props.setUserTokens(newEnshBal.toFixed());
					setEthImported("0.0");
					setEnshImported("0.0");
					// clear checkboxes
					for (const checkbox of checkboxList) {
						checkbox.checked = false;
					}
				}
			})
			.catch(err => {
				alert("Error importing fees for " + feesChain + ":\n"
					+ err.message);
				reject(err);
				return false;
			});
		resolve(true);
		return true;
	}

	// render UI
	return (
		<>
		<br/>
		<h3>Import Fees from Foreign Chain</h3>
		<br/>
		{ /* form field to enter target chain Id */ }
		<Form.Group className="mb-3" controlId="chainIdEntry">
			<Form.Label>Chain Id Where Fees Originate</Form.Label>
			<Form.Control type="text" value={feesChain} placeholder="0"
				onChange={handleChainChange}
			/>
		</Form.Group>
		<br/>
		<Table striped bordered hover responsive variant="dark">
			<caption className="caption-top">
				$ENSHROUD and ETH to Be Added:
			</caption>
			<thead>
				<tr align="center" key="feesHeader">
					<th scope="col">Select</th>
					<th scope="col">Asset</th>
					<th scope="col">Fee Amount (1e-18)</th>
				</tr>
			</thead>
			<tbody>
				<tr align="center" valign="middle" key="enshFees">
					{ /* selectable checkbox */ }
					<td>
						<Form.Check type="checkbox" id="select-ensh"
							name="feeSelect" className="m-2" value="ENSHROUD"
						/>
					</td>
					{ /* Name */ }
					<td>{"$ENSHROUD (" + tokenAddr + ")"}</td>
					{ /* fee quantum */ }
					<td>
						<Form.Group className="mb-3" controlId="feesEnshroud">
							<Form.Label>Fees to Import</Form.Label>
							<Form.Control type="text"
								value={enshImported} placeholder="0.0"
								onChange={handleEnshAmtChange}
							/>
						</Form.Group>
					</td>
				</tr>
				<tr align="center" valign="middle" key="ethFees">
					{ /* selectable checkbox */ }
					<td>
						<Form.Check type="checkbox" id="select-eth"
							name="feeSelect" className="m-2" value="ETH"
						/>
					</td>
					{ /* Name */ }
					<td>ETH</td>
					{ /* fee quantum */ }
					<td>
						<Form.Group className="mb-3" controlId="feesEth">
							<Form.Label>Fees to Import</Form.Label>
							<Form.Control type="text"
								value={ethImported} placeholder="0.0"
								onChange={handleEthAmtChange}
							/>
						</Form.Group>
					</td>
				</tr>
			</tbody>
		</Table>
		<br/>

		<LoadingButton variant="success"
			buttonStyle="m-3"
			buttonText="Import Fees"
			buttonIcon="images/plus-circle.svg"
			buttonTitle="Import fees for each checked asset as indicated"
			netMethod={(resolve, reject) => importDAOfees(resolve, reject)}
		/>
		</>
	);
}

/* main UI method to manage user DAOPool stakings and reward claims
 * @param props.userTokens the number of $ENSHROUD tokens the user owns (wei)
 * @param props.setUserTokens method for setting/updating user token balance
 * @param props.userData the User{} struct data for the user in DAOPool
 * @param props.setUserData method for setting/updating User{} struct
 * @param props.timeLock the Timelock for the user in DAOPool contract
 * @param props.setDAOTimelock set the DAOPool 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
 * @param props.stakedShares number of shares user currently staked in DAOPool
 * @param props.setStakedShares set the number of staked shares
 * @param props.totalShares total number of shares currently staked in DAOPool
 * @param props.setTotalShares set the number of total staked shares
 * @param props.totalStake total amount of $ENSHROUD currently staked in DAOPool
 * @param props.setTotalStake set the number of total staked tokens
 */
function DAOManagement(props) {
	// enable use of our wallet accounts
	const { state: { accounts } } = useEth();
	const userAcct = accounts[0];

	// state for number of blocks that constitute an Epoch on this chain
	const [epochLength, setEpochLength] = useState(0);

	// state for the unlocking quantity of $ENSHROUD tokens in DAOPool Timelock
	const [unlockableTokens, setUnlockableTokens] = useState(new BigNumber(0));

	// do actual rendering of sections for possible actions
	return (
		<div className="enshDAOManagement">
			<h2 align="center">
				DAO Pool Stakings for Yields
			</h2>
			<br/><br/>
			<h3 align="center">
				Stake your $ENSHROUD Tokens to Earn Shares of 95%
				of <i>EnshroudProtocol</i> Fee Revenue
			</h3>
			<GetDAOStaking
				setUserTokens={props.setUserTokens}
				setUserData={props.setUserData}
				setDAOTimelock={props.setDAOTimelock}
				mgrTimelock={props.mgrTimelock}
				setTMTimelock={props.setTMTimelock}
				withdrawable={props.withdrawable}
				setUnlocked={props.setUnlocked}
				setStakedShares={props.setStakedShares}
				setTotalShares={props.setTotalShares}
				setTotalStake={props.setTotalStake}
				epochLength={epochLength}
				setEpochLength={setEpochLength}
			/>
			<DAOStakingRenderer
				user={userAcct}
				userTokens={props.userTokens}
				userData={props.userData}
				setUserData={props.setUserData}
				timeLock={props.timeLock}
				setDAOTimelock={props.setDAOTimelock}
				mgrTimelock={props.mgrTimelock}
				withdrawable={props.withdrawable}
				stakedShares={props.stakedShares}
				totalShares={props.totalShares}
				unlockableTokens={unlockableTokens}
				setUnlockableTokens={setUnlockableTokens}
			/>
			<br/>
			<DAOActions
				userTokens={props.userTokens}
				setUserTokens={props.setUserTokens}
				userData={props.userData}
				setUserData={props.setUserData}
				timeLock={props.timeLock}
				setDAOTimelock={props.setDAOTimelock}
				mgrTimelock={props.mgrTimelock}
				setTMTimelock={props.setTMTimelock}
				withdrawable={props.withdrawable}
				setUnlocked={props.setUnlocked}
				stakedShares={props.stakedShares}
				setStakedShares={props.setStakedShares}
				totalShares={props.totalShares}
				setTotalShares={props.setTotalShares}
				totalStake={props.totalStake}
				setTotalStake={props.setTotalStake}
				epochLength={epochLength}
			/>
			<br/>
			<DAOFeeImport
				userTokens={props.userTokens}
				setUserTokens={props.setUserTokens}
			/>
			<br/>
		</div>
	);
}

export default DAOManagement;
