/*
 * last modified---
 * 	06-06-26 add DOM ids for automation support
 * 	04-07-25 supply AES keys with MVO withdraw requests
 * 	03-19-25 implement chain.link price feeds
 * 	07-26-24 minor wording tweaks
 * 	01-16-24 fix sync problem with SelectableEnft checkboxes
 * 	01-04-24 add explicit Net Credit column to Burn Configuration Summary table
 * 	12-12-23 import PayeeConfig from its own file
 * 	11-21-23 do amount accounting only in BigNumberS
 * 	11-17-23 debug; add display of withdraw fees (<TotalItem> and computed fee
 *			 total (<BurnConfigSummary>)
 * 	11-06-23 rework replacePayeeConfig() => resetPayeeConfig()
 * 	10-23-23 refactor to store eNFTs[] in local state and establish availability
 * 	10-18-23 remove any use of payeeConfigs passed in from Enshroud
 * 	10-06-23 move SmartContractSubmitter invocation here from Enshroud
 * 	08-31-23 new
 *
 * purpose---
 * 	UI for redeeming eNFTs
 */

import React, { useState, Fragment } from 'react';
import Container from 'react-bootstrap/Container';
import Table from 'react-bootstrap/Table';
import Button from 'react-bootstrap/Button';
import Image from 'react-bootstrap/Image';
import Form from 'react-bootstrap/Form';
import Accordion from 'react-bootstrap/Accordion';
import { useAccordionButton } from 'react-bootstrap/AccordionButton';
import ListGroup from 'react-bootstrap/ListGroup';
import InputGroup from 'react-bootstrap/InputGroup';
import Badge from 'react-bootstrap/Badge';
import useEth from './EthContext/useEth';
import MVOComm from './MVOComm.js';
import SmartContractSubmitter from './SmartContractSubmitter.jsx';
import LoadingButton from './LoadingButton.jsx';
import PayeeConfig from './PayeeConfig.js';
import NoticeWrongNetwork, { NoticeNoArtifact } from './Notices.jsx';
import DAlert from './DAlert.jsx';
import ChainLinkPriceFeed from './priceFeeds.js';
const BigNumber = require("bignumber.js");


// max number of array entries (inputs or outputs), EnshroudProtocol.ARRAY_LIMIT
const ARRAY_LIMIT = 20;

/* render an asset total as a table row, with radio selector
 * @param props.asset the token contract address involved
 * @param props.amount total value user has in this asset in eNFTs
 * @param props.avail sum of all eNFTs of this type (max that can be burned)
 * @param props.id the value to use as key for <tr/>
 * @param props.selectedConfig active/selected PayeeConfig
 * @param props.onConfigSelect method to call to report config is active or not
 * @param props.onTotalChange method to call when total burn amount changes
 * @param props.tokenSymbols the list of token symbols we know about so far
 */
function TotalItem(props) {
	// enable use of Web3
	const { state: { accounts, contracts, chainConn, web3 } } = useEth();
	const enshProtoContract = contracts["EnshroudProtocol"];
	const userAcct = web3.utils.toChecksumAddress(accounts[0]);

	// get the token symbol for the asset
	var tokenSymbol = '';
	const tokenRecord
		= props.tokenSymbols.find(elt => elt.contract === props.asset);
	if (tokenRecord !== undefined) {
		tokenSymbol = tokenRecord.symbol;
	}

	// local state to store the amount in the form
	const [burnAmount, setBurnAmount] = useState("");

	// local state to store the fee amount in percent
	const [withdrawFee, setWithdrawFee] = useState(0.0);

	// local state for market price of this asset
	const [marketPrice, setMarketPrice] = useState("0");

	// method to get the withdraw fee for an asset, or return the default
	function getWithdrawFee(tokenAddr) {
		var wFee = "0";
		enshProtoContract.methods.assetWithdrawFee(tokenAddr)
			.call({ from: userAcct })
			.then(fee => {
				if (fee !== "0") {
					wFee = web3.utils.fromWei(fee, 'milliether');
					// convert to percent
					let pct = +wFee / 10.0;
					// use updater function to avoid altering a stale state
					setWithdrawFee(withdrawFee => (pct));
				} else {
					// fall back to default, using updater function
					setWithdrawFee(withdrawFee => (chainConn.chainConfig.defaultWithFee));
				}
			})
			.catch(err => {
				console.error("Error fetching withdraw fee, " + err);
			});
	}
	if (withdrawFee === 0.0) {
		// trigger fee fetch for this token
		getWithdrawFee(props.asset);
	}
	// else: already have non-default value

	// process a change to the amount field
	const handleAmountChange = 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 burnAmtStr = inpVal;
		var enteredAmt = new BigNumber(burnAmtStr);
		if (enteredAmt.isNegative()) {
			// go back to previous value
			burnAmtStr = burnAmount;
			enteredAmt = new BigNumber(burnAmtStr);
		}
		amt = amt.plus(enteredAmt);

		// get exact unrounded min
		const amount = BigNumber.minimum(amt, props.avail);
		if (!amount.eq(enteredAmt)) {
			burnAmtStr = amount.toFixed();
		}
		setBurnAmount(burnAmtStr);
		props.onTotalChange(props.asset, amount.toFixed());
		// force clear on selected eNFTs
		props.selectedConfig.selectedENFTs.clear();

		// get ChainAsset for this asset, extract price feed if there is one
		var mktPriceFeed = '';
		const chAsset = chainConn.chainConfig.assetList.find(
									elt => elt.contractAddress === props.asset);
		if (chAsset !== undefined) {
			mktPriceFeed = chAsset.priceFeed;
		}
		if (mktPriceFeed !== undefined && mktPriceFeed !== '') {
			// get the market price for this asset
			const clFeed = new ChainLinkPriceFeed();
			const usdPrice = await clFeed.getPrice(web3, mktPriceFeed);
			if (usdPrice !== undefined) setMarketPrice(usdPrice.toString());
		}
	};

	// shorthands
	const switchId = `sel-${props.asset}`;
	const symId = `sym-${props.asset}`;
	const amtId = `amt-${props.asset}`;
	const availId = `avail-${props.asset}`;
	const maxId = `max-${props.asset}`;
	const amtTitle = "Enter burn amount in ";
	const aria = `select ${tokenSymbol}`;

	// determine whether the radio should be checked or not
	var switchState = false;
	if (props.selectedConfig !== undefined
		&& props.selectedConfig.asset === props.asset) {
		switchState = props.selectedConfig.active;
	}
	var switchEnabled = true;
	if (props.avail <= 0) {
		switchState = false;
		switchEnabled = false;
	}
	if (!switchState) {
		// manually turn off radio selection display after it's mounted
		const sel = document.getElementById(switchId);
		if (sel !== null) sel.checked = false;
	}

	// clip burn amount if now > max
	if (burnAmount > 0) {
		const maxAmt = new BigNumber(props.avail);
		if (maxAmt.lt(burnAmount)) {
			setBurnAmount(maxAmt.toFixed())
		}
	}

	// set max amount
	function setMaxAmount() {
		setBurnAmount(props.avail);
		props.onTotalChange(props.asset, props.avail);
	}

	// prep and show a fiat value column matching the total amount being spent
	const tokenVal = new BigNumber(burnAmount).multipliedBy(marketPrice)
					.decimalPlaces(2).toString();

	var dispAmt = burnAmount === "0.0" ? "" : burnAmount;
	// check for entry of a series of zeros
	const zeroPat = /0*\.0*/;
	if (burnAmount.match(zeroPat)) {
		dispAmt = burnAmount;
	}

	// render line
	return (
		<tr align="center" valign="middle">
			<td>
				<Form.Check id={switchId} name="tokenRadio" type="radio"
					selected={switchState} className="mx-auto p-2"
					onChange={(asset) => props.onConfigSelect(props.asset)}
					aria-label={aria} disabled={!switchEnabled}
				/>
			</td>
			<td id={symId} title={props.asset}>{tokenSymbol}</td>
			<td title="Withdraw fee deducted by smart contract">
				{withdrawFee} %
			</td>
			<td id={availId}>
				{props.amount}&nbsp;&nbsp;<i>({props.avail})</i>
			</td>
			<td>
				<InputGroup>
					<Form.Control type="text" id={amtId} name={amtId}
						value={dispAmt} className="p-2"
						title={amtTitle + tokenSymbol}
						onChange={handleAmountChange} placeholder="0"
					/>
					<Button title="Set amount to available balance" id={maxId}
						variant="link" size="sm" onClick={setMaxAmount}
						role="button"
					>
						Max
					</Button>
				</InputGroup>
			</td>
			<td>{tokenVal > 0 ? tokenVal : "N/A"}</td>
		</tr>
	);
}

// toggle the eNFT list accordion for a particular asset type
function NftListToggle({ children, eventKey }) {
	const decoratedOnClick = useAccordionButton(eventKey, () =>
		"",
	);

	const bId = "listToggle-" + eventKey;
	return (
		<Button type="button" onClick={decoratedOnClick} variant="info"
			title="Toggle eNFT list" id={bId}
		>
			{children}
		</Button>
	);
}

/* render a single eNFT as a suitable table row (accordion contents)
 * @param props.eNFT the Enft record
 * @param props.asset the token contract involved
 * @param props.tokenSymbols the list of token symbols we know about so far
 * @param props.payeeConfigs the existing list of PayeeConfig records
 */
function SelectableEnft(props) {
	const { state: { chainConn, web3 } } = useEth();

	// get the token symbol for the asset
	var tokenSymbol = '';
	const tokenRecord
		= props.tokenSymbols.find(elt => elt.contract === props.asset);
	if (tokenRecord !== undefined) {
		tokenSymbol = tokenRecord.symbol;
	}

	// shorthands
	const enft = props.eNFT;
	const aria = `select ${tokenSymbol} eNFT`;
	const name = `${tokenSymbol}select`;
	const truncId = enft.id.substring(0,4) + '...'
					+ enft.id.substring(60);
	const amt = web3.utils.fromWei(enft.amount.toString());
	const availId = `avail-${enft.id}`;

	// get the relevant PayeeConfig record
	var enftSelected = false;
	const payeeConfig
		= props.payeeConfigs.find(elt => elt.asset === props.asset);
	if (payeeConfig !== undefined) {
		// determine current status of this Enft (could have been auto-selected)
		enftSelected = payeeConfig.isENFTselected(enft.id);
		// manually force DOM to make the checkbox match
		const chkBox = document.getElementById(enft.id);
		if (chkBox !== null) {
			chkBox.checked = enftSelected;
		}
	}

	// set the selected individual eNFT
	function selectENFT(asset, id, selected) {
		if (payeeConfig !== undefined) {
			if (selected && !enftSelected) {
				// select this Enft
				if (enft.avail) {
					payeeConfig.selectENFT(id, enft);
					enftSelected = true;
				}
			}
			else if (!selected && enftSelected) {
				// deselect this Enft
				payeeConfig.deselectENFT(id);
				enftSelected = false;
			}
		} else {
			alert("No payee config associated with this Enft");
		}
		// NB: this doesn't need to re-render because checkbox is only display
	}

	return (
		<tr align="center" valign="middle">
			<td>
				<Form.Check type="checkbox" id={enft.id} name={name}
					selected={enftSelected}
					onClick={(asset, selId, stat) => selectENFT(enft.asset, enft.id, !enftSelected)}
					value={enft.id} aria-label={aria} disabled={!enft.avail}
				/>
			</td>
			<td id={availId}>
				{amt}&nbsp;&nbsp;
				{enft.avail ?
					<Badge pill bg="success"
						title={"Available " + enft.unavailReason}>+</Badge>
					: <Badge pill bg="warning"
						title={"Unavailable: " + enft.unavailReason}>-</Badge>
				}
			</td>
			<td className="text-break" title={enft.id}>{truncId}</td>
			<td>{enft.generation}</td>
			<td>
				{enft.validateSig(chainConn, web3)
					? <Badge pill bg="success" title="MVO signature validated">
						<Image src="images/check2.svg"
						fluid rounded className="p-2" height={40} width={40} />
					  </Badge>
					: <Badge pill bg="danger" title="MVO signature invalid">
						<Image src="images/x-lg.svg"
						fluid rounded className="p-2" height={40} width={40} />
					  </Badge>
				}
			</td>
		</tr>
	);
}

/* generate a table of eNFTs, inside an accordion
 * @param props.asset the token contract
 * @param props.id suitable key value
 * @param props.enftData array of EnftS we are to represent
 * @param props.tokenSymbols the list of token symbols we know about so far
 * @param props.payeeConfigs the existing list of PayeeConfig records
 */
function AssetENFTdetails(props) {
	// get the token symbol for the asset
	var tokenSymbol = '';
	const tokenRecord
		= props.tokenSymbols.find(elt => elt.contract === props.asset);
	if (tokenRecord !== undefined) {
		tokenSymbol = tokenRecord.symbol;
	}

	return (
		<tr align="left" valign="middle">
			<td colSpan="6">
				<Accordion flush>
					<Accordion.Item eventKey={props.id}>
						<NftListToggle
							eventKey={props.id}
							key={`toggle${props.id}`} >
							Show {tokenSymbol} eNFTs&nbsp;
							<Badge bg="success">
								{props.enftData.length}
							</Badge>&nbsp;&nbsp;
							<Image src="images/zoom-in.svg" fluid rounded
								height={40} width={40}
							/>
						</NftListToggle>
						<Accordion.Body>
							<div className="accordion-body">
							<Table striped bordered hover responsive
								variant="dark">
								<caption className="caption-top">
									{tokenSymbol} eNFT details:
								</caption>
								<thead>
									<tr align="center" key="header{props.id}">
										<th scope="col"
											title="Use to override auto-selection of input eNFTs for Total Burn Amount">
											Select
										</th>
										<th scope="col"
											title="In ethers units, converted from wei for display">
											Amount (1e-18)
										</th>
										<th scope="col"
											title="Use hover text to see complete ID, or download on Home page">
											eNFT ID
										</th>
										<th scope="col"
											title="Number of times this value has circulated since it was deposited on-chain">
											Generation
										</th>
										<th scope="col"
											title="Results of checking the MVO's signature on the details of this eNFT">
											Sig Verified
										</th>
									</tr>
								</thead>
								<tbody>
									{props.enftData.map((enft) =>
										<SelectableEnft
											key={enft.id}
											eNFT={enft}
											asset={props.asset}
											tokenSymbols={props.tokenSymbols}
											payeeConfigs={props.payeeConfigs}
										/>
									)}
								</tbody>
							</Table>
							</div>
						</Accordion.Body>
					</Accordion.Item>
				</Accordion>
			</td>
		</tr>
	);
}

/* method to display the table of eNFTs for a given asset type
 * @param prop.details the list of records, in this form:
 * 	{id: <key index>, asset: <token>, total: <eNFT sum>, available: <sum avail>,
 * 	eNFTs: [Enft]}
 * @param props.payeeConfigs the existing list of PayeeConfig records
 * @param props.activeConfigs the list of assets currently configured active
 * @param props.onConfigSelect method to set the PayeeConfig active or not
 * @param props.onTotalChange method to change the total amount of asset spent
 * @param props.buildPayeeConfigs  method to initialize all new PayeeConfigS
 * @param props.onNewPayeeConfig method to initialize a new PayeeConfig record
 * @param props.tokenSymbols the list of token symbols we know about so far
 * @param props.initTokenSymbols method to register token symbols
 */
function ENFTTable(props) {
	var doBody = true;
	if (props.payeeConfigs.length === 0) {
		// no configs yet, no rendering to be done
		doBody = false;
	}
	var bodyContents = <tr></tr>;

	if (doBody) {
		bodyContents = (
			props.details.map((row) =>
				<Fragment key={row.id}>
					<TotalItem
						asset={row.asset}
						amount={row.total}
						avail={row.available}
						id={row.id}
						selectedConfig={props.selectedConfig}
						onConfigSelect={(contract) => props.onConfigSelect(contract)}
						onTotalChange={(asset, amount) => props.onTotalChange(asset, amount)}
						tokenSymbols={props.tokenSymbols}
					/>
					<AssetENFTdetails
						asset={row.asset}
						enftData={row.eNFTs}
						id={row.id}
						tokenSymbols={props.tokenSymbols}
						payeeConfigs={props.payeeConfigs}
					/>
				</Fragment>
			)
		);
	}
	return (
		<tbody>
			{bodyContents}
		</tbody>
	);
}

/* render the summary data on the burn
 * @param props.selectedConfig the selected PayeeConfig (undefined if no radio)
 * @param props.burnAmounts the list of burn amounts entered
 */
function BurnConfigSummary(props) {
	// enable use of Web3
	const { state: { accounts, contracts, chainConn, web3 } } = useEth();
	const enshProtoContract = contracts["EnshroudProtocol"];
	const userAcct = web3.utils.toChecksumAddress(accounts[0]);

	// local state to store the fee amount in percent
	const [withdrawFee, setWithdrawFee] = useState(0.0);

	// local state for market price of this asset
	const [marketPrice, setMarketPrice] = useState("0");

	// default content if nothing is selected yet
	if (props.selectedConfig === undefined) {
		return (
			<div align="left" key="burnSummary">
				<br/>
				<h5>No Burn Configuration Available</h5>
				<br/>
			</div>
		);
	}

	// input shorthands
	const asset = props.selectedConfig.asset;
	const symbol = props.selectedConfig.symbol;
	var burnAmount = new BigNumber(0.0);
	const burnAmt = props.burnAmounts.find(elt => elt.contract === asset);
	if (burnAmt !== undefined) {
		burnAmount = burnAmount.plus(burnAmt.amount);
	}

	// method to obtain market price, if a feed is available
	async function getMarketPrice() {
		// get ChainAsset for this asset, extract price feed if there is one
		var mktPriceFeed = '';
		const chAsset = chainConn.chainConfig.assetList.find(
										elt => elt.contractAddress === asset);
		if (chAsset !== undefined) {
			mktPriceFeed = chAsset.priceFeed;
		}
		if (mktPriceFeed !== undefined && mktPriceFeed !== '') {
			// get the market price for this
			const clFeed = new ChainLinkPriceFeed();
			const usdPrice = await clFeed.getPrice(web3, mktPriceFeed);
			if (usdPrice !== undefined) setMarketPrice(usdPrice.toString());
		}
	}
	if (marketPrice === "0") {
		getMarketPrice();
	}

	// method to get the withdraw fee for an asset, or return the default
	function getWithdrawFee(tokenAddr) {
		var wFee = "0";
		enshProtoContract.methods.assetWithdrawFee(tokenAddr)
			.call({ from: userAcct })
			.then(fee => {
				if (fee !== "0") {
					wFee = web3.utils.fromWei(fee, 'milliether');
					// convert to percent
					let pct = new BigNumber(wFee).dividedBy(10.0);
					// use updater function to avoid altering a stale state
					setWithdrawFee(withdrawFee => (pct));
				} else {
					// fall back to default, using updater function
					let pct
						= new BigNumber(chainConn.chainConfig.defaultWithFee);
					setWithdrawFee(withdrawFee => (pct));
				}
			})
			.catch(err => {
				console.error("Error fetching withdraw fee, " + err)
				alert("Unable to fetch withdraw fee, default calculation "
						+ "may be incorrect");
				// use default
				let pct = new BigNumber(chainConn.chainConfig.defaultWithFee);
				setWithdrawFee(withdrawFee => (pct));
			});
	}
	if (withdrawFee === 0.0) {
		// trigger fee fetch for this token
		getWithdrawFee(asset);
		// (will now be a BigNumber)
	}
	// else: already have non-default value
	const withFee = burnAmount.multipliedBy(withdrawFee).dividedBy(100.0);
	const netWith = burnAmount.minus(withFee);
	const mktValue = new BigNumber(burnAmount).multipliedBy(marketPrice)
					.decimalPlaces(2).toString();
	const netTitle = "The fee for withdrawing the Total Burn Amount of "
					+ symbol + " from the Enshroud smart contract";
	const nf = new Intl.NumberFormat("en-US", {maximumFractionDigits: 8});

	// render this data in a small table
	return (
		<Table bordered responsive>
			<caption className="caption-top">
				Selected Burn Configuration Summary:
			</caption>
			<thead>
				<tr align="center" valign="middle" key="burnConfigHead">
					<th scope="col"
						title="You will receive this token on-chain">
						Token for eNFT Redemption
					</th>
					<th scope="col"
						title="Amount shown in ethers units for readability, will be converted to wei">
						Total Burn Amount (1e-18)
					</th>
					<th scope="col"
						title="You will receive the Total less this amount, shown in ethers">
						Withdrawal Fee (1e-18)
					</th>
					<th scope="col"
						title="The Total Burn Amount minus with Withdrawal Fee, shown in ethers">
						Net Credit On-chain (1e-18)
					</th>
					<th scope="col"
						title="Market value of Total Burn Amount in USD, if available">
						Burn Value in USD
					</th>
				</tr>
			</thead>
			<tbody>
				<tr align="center" valign="middle" key="burnConfigEntry">
					<td title={asset}>{symbol}</td>
					<td title="You will burn eNFT value equal to this amount"
						id="burnTotal">
						{burnAmount.toFixed()}
					</td>
					<td title={netTitle}>{nf.format(withFee)}</td>
					<td
						title={"Your balance in " + symbol + " will increase by this amount"}
						id="netCredit">
						{netWith.toFixed()}
					</td>
					<td>{mktValue > 0 ? mktValue : "N/A"}</td>
				</tr>
			</tbody>
		</Table>
	);
}

/* main display method for both input eNFT table and withdrawal amount config
 * @param props.parseReplyMethod method to process MVO replies received
 * @param props.eNFTList the list of user EnftS (decrypted)
 * @param props.opsBlock decrypted JSON object last received from the MVO
 * @param props.active whether SmartContractSubmitter should display content
 */
function BurnENFTs(props) {
	// enable use of our blockchain interfaces
	const { state: { artifacts, contracts, accounts, web3, chainConn } }
		= useEth();
	const userAcct = web3.utils.toChecksumAddress(accounts[0]);
	const enshContract = contracts["EnshroudProtocol"];

	// state storage of passed eNFTs, downloaded and decrypted and passed down
	const [eNFTs, setENFTs] = useState([]);

	/* method to check the availability of passed eNFTs
	 * @param enftList the list of eNFTs to check
	 */
	async function checkEnftAvailability(enftList) {
		const redoEnfts = [];
		for await (const enft of enftList) {
			/* Determine whether the eNFT is "available," meaning aged for the
			 * required number of blocks since minting, plus not greylisted.
			 * This async method sets enft.avail, and if set to false, also
			 * sets enft.unavailReason showing why.
			 */
			await enft.isAvailable(web3, enshContract);

			// update by merging state
			redoEnfts.push(enft);
		}

		// mass delete and re-add any that changed (queued w/ updater fn.)
		if (redoEnfts.length > 0) {
			// NB: this will trigger two re-renders, one after each operation
			setENFTs(eNFTs => (eNFTs.filter(
						elt => !redoEnfts.some(enft => enft.id === elt.id))));
			setENFTs(eNFTs => ([...eNFTs, ...redoEnfts]));
		}
	}

	// audit that passed eNFTs (if any) are known (NB: all are decrypted)
	const newEnfts = [];
	props.eNFTList.forEach(enft => {
		const existEnft = eNFTs.find(elt => elt.id === enft.id);
		if (existEnft === undefined) {
			// add
			newEnfts.push(enft);
		}
	});
	if (newEnfts.length > 0) {
		if (eNFTs.length === 0) {
			setENFTs([...newEnfts]);
		} else {
			setENFTs([...eNFTs, ...newEnfts]);
		}

		// fetch and update availability of these EnftS
		checkEnftAvailability(newEnfts);
	}

	/* Based on passed-in EnftS, build a list of assets, totals, and eNFTs.
	 * NB: these are all read-only datums, so we don't need to useState.
	 * Instead we build the mappings afresh with each render.
	 */
	const assetTotals = new Map();		// contract -> total in wei (ledger)
	const assetAvails = new Map();		// contract -> total in wei (confirmed)
	const assetENFTs = new Map();		// contract -> array of Enft
	eNFTs.forEach(enft => {
		let asset = enft.asset;
		if (assetTotals.has(asset)) {
			// increase the total
			let existAmt = assetTotals.get(asset);
			existAmt = existAmt.add(web3.utils.toBN(enft.amount));
			assetTotals.set(asset, existAmt);

			// add eNFT to array
			assetENFTs.get(asset).push(enft);
		} else {
			// initialize the total
			assetTotals.set(asset, web3.utils.toBN(enft.amount));

			// init array with this eNFT
			const nftArray = [];
			nftArray.push(enft);
			assetENFTs.set(asset, nftArray);
		}

		// add this Enft to available balance if it's marked available
		if (enft.avail) {
			if (assetAvails.has(asset)) {
				// increase
				let existAvail = assetAvails.get(asset);
				existAvail = existAvail.add(web3.utils.toBN(enft.amount));
				assetAvails.set(asset, existAvail);
			} else {
				// init total
				assetAvails.set(asset, web3.utils.toBN(enft.amount));
			}
		}
	});

	// construct data to pass to ENFTTable as props (all numbers in ethers)
	let assetIdx = 1;
	const eNFTTableProps = [];
	for (const asst of assetTotals.keys()) {
		const amount = web3.utils.fromWei(assetTotals.get(asst));
		var avail = "0";
		const avBucket = assetAvails.get(asst);
		if (avBucket !== undefined) {
			avail = web3.utils.fromWei(avBucket);
		}
		const nftArray = assetENFTs.get(asst);
		eNFTTableProps.push({id: assetIdx++,
							asset: asst,
							total: amount,
							available: avail,
							eNFTs: nftArray});
	}

	/* Dynamic payee configurations for every asset type represented in the
	 * collection of EnftS.  Note that the list of PayeeConfigS are strictly
	 * dictated by the downloaded Enft data, thus local to this page.
	 */
	const [payeeConfigs, setPayeeConfigs] = useState([]);

	// build all the new PayeeConfig records at once from a list (see ENFTTable)
	function initPayeeConfigs(contractList) {
		const pConfigs = [];
		for (const contract of contractList) {
			const config = new PayeeConfig();
			config.asset = contract;
			pConfigs.push(config);
		}
		if (payeeConfigs.length === 0) {
			setPayeeConfigs([...pConfigs]);
		} else {
			console.warn("unexpected initPayeeConfigs() with existing configs");
			setPayeeConfigs([...payeeConfigs, ...pConfigs]);
		}
	}

	// add a new payeeConfig to the list
	function addPayeeConfig(tokenContract) {
		var payeeConfig = payeeConfigs.find(elt => elt.asset === tokenContract);
		// we need a payeeConfig for every asset type
		if (payeeConfig === undefined) {
			payeeConfig = new PayeeConfig();
			payeeConfig.asset = tokenContract;
			setPayeeConfigs(payeeConfigs => ([...payeeConfigs, payeeConfig]));
		} else {
			console.warn("PayeeConfig for added contract " + tokenContract
						+ " already exists");
		}
		return payeeConfig;
	}

	// result of smart contract submission (used only on success)
	const [smartResult, setSmartResult] = useState("");

	/* merge the user's modified payee config into our list (called by
	 * SmartContractSubmitter upon a successful smart contract burn/withdraw)
	 * @param pConfig the updated PayeeConfig (single item)
	 */
	function resetPayeeConfig(pConfig) {
		/* 1. To prevent errors re-rendering SmartContractSubmitter, add a done
		 * 	  flag to the passed OperationsBlock json object.
		 */
		if (props.opsBlock !== undefined) {
			props.opsBlock.done = true;
		}

		const existing = payeeConfigs.find(elt => elt.asset === pConfig.asset);
		if (existing !== undefined) {
			// 2. remove all input eNFTs, now burned
			pConfig.selectedENFTs.forEach((enft, id) => {
				// get index of this Id
				let delIdx = -1;
				for (let idIdx = 0; idIdx < eNFTs.length; idIdx++) {
					if (eNFTs[idIdx].id === id) {
						delIdx = idIdx;
						break;
					}
				}
				// if index found, remove it
				if (delIdx >= 0) {
					eNFTs.splice(delIdx, 1);
				} else {
					console.error("burned eNFT id " + id + " not found");
				}
			});

			// and clear the selected eNFT list, which changes the radio itself
			pConfig.selectedENFTs.clear();

			// 3. wipe previous payees
			pConfig.payees.splice(0, pConfig.payees.length);

			// 4. set this payeeConfig as no longer selected
			pConfig.active = false;
			pConfig.total = new BigNumber(0);
			setSelectedConfig(undefined);

			// 5. zero out burnAmount for this asset
			const burnAmt
				= burnAmounts.find(elt => elt.contract === pConfig.asset);
			if (burnAmt !== undefined) {
				setAssetBurnAmount(pConfig.asset, "0.0");
			}
		} else {
			console.error("no preexisting config for " + pConfig.symbol);
		}

		// indicate that the burn worked
		if (smartResult !== '') setSmartResult('');
		const burnSuccess = <DAlert variant="success"
							title="Your burn of eNFTs succeeded!"
							data="Check your account's balance in the underlying token asset.  If an eNFT was minted to your own account as change, you can now download it to view details." />;
		setSmartResult(smartResult => (burnSuccess));
	}

	// the currently PayeeConfig currently selected for burning
	const [selectedConfig, setSelectedConfig] = useState();

	/* method to select a config from the available list
	 * @param contract the token address (index in passed map)
	 */
	function selectPayeeConfig(contract) {
		const selConfig = payeeConfigs.find(elt => elt.asset === contract);
		if (selConfig !== undefined) {
			if (selectedConfig !== undefined) {
				selectedConfig.active = false;
			}
			selConfig.active = true;
			setSelectedConfig(selConfig);
		} else {
			// hard to see how this case arises, but initialize a new one
			const pConfig = addPayeeConfig(contract);
			selectedConfig.active = false;
			pConfig.active = true;
			setSelectedConfig(pConfig);
		}
	}

	/* List of balances to be burnt, by asset type (differs from .total value
	 * in a PayeeConfig; an implicit SpendPayee will be created for difference).
	 * These records look like: {contract: address, amount: value in ethers}.
	 */
	const [burnAmounts, setBurnAmounts] = useState([]);

	/* method to set the balance to be burned for an asset type
	 * @param asset the token contract
	 * @param amount the amount of this user wants to burn/withdraw
	 */
	function setAssetBurnAmount(asset, amount) {
		const existing = burnAmounts.find(elt => elt.contract === asset);
		if (existing !== undefined) {
			// update this one
			existing.amount = amount;
			setBurnAmounts(burnAmounts => (burnAmounts.map(s => {
				if (s.contract === existing.contract) {
					return existing;
				} else {
					return s;
				}
			})));
		} else {
			// add new
			const assetAmt = {contract: asset, amount: amount};
			setBurnAmounts(burnAmounts => ([...burnAmounts, assetAmt]));
		}
	}

	/* List of symbols for each token contract for which EnftS (and therefore
	 * PayeeConfigS) exist.  These records look like:
	 * 	{contract: address, symbol: contract.symbol()}.
	 */
	const [tokenSymbols, setTokenSymbols] = useState([]);

	// set all the new tokenSymbol records at once from a list (see ENFTTable)
	function initTokenSymbols(symbolList) {
		if (tokenSymbols.length === 0) {
			// replace entire list
			setTokenSymbols([...symbolList]);
		} else {
			// append to list
			setTokenSymbols([...tokenSymbols, ...symbolList]);
		}

		// record symbols in PayeeConfig for same contract
		symbolList.forEach(symb => {
			const payeeConfig
				= payeeConfigs.find(elt => elt.asset === symb.contract);
			if (payeeConfig !== undefined) {
				payeeConfig.symbol = symb.symbol;
			}
		});
	}

	/* See if we need to create a PayeeConfig for each asset type.
	 * Also check whether we need to fetch a symbol for the token.
	 */
	const newAssets = [];
	const newSymbols = [];
	eNFTTableProps.forEach(confRec => {
		// check for a PayeeConfig
		const existingConfig
			= payeeConfigs.find(elt => elt.asset === confRec.asset);
		if (existingConfig === undefined) {
			newAssets.push(confRec.asset);
		}

		// check for a token symbol
		const existSymbol
			= tokenSymbols.find(elt => elt.contract === confRec.asset);
		if (existSymbol === undefined) {
			newSymbols.push(confRec.asset);
		}
	});

	if (payeeConfigs.length === 0) {
		// add new all at once
		if (newAssets.length > 0) {
			initPayeeConfigs(newAssets);
		}
	} else if (newAssets.length > 0) {
		// add only the new ones
		for (const asset of newAssets) {
			addPayeeConfig(asset);
		}
	}

	// get symbol of an erc20-compatible 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;
	};

	// obtain all token symbols from each respective token contract w/ symbol()
	const getAllTokenSymbols = async () => {
		if (newSymbols.length === 0) {
			return;
		}
		const tokenSymbols = [];
		for await (const tokenContract of newSymbols) {
			const tokSym = await getTokenSymbol(tokenContract);
			if (tokSym !== '') {
				const tokRec = {contract: tokenContract, symbol: tokSym};
				tokenSymbols.push(tokRec);
			} else {
				console.error("Error fetching token symbol for "
							+ tokenContract);
			}
		}

		// tell top level to update state
		initTokenSymbols(tokenSymbols);
	};
	getAllTokenSymbols();

	// method to send a signed wallet download request to an MVO
	async function sendWalletReqToMVO(resolve, reject) {
		// examine passed MVO configuration to ensure it's been downloaded
		const mvoConfig = chainConn.MVOConf;
		const chId = chainConn.chainConfig.chainId;
		if (mvoConfig.availableMVOs.length === 0) {
			let inputErr = new Error("No MVOs listed for chainId " + chId
					+ "; is " + chainConn.chainConfig.chain + " connected?");
			alert(inputErr.message);
			reject(inputErr);
			return false;
		}

		// obtain secure communicator to randomly selected MVO for this chain
		const mvoComm = mvoConfig.getMVOCommunicator('wallet', true);
		if (!(mvoComm instanceof MVOComm)) {
			let mvoErr = new Error("Could not select an MVO");
			alert(mvoErr.message);
			reject(mvoErr);
			return false;
		}

		// access msg.sender and verifying contract address
		const sender = userAcct;
		const enshAddress = enshContract.options.address;

		// generate reply key and the payload we must sign
		var replyKey = '';
		var payload = '';
		if (!mvoComm.encrypted) {
			// old version, for use without encryption (passed as POST param)
			payload = 'walletDownload={"chainId":"' + chId
						+ '","sender":"' + sender + '","IDList":[]}';

			// send plain data unencrypted and unsigned
			mvoComm.sendToMVO(payload, props.parseReplyMethod);
		} else {
			// generate an AES key for the MVO to use to encrypt normal replies
			mvoComm.generateAesKey();
			// NB: generateAesKey() stored raw key in mvoComm.replyKeyB64
			replyKey = mvoComm.decryptKey;

			// define eth_signTypedData_v4 parameters
			const msgParams = JSON.stringify({
				// EIP-712 domain info (depends upon chId scan URL for display)
				domain: {
					chainId: chId,
					name: 'Enshroud',
					verifyingContract: enshAddress,
					version: '1',
				},
				// descriptive info on what's being signed and for whom
				message: {
					contents: 'Send encrypted request to download the eNFTs and keys in your wallet',
					to: {
						MVOId: mvoComm.mvo,
						URL: mvoComm.mvoURL,
					},
					requestJson: {
						walletDownload: {
							chainId: `${chId}`,
							sender: sender,
							IDList: [],
							replyKey: replyKey,
						},
					},
				},
				primaryType: 'Request',
				types: {
					// the domain the contract is hosted on
					EIP712Domain: [
						{ name: 'chainId', type: 'uint256' },
						{ name: 'name', type: 'string' },
						{ name: 'verifyingContract', type: 'address' },
						{ name: 'version', type: 'string' },
					],
					// refer to primaryType
					Request: [
						{ name: 'contents', type: 'string' },
						{ name: 'to', type: 'MVO' },
						{ name: 'requestJson', type: 'WalletDownload' },
					],
					// not an EIP712Domain definition
					MVO: [
						{ name: 'MVOId', type: 'string' },
						{ name: 'URL', type: 'string' },
					],
					// not an EIP712Domain definition
					WalletDownload: [
						{ name: 'walletDownload', type: 'Payload' },
					],
					// not an EIP712Domain definition
					Payload: [
						{ name: 'chainId', type: 'string' },
						{ name: 'sender', type: 'address' },
						{ name: 'IDList', type: 'string[]' },
						{ name: 'replyKey', type: 'string' },
					],
				},
			});
			const method = 'eth_signTypedData_v4';
			var params = [sender, msgParams];

			// now obtain signature on params in a EIP-712 compatible way
			var userSig;
			await web3.currentProvider.sendAsync(
				{
					method,
					params,
					from: sender,
				},
				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");
						reject(sigErr);
						return false;
					}

					// append signature to the arguments
					const allArgs = JSON.parse(msgParams);
					allArgs.signature = userSig;

					// encrypt + send the message to the MVO, passing callback
					mvoComm.sendToMVO(JSON.stringify(allArgs),
									  props.parseReplyMethod);
					// NB: even if sendToMVO() fails, we still want to resolve
					resolve(true);
				}
			);
		}

		// clear the existing eNFT list
		eNFTs.splice(0, eNFTs.length);
		return true;
	}
	
	// method to send a signed wallet withdraw request to an MVO
	async function sendWithdrawReqToMVO(resolve, reject) {
		// access msg.sender and verifying contract address
		const sender = userAcct;
		const enshAddress = enshContract.options.address;

		// examine passed MVO configuration to ensure it's been downloaded
		const mvoConfig = chainConn.MVOConf;
		const chId = chainConn.chainConfig.chainId;
		if (mvoConfig.availableMVOs.length === 0) {
			let inputErr = new Error("No MVOs listed for chainId " + chId
					+ "; is " + chainConn.chainConfig.chain + " connected?");
			alert(inputErr.message);
			reject(inputErr);
			return false;
		}

		// obtain secure communicator to randomly selected MVO for this chain
		const mvoComm = mvoConfig.getMVOCommunicator('spend', true);
		if (!(mvoComm instanceof MVOComm)) {
			let mvoErr = new Error("Could not select an MVO");
			alert(mvoErr.message);
			reject(mvoErr);
			return false;
		}

		if (selectedConfig === undefined) {
			let noSel = new Error("No burn asset radio button selected");
			alert(noSel.message);
			reject(noSel);
			return false;
		}

		// verify that we haven't lost our eNFT[] list
		if (props.eNFTList.length === 0) {
			let noEnfts = new Error("Please Refresh the list of eNFTs");
			alert(noEnfts.message);
			reject(noEnfts);
			return false;
		}

		/* Before doing anything else, we must audit the payee config for
		 * sanity.  There should be zero payees defined, as the output is
		 * on-chain erc20 balance credit.
		 *
		 * But if the eNFT amount is not fully utilized, the MVO will gin up a
		 * new implicit payee back to the sender to catch the difference.
		 */
		const token = selectedConfig.asset;
		const tokSym = tokenSymbols.find(elt => elt.contract === token);
		const burnAmount = burnAmounts.find(elt => elt.contract === token);
		if (burnAmount === undefined) {
			let amtErr = new Error("No burn amount for " + tokSym.symbol);
			alert(amtErr.message);
			reject(amtErr);
			return false;
		}
		var burnTotal = burnAmount.amount;
		const burnAmountBN
			= web3.utils.toBN(web3.utils.toWei(burnTotal.toString()));
		if (burnAmountBN.isZero() || burnAmountBN.isNeg()) {
			let bTotErr = new Error("Invalid total burn amount, " + burnTotal);
			alert(bTotErr.message);
			reject(bTotErr);
			return false;
		}

		// NB: there should never be a payee or a payee total set
		if (selectedConfig.payees.length > 0) {
			selectedConfig.payees.splice(0, selectedConfig.payees.length);
			selectedConfig.total = 0.0;
			console.warn("cleared improper payees in " + tokSym + " config");
		}

		/* Loop through all Enfts in wallet, and arrive at overall and
		 * selected totals.  All Enfts must be known valid and available.
		 */
		var enftTotal = new BigNumber(0.0);
		var selTotal = new BigNumber(0.0);
		eNFTs.forEach(enft => {
			if (enft.asset === selectedConfig.asset && enft.valid && enft.avail)
			{
				const enftAmt = web3.utils.fromWei(enft.amount);
				enftTotal = enftTotal.plus(enftAmt);
				if (selectedConfig.isENFTselected(enft.id)) {
					selTotal = selTotal.plus(enftAmt);
				}
			}
			// else: ignore
		});

		// if we don't have enough of the indicated asset, that's it, stop
		if (enftTotal.lt(burnTotal)) {
			let insuff = new Error("Insufficient eNFT value to fund a "
									+ "burn of " + burnTotal + " "
									+ tokSym.symbol + ", max = "
									+ enftTotal.toString());
			alert(insuff.message);
			reject(insuff);
			return false;
		}

		// check for adequate selected value
		if (selTotal.lt(burnTotal)) {
			// loop through eNFTs of this asset type again and sort by gen
			const assetEnfts = [];
			eNFTs.forEach(enft => {
				if (enft.asset === token && enft.valid && enft.avail) {
					assetEnfts.push(enft);
				}
			});
			// use a descending sort so that highest generations come first
			assetEnfts.sort((a, b) => b.generation - a.generation);

			// loop through the sorted Enfts and select new until we have enough
			for (const enft of assetEnfts) {
				if (selectedConfig.isENFTselected(enft.id)) continue;
				const enftAmt = web3.utils.fromWei(enft.amount);
				selTotal = selTotal.plus(enftAmt);
				selectedConfig.selectENFT(enft.id, enft);
				if (selTotal.gte(burnTotal)) {
					break;
				}
			}
		}

		// the MVO will generate an implicit change Enft back to us for diff
		var overage = selTotal.minus(burnTotal);

		// shouldn't occur, but check
		if (overage.lt(0.0)) {
			let insuff = new Error("Insufficient selected eNFT value to fund a "
									+ "burn of " + burnTotal + " "
									+ tokSym.symbol + ", selected = "
									+ selTotal.toString());
			alert(insuff.message);
			reject(insuff);
			return false;
		}
		const burnAmountWei = web3.utils.toWei(burnTotal.toString());

		/* The smart contract (and the MVOs) will only allow us to utilize
		 * a max of 20 input eNFTs.  Build json strings for all inputs.
		 * (Our overage payee, if any, is implicit.)
		 */
		const nf = new Intl.NumberFormat("en-US", {minimumIntegerDigits: 3});
		var totInputs = selectedConfig.selectedENFTs.size;
		if (totInputs > ARRAY_LIMIT) {
			let inErr = new Error("Sorry, the maximum number of input eNFTs "
									+ "which can be used at once is "
									+ ARRAY_LIMIT + ".");
			alert(inErr.message);
			reject(inErr);
			return false;
		}

		// append each input to array (for encrypted case) or string (plain)
		var inputJson = '"inputs":[';
		var inputIdx = 1;
		var inputArray = [];
		const mapIter = selectedConfig.selectedENFTs.entries();
		for (const enft of mapIter) {
			let inputIdent = "input" + nf.format(inputIdx);
			let inputStr = '{"inputLabel":"' + inputIdent + '":,"inputSpec":';
			const eNFT = enft[1];
			let inputDetJson = eNFT.emitEnftJSON();
			inputStr += inputDetJson;

			// add key=AESkey
			let aesKey = eNFT.AESkey;
			if (aesKey !== '') {
				let keyStr = ',"key":"' + aesKey + '"';
				inputStr += keyStr;
			}
			inputStr += "}";

			if (inputIdx < selectedConfig.selectedENFTs.size) {
				// add a comma except on last one
				inputStr += ',';
			}
			inputJson += inputStr;

			// do same thing with objects for the array
			const inputDetails = JSON.parse(inputDetJson);
			const inputObj = {	inputLabel: inputIdent,
								inputSpec: inputDetails,
								key: aesKey };
			inputArray.push(inputObj);
			inputIdx++;
		}
		inputJson += ']';

		// now generate reply key and the payload we must sign
		var replyKey = '';
		var payload = '';
		if (!mvoComm.encrypted) {
			// old version, for use without encryption (passed as POST param)
			// NB: unused in current implementation
			payload = 'withdrawspec={"chainId":"' + chId
						+ '","opcode":"withdraw","sender":"' + sender
						+ '","asset":"' + token + '","amount":"' + burnAmountWei
						+ '",' + inputJson + ',"signature":'
						// NB: in unencrypted case signature is never verified
						+ '"130 character signature string goes here"}';

			// send plain data unencrypted and unsigned
			mvoComm.sendToMVO(payload, props.parseReplyMethod);
			resolve(true);
		} else {
			// generate an AES key for the MVO to use to encrypt normal replies
			mvoComm.generateAesKey();
			// NB: generateAesKey() stored raw key in mvoComm.replyKeyB64
			replyKey = mvoComm.decryptKey;

			// define eth_signTypedData_v4 parameters
			const msgParams = JSON.stringify({
				// EIP-712 domain info (depends upon chId scan URL for display)
				domain: {
					chainId: chId,
					name: 'Enshroud',
					verifyingContract: enshAddress,
					version: '1',
				},
				// descriptive info on what's being signed and for whom
				message: {
					contents: 'Send encrypted request to burn eNFTs for tokens on-chain',
					to: {
						MVOId: mvoComm.mvo,
						URL: mvoComm.mvoURL,
					},
					requestJson: {
						withdrawspec: {
							chainId: `${chId}`,
							opcode: 'withdraw',
							sender: sender,
							asset: token,
							amount: burnAmountWei,
							inputs: inputArray,
							replyKey: replyKey,
						},
					},
				},
				primaryType: 'Request',
				types: {
					// the domain the contract is hosted on
					EIP712Domain: [
						{ name: 'chainId', type: 'uint256' },
						{ name: 'name', type: 'string' },
						{ name: 'verifyingContract', type: 'address' },
						{ name: 'version', type: 'string' },
					],
					// refer to primaryType
					Request: [
						{ name: 'contents', type: 'string' },
						{ name: 'to', type: 'MVO' },
						{ name: 'requestJson', type: 'WithdrawSpec' },
					],
					// not an EIP712Domain definition
					MVO: [
						{ name: 'MVOId', type: 'string' },
						{ name: 'URL', type: 'string' },
					],
					// not an EIP712Domain definition
					WithdrawSpec: [
						{ name: 'withdrawspec', type: 'Payload' },
					],
					// not an EIP712Domain definition
					Payload: [
						{ name: 'chainId', type: 'string' },
						{ name: 'opcode', type: 'string' },
						{ name: 'sender', type: 'address' },
						{ name: 'asset', type: 'address' },
						{ name: 'amount', type: 'string' },
						{ name: 'inputs', type: 'Input[]' },
						{ name: 'replyKey', type: 'string' },
					],
					// not an EIP712Domain definition
					Input: [
						// NB: smart contract supports max of 20 eNFTs at once
						{ name: 'inputLabel', type: 'string'},
						{ name: 'inputSpec', type: 'EnshroudedSpec'},
						{ name: 'key', type: 'string'},
						// NB: at least one input required
					],
					// not an EIP712Domain definition
					EnshroudedSpec: [
						{ name: 'enshrouded', type: 'InputSpec' },
					],
					// not an EIP712Domain definition
					InputSpec: [
						{ name: 'id', type: 'string' },
						{ name: 'schema', type: 'string' },
						{ name: 'owner', type: 'address' },
						{ name: 'asset', type: 'address' },
						{ name: 'amount' , type: 'string' },
						{ name: 'rand', type: 'string' },
						{ name: 'generation', type: 'string' },
						//{ name: 'expiration', type: 'string' },
						//{ name: 'growth', type: 'string' },
						//{ name: 'cost', type: 'string' },
						{ name: 'memo', type: 'string' },
						{ name: 'signer', type: 'string' },
						{ name: 'signature', type: 'string' },
					],
				},
			});
			//console.debug("EIP712 sign args: \"" + msgParams + "\"");
			const method = 'eth_signTypedData_v4';
			var params = [sender, msgParams];

			// now obtain signature on params in a EIP-712 compatible way
			var userSig;
			await web3.currentProvider.sendAsync(
				{
					method,
					params,
					from: sender,
				},
				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");
						reject(sigErr);
						return false;
					}

					// append signature to the arguments
					const allArgs = JSON.parse(msgParams);
					allArgs.signature = userSig;

					// encrypt + send the message to the MVO, passing callback
					mvoComm.sendToMVO(JSON.stringify(allArgs),
									  props.parseReplyMethod);
					// NB: even if sendToMVO() fails, we still want to resolve
					resolve(true);
				}
			);
		}
		return true;
	}

	// rendering for eNFT burn UI
	const burnENFTs =
		<>
		<div className="BurnENFTs">
		<Container fluid align="center">
			<h2>Burn eNFTs to Withdraw On-chain Coins</h2>
			<br/><br/>

			{ /* Refresh button for balances */ }
			<h4>Your eNFT Balances:
				<LoadingButton
					variant="primary"
					buttonTitle="This fetches the eNFT list from an MVO, and populates the eNFTs Available table below"
					netMethod={(resolve, reject) => sendWalletReqToMVO(resolve, reject)}
					buttonText="Refresh"
					buttonStyle="m-3"
					buttonIcon="images/download.svg"
				/>
				<i>(signature required)</i>
			</h4>
			<br/><br/>
		</Container>

		{ /* instructions */ }
		<Container fluid align="left">
			<h4>Redeeming Your Existing eNFTs</h4>
			<ListGroup as="ol" numbered className="text-muted">
				<ListGroup.Item as="li">
					In the <i>eNFTs Available for Redemption</i> table below,
					use the radio buttons to specify the asset you wish to
					withdraw, and enter the total amount of that token to be
					redeemed.  If the table is empty, use
					the <b>Refresh</b> button above to populate it.  Also
					use <b>Refresh</b> after making a withdrawal, to add to
					the table any change eNFT minted back to your account.
				</ListGroup.Item>
				<ListGroup.Item as="li">
					Using the radio buttons, select an asset whose eNFTs you
					wish to burn in order to redeem for tokens on-chain.<br/>
					<b>Note:</b> eNFTs representing wrapped native tokens will
					be redeemed for native tokens (e.g. <i>WETH</i> will be
					unwrapped to become <i>ETH</i>).
				</ListGroup.Item>
				<ListGroup.Item as="li">
					Review the Selected Burn Configuration Summary in the
					middle section below.  Some of your input eNFTs of the
					selected asset type will be burned, and the Total Burn
					Amount will be spent from the Enshroud smart contract back
					to your own account address on-chain, less withdraw fees.
				</ListGroup.Item>
				<ListGroup.Item as="li" variant="info">
					If the sum of your input eNFTs exceeds your Total Burn
					Amount, you will automatically receive a "change" eNFT
					minted back to your account address for the remainder.
				</ListGroup.Item>
				<ListGroup.Item as="li">
					Configure (encrypt) your Burn/Withdawal by signing and
					sending a request to an MVO on Layer2, by using
					the <b>Configure Burn</b> button below.
				</ListGroup.Item>
				<ListGroup.Item as="li">
					You will then have a chance to review the preprocessed
					burn transaction once it has been returned from the MVO,
					before signing again and sending it to the blockchain for
					mining.
				</ListGroup.Item>
				<ListGroup.Item as="li" variant="info">
					(Optional) You can override the default selection of input
					eNFTs (highest generations first) by opening the list of
					eNFTs and selecting them manually.
				</ListGroup.Item>
			</ListGroup>
		</Container>
		<br/><br/>

		{ /* table for eNFTs in wallet */ }
		<Container fluid>
			<Table striped bordered hover responsive variant="dark">
				<caption className="caption-top">
					eNFTs Available for Redemption:<br/>
					<i>
						(Avail. can be less if one or more eNFTs is unconfirmed)
					</i>
				</caption>
				<thead>
					<tr align="center" valign="middle" key="eNFThead">
						<th scope="col" title="Select this asset to redeem">
							Select
						</th>
						<th scope="col" title="Symbol for this token">Asset</th>
						<th scope="col"
							title="Withdrawal fee for this token, in percent">
							Withdraw Fee
						</th>
						<th scope="col"
							title="In ethers units, converted from wei for display">
							Balance <i>(available)</i>
						</th>
						<th scope="col"
							title="The total amount you wish to receive on-chain, in ethers units">
							Total Burn Amount (1e-18)
						</th>
						<th scope="col"
							title="The USD value of the total burn amount at market prices, if available">
							USD Value
						</th>
					</tr>
				</thead>

				{ /* this supplies the <tbody/> */}
				<ENFTTable
					details={eNFTTableProps}
					buildPayeeConfigs={(configList) => initPayeeConfigs(configList)}
					payeeConfigs={payeeConfigs}
					selectedConfig={selectedConfig}
					onConfigSelect={(contract) => selectPayeeConfig(contract)}
					tokenSymbols={tokenSymbols}
					initTokenSymbols={(symbolList) => initTokenSymbols(symbolList)}
					onTotalChange={(asset, amount) => setAssetBurnAmount(asset, amount)}
				/>
			</Table>
			<br/>
		</Container>
		<hr/>

		{ /* Burn configuration summary */ }
		<Container fluid>
			<BurnConfigSummary
				selectedConfig={selectedConfig}
				burnAmounts={burnAmounts}	
			/>
			<hr/>
			<br/>

			{ /* configure burn button */ }
			<h4 align="left">
				<LoadingButton
					variant="primary"
					buttonStyle="m-3"
					buttonTitle="This sends the burn config to an MVO for encryption"
					buttonText="Configure Burn"
					buttonIcon="images/send-check.svg"
					netMethod={(resolve, reject) => sendWithdrawReqToMVO(resolve, reject)}
				/>
				<i>(signature required)</i>
			</h4>
			<p className="text-muted">
				Note: Your account signature is required for authentication to
				the MVO.<br/>
				However no on-chain action occurs during the configuration step.
				<br/>
				You will also review and sign the encrypted transaction again
				before submitting it to the blockchain.
			</p>
			<br/>
		</Container>

		{ /* Show the result of the last successful smart contract submission.
		   * Note this will not be visible once the user has closed the DAlert.
		   */ }
		<br/>
		{smartResult}
		<br/>

		<h4>Smart Contract Submission Preview</h4>
		<SmartContractSubmitter
			opsBlock={props.opsBlock}
			active={props.active}
			eNFTList={eNFTs}
			opcode='withdraw'
			payeeConfigs={payeeConfigs}
			resetConfigs={(config) => resetPayeeConfig(config)}
		/>
		</div>
		</>;

	return (
		<div id="BurnENFTs">
		{
			!artifacts.EnshroudProtocol ? <NoticeNoArtifact /> :
			contracts == null ||
					!contracts["EnshroudProtocol"] ? <NoticeWrongNetwork /> :
				burnENFTs
		}
		</div>
	);
}

export default BurnENFTs;
