/*
 * last modified---
 * 	06-06-26 add use of DOM ids for browser automation support
 * 	12-22-23 correct amount display of net on-chain withdrawal in BurnENFTtable
 * 	11-21-23 use BigNumber for payee.amount values
 * 	11-17-23 add on-chain info section for Burn; prevent render loop on errors
 * 	11-09-23 debug Burn
 * 	11-06-23 move code handling spend success to SpendENFTs.resetPayeeConfigs()
 * 	11-03-23 debug Spend
 * 	10-27-23 compute and check obHash, plus use DAlerts, in Spend and Burn cases
 * 	10-18-23 correlate sub-section args passed down from callers
 * 	10-05-23 debug Mint, use Badges
 * 	09-14-23 change arguments from Maps to Arrays for payeeConfigs and deposits
 * 	09-01-23 add sections to handle the OB for a withdraw
 * 	08-29-23 add sections to handle the OB for a spend
 * 	08-15-23 new
 *
 * purpose---
 * 	component to display OperationsBlockS received from an MVO, and prompt user
 * 	for submission to the appropriate smart contract method
 */

import React, { useState, useEffect } from 'react';
import Card from 'react-bootstrap/Card';
import Image from 'react-bootstrap/Image';
import Badge from 'react-bootstrap/Badge';
import Table from 'react-bootstrap/Table';
import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';
import useEth from './EthContext/useEth';
import DataToast from './DataToast.jsx';
import DAlert from './DAlert.jsx';
const BigNumber = require("bignumber.js");


/* helper method to calculate the detailsHash for an eNFT
 * @param payee the address an eNFT is to be issued to
 * @param id the unique ID of the eNFT
 * @param amount the amount in hex (wei)
 * @param rand the random salt value, in Base64
 * @param web3 the Web3 object
 * @return the bytes32 hash, without 0x prefix
 */
function buildDetailsHash(payee, id, asset, amount, rand, web3) {
	const idAsUint256 = "0x" + id;
	// get salt into format for bytes16
	const randBytes = atob(rand);
	const salt = Uint8Array.from(randBytes, x => x.charCodeAt(0));
	const hexSalt = web3.utils.bytesToHex(salt);
	const enc = web3.eth.abi.encodeParameters(
					['address', 'uint256', 'address', 'uint256', 'bytes16'],
					[payee, idAsUint256, asset, amount, hexSalt]);
	const hash = web3.utils.sha3(enc);
	return web3.utils.stripHexPrefix(hash);
}

/* render a total amount being minted or de-minted, as a short table
 * @param props.opsBlock the JSON object returned from the MVO
 */
function ShowMintTotal(props) {
	const { state: { chainConn, web3 } } = useEth();
	const json = props.opsBlock.MVOSigned;
	const hdr = 'hdr' + json.asset;
	const row = 'row' + json.asset;
	// determine symbol for token asset
	const assetConf = chainConn.chainConfig.assetList.find(
									elt => elt.contractAddress === json.asset);
	const tokenSymbol = assetConf.symbol;
	// determine amount in 1e-18
	const amount
		= web3.utils.fromWei(web3.utils.hexToNumberString("0x" + json.amount));

	return (
		<>
		<Table bordered responsive>
			<caption className="caption-top">
				Total Amount You Are Minting
			</caption>
			<thead>
				<tr align="center" valign="middle" key={hdr}>
					<th scope="col">Asset (token address)</th>
					<th scope="col">Symbol</th>
					<th scope="col">Amount (ethers, 1e-18 scale)</th>
				</tr>
			</thead>
			<tbody>
				<tr align="center" valign="middle" key={row}>
					<td>{json.asset}</td>
					<td>{tokenSymbol}</td>
					<td>{amount}</td>
				</tr>
			</tbody>
		</Table>
		<br/>
		</>
	);
}

/* create custom button with loading indicator which invokes doDepositMinting()
 * @param props.mintMethod the method to use to effect a minting
 * @param props.opsInfo the OpsInfo argument for the smart contract
 * @param props.mvoInfo the MVOinfo argument for the smart contract
 * @param props.tokenContract the __tokenContract argument for the contract
 * @param props.errCase true if we should grey out the button and use err title
 */
function MakeMintFromDepositButton(props) {
	const [isLoading, setLoading] = useState(false);
	const mintMethod = props.mintMethod;
	const opsInfo = props.opsInfo;
	const mvoInfo = props.mvoInfo;
	const tokenContract = props.tokenContract;
	const error = props.errCase;

	useEffect(() => {
		function doRequest() {
			return new Promise((resolve, reject) => mintMethod(resolve,
															   reject,
															   opsInfo,
															   mvoInfo,
															   tokenContract));
		}

		if (isLoading) {
			doRequest().then(() => {
				setLoading(false);
			})
			.catch((err) => {
				setLoading(false);
			});
		}
	}, [isLoading, mintMethod, opsInfo, mvoInfo, tokenContract]);
	const handleClick = () => setLoading(true);

	return (
		<Button type="button" variant="primary"
			title={error ? "Fatal issues were detected in your transaction.  It cannot be submitted for mining"
					: "Submit your transaction to mint eNFTs from your balance on deposit"}
			disabled={isLoading || error}
			onClick={!isLoading ? handleClick : null}
			className="mx-auto m-3"
		>
			{isLoading ? 'Loading…' : 'Mint'}
				<Image src="images/send-check.svg" fluid rounded
					height={30} width={30} />
		</Button>
	);
}

/* create custom button with loading indicator which invokes doEnftSpend()
 * @param props.spendMethod the method to use to effect a spend
 * @param props.opsInfo the OpsInfo argument for the smart contract
 * @param props.mvoInfo the MVOinfo argument for the smart contract
 * @param props.errCase true if we should grey out the button and use err title
 */
function MakeSpendButton(props) {
	const [isLoading, setLoading] = useState(false);
	const spendMethod = props.spendMethod;
	const opsInfo = props.opsInfo;
	const mvoInfo = props.mvoInfo;
	const error = props.errCase;

	useEffect(() => {
		function doRequest() {
			return new Promise((resolve, reject) => spendMethod(resolve,
																reject,
																opsInfo,
																mvoInfo));
		}

		if (isLoading) {
			doRequest().then(() => {
				setLoading(false);
			})
			.catch((err) => {
				setLoading(false);
			});
		}
	}, [isLoading, spendMethod, opsInfo, mvoInfo]);
	const handleClick = () => setLoading(true);

	return (
		<Button type="button" variant="primary"
			title={error ? "Fatal issues were detected in your transaction.  It cannot be submitted for mining"
					: "Submit your transaction to spend eNFTs as described"}
			disabled={isLoading || error}
			onClick={!isLoading ? handleClick : null}
			className="mx-auto m-3"
		>
			{isLoading ? 'Loading…' : 'Spend'}
				<Image src="images/send-check.svg" fluid rounded
					height={30} width={30} />
		</Button>
	);
}

/* create custom button with loading indicator which invokes doEnftBurn()
 * @param props.burnMethod the method to use to effect a burn
 * @param props.opsInfo the OpsInfo argument for the smart contract
 * @param props.mvoInfo the MVOinfo argument for the smart contract
 * @param props.redAmounts the redeemed input amounts, same order as OpsInfo
 * @param props.redRands the redeemed input rand vals, same order as OpsInfo
 * @param props.errCase true if we should grey out the button and use err title
 * @param props.tokenContract the address of the token being redeemed
 */
function MakeBurnButton(props) {
	const [isLoading, setLoading] = useState(false);
	const burnMethod = props.burnMethod;
	const opsInfo = props.opsInfo;
	const mvoInfo = props.mvoInfo;
	const redAmounts = props.redAmounts;
	const redRands = props.redRands;
	const error = props.errCase;
	const tokenContract = props.tokenContract;

	useEffect(() => {
		function doRequest() {
			return new Promise((resolve, reject) => burnMethod(resolve,
															   reject,
															   opsInfo,
															   mvoInfo,
															   redAmounts,
															   redRands,
															   tokenContract));
		}

		if (isLoading) {
			doRequest().then(() => {
				setLoading(false);
			})
			.catch((err) => {
				setLoading(false);
			});
		}
	}, [isLoading, burnMethod, opsInfo, mvoInfo, redAmounts, redRands, tokenContract]);
	const handleClick = () => setLoading(true);

	return (
		<Button type="button" variant="primary"
			title={error ? "Fatal issues were detected in your transaction.  It cannot be submitted for mining"
					: "Submit your transaction to redeem eNFTs as described"}
			disabled={isLoading || error}
			onClick={!isLoading ? handleClick : null}
			className="mx-auto m-3"
		>
			{isLoading ? 'Loading…' : 'Burn'}
				<Image src="images/send-check.svg" fluid rounded
					height={30} width={30} />
		</Button>
	);
}

/* render for review a table of output eNFTs to be minted
 * @param props.opsBlock the JSON object containing the MVO's OB
 * @param props.payeeConfigs list of configs containing the one for this asset
 * @param props.resetConfigs method to reset (clear) used PayeeConfigS
 * which provides the original payee list input to the MVO
 * @param props.deposits the balances held by the user
 * @param props.onNewDeposit method to update a deposit we minted from
 */
function MintedENFTtable(props) {
	const { state: { accounts, contracts, web3, chainConn } } = useEth();

	// aliases
	const enshProtoContract = contracts["EnshroudProtocol"];
	const json = props.opsBlock.MVOSigned;
	const userAcct = web3.utils.toChecksumAddress(accounts[0]);
	// obtain the particular config for this asset
	const payeeConfig = props.payeeConfigs.find(
											conf => conf.asset === json.asset);
	const balances = props.deposits;

	// track result of minting with a DAlert (empty on success)
	const [mintResult, setMintResult] = useState("");

	// method to perform a EnshroudProtocol.mintENFTsFromDeposit()
	const doDepositMinting
		= async (resolve, reject, opsInfo, mvoInfo, asset) => {
		if (asset === undefined || opsInfo === undefined
			|| mvoInfo === undefined
			|| asset === '0x0000000000000000000000000000000000000000')
		{
			let noInput = new Error("doDepositMinting(): missing input param");
			const errAlert = <DAlert title="Deposit minting failed"
								data={noInput.message} />;
			setMintResult(errAlert);
			reject(noInput);
			return false;
		}

		// generate the call to do the minting
		var gotErr = false;
		await enshProtoContract.methods.mintENFTsFromDeposit(opsInfo, mvoInfo,
															 asset)
				.send({from: userAcct})
			.then(receipt => {
				// 1. debit user's deposit balance in {asset}
				const dep = balances.find(elt => elt.contractAddress === asset);
				let curBal = web3.utils.toBN(dep.balanceOf);
				const mintAmt = web3.utils.toBN(opsInfo.amount);
				curBal = curBal.sub(mintAmt);
				if (curBal.lt(0)) {
					console.error("minting resulted in negative wei balance of "
								+ curBal.toString());
				} else {
					dep.balanceOf = curBal.toString();
					// update in caller-level state
					props.onNewDeposit(dep);
				}

				// 2. wipe previous payees and notify caller
				payeeConfig.payees.splice(0, payeeConfig.payees.length);
				props.resetConfigs(payeeConfig);

				alert("Your eNFT minting succeeded!");
				setMintResult('');
			})
			.catch(err => {
				console.error("Minting of eNFTs from deposit failed, code: "
							+ err.code + ", message: " + err.message);
				// clear previous error if any
				if (mintResult !== '') setMintResult('');
				const mintFail = <DAlert variant="danger"
								title="Your minting of eNFTs did not succeed"
								data={"due to: " + err.message} />;
				alert("Your minting of eNFTs did not succeed, due to: "
						+ err.message);
				setMintResult(mintResult => (mintFail));
				reject(err);
				gotErr = true;
			});
		if (gotErr) {
			return false;
		}
		resolve(true);
		return true;
	};

	// args for mintENFTsFromDeposit()
	var OpsInfo = {
		recipients: [],		// address[]
		detailsHashes: [],	// bytes32[]
		inputIds: [],		// uint256[], not used
		outputIds: [],		// uint256[]
		metadata: [],		// string[]
		amount: "0x" + json.amount,	// uint256
		obHash: "0x" + json.argsHash,	// bytes32
	};

	// another arg for mintENFTsFromDeposit()
	var MVOinfo = {
		mvoSigs: [],	// bytes[]
		mvoIDs: [],	// string[]
	}
	// push all MVO obHash sigs
	const nf = new Intl.NumberFormat("en-US", {minimumIntegerDigits: 3});
	let mIdx = 1;
	props.opsBlock.signatures.forEach(signature => {
		const mvoIdx = nf.format(mIdx++);
		MVOinfo.mvoSigs.push('0x' + signature[mvoIdx].argsSig);
		MVOinfo.mvoIDs.push(signature[mvoIdx].signer);
	});

	/* Find the corresponding payee item for each minted eNFT, compute the
	 * detailsHash, and verify vs. the passed detailsHash value.  This ensures
	 * that the MVOs didn't do anything dirty.
	 * The payees and eNFTs can safely be assumed to occur in the same order.
	 * As we cycle through the eNFTs, we also build the arguments to be passed
	 * to the EnshroudProtocol.mintENFTsFromDeposit() method (via OpsInfo).
	 */
	var sum = new BigNumber(0.0);
	let idx = 0;
	var fatalFoulup = false;
	var errDescrip = '';
	if (json.outputs.length !== payeeConfig.payees.length) {
		console.error("Unexpected number of eNFTs (" + json.outputs.length
					+ "), should be equal to " + payeeConfig.payees.length);
		const errMsg = "number of output eNFTs is " + json.outputs.length
						+ ", should be " + payeeConfig.payees.length;
		errDescrip = <DAlert variant="warning"
						title="Cannot mint eNFTs"
						data={"due to: " + errMsg} />;
		fatalFoulup = true;
	}

	// create map of ids to bool for detailsHash matches
	const detailsMatches = new Map();
	// create map of ids to ethers amounts
	const outputAmts = new Map();
	// create array of output eNFT specs
	const outEnfts = [];

	payeeConfig.payees.forEach(payee => {
		sum = sum.plus(payee.amount);
		const output = json.outputs[idx];
		// get the .001, .002 etc element, which will be an output spec
		const outIdx = nf.format(idx+1);
		const outputSpec = output[outIdx];
		// build detailsHash from combination of these records
		if (outputSpec.address !== payee.address) {
			console.error("Mismatch: outputSpec[" + outIdx + "] has address "
						+ outputSpec.address + ", s/b payee address "
						+ payee.address);
			fatalFoulup = true;
			if (errDescrip === '') {
				const eMsg = "unexpected payee on eNFT[" + outIdx
							+ "], should be " + payee.address + " not "
							+ outputSpec.address;
				errDescrip = <DAlert variant="warning"
								title="Cannot mint eNFTs"
								data={"due to: " + eMsg} />;
			}
		} else {
			outputAmts.set(outputSpec.id, payee.amount.toString());
			OpsInfo.recipients.push(outputSpec.address);
			const idAsUint256 = "0x" + outputSpec.id;
			OpsInfo.outputIds.push(idAsUint256);
			OpsInfo.metadata.push(JSON.stringify(outputSpec.metadata));
			const weiAmt = web3.utils.toWei(payee.amount.toString());
			const detailsHash = buildDetailsHash(outputSpec.address,
												 outputSpec.id,
												 json.asset,
												 web3.utils.numberToHex(weiAmt),
												 payee.rand,
												 web3);
			if (detailsHash === outputSpec.hash) {
				detailsMatches.set(outputSpec.id, true);
				OpsInfo.detailsHashes.push("0x" + outputSpec.hash);
			} else {
				detailsMatches.set(outputSpec.id, false);
				console.error("eNFT Id " + outputSpec.id
							+ " failed hash check!");
				fatalFoulup = true;
				if (errDescrip === '') {
					const eMsg = "eNFT with Id " + outputSpec.id
								+ " failed details hash check";
					errDescrip = <DAlert variant="warning"
									title="Cannot mint eNFTs"
									data={"due to: " + eMsg} />;
				}
			}
		}
		outEnfts.push(outputSpec);
		idx++;
	});

	// double-check the sum
	const weiSum = web3.utils.toWei(sum.toString());
	const jsonAmt = web3.utils.hexToNumberString("0x" + json.amount);
	if (weiSum !== jsonAmt) {
		console.error("output eNFTs account for " + weiSum + " while amount is "
					+ jsonAmt);
		fatalFoulup = true;
		if (errDescrip === '') {
			const eMsg = "output eNFTs add up to " + weiSum
						+ ", but total mint amount is " + jsonAmt;
			errDescrip = <DAlert variant="warning"
							title="Cannot mint eNFTs"
							data={"due to: " + eMsg} />;
		}
	}

	// test whether the passed obHash comes out correctly when computed here
	const locObEncode = web3.eth.abi.encodeParameters(
						['uint256', 'uint256[]', 'bytes32[]'],
					[OpsInfo.amount, OpsInfo.outputIds, OpsInfo.detailsHashes]);
	const locObHash = web3.utils.sha3(locObEncode);
	if (locObHash === OpsInfo.obHash) {
		// now check the MVO signatures
		let mIdx = 0;
		MVOinfo.mvoIDs.forEach(mId => {
			const mvoStaking = chainConn.MVOConf.getMVOStaking(mId);
			const mvoSig = MVOinfo.mvoSigs[mIdx++];
			const mvoAcct = web3.eth.accounts.recover(locObHash, mvoSig, true);
			if (mvoAcct !== mvoStaking.signingAddress) {
				console.error("sig of " + mId + " on OBhash failed");
				fatalFoulup = true;
				const eMsg = "bad MVO signature on OperationsBlock hash for "
							+ mId;
				if (errDescrip === '') {
					errDescrip = <DAlert variant="warning"
									title="Cannot mint eNFTs"
									data={"due to: " + eMsg} />;
				}
			}
		});
	} else {
		console.error("mint obHash does not match");
		fatalFoulup = true;
		if (errDescrip === '') {
			errDescrip = <DAlert variant="warning"
							title="Cannot mint eNFTs"
							data="due to: bad OperationsBlock hash value" />;
		}
	}

	// actual rendering
	return (
		<>
		<Table bordered responsive hover variant="dark">
			<caption className="caption-top">
				eNFTs You Are Minting:
			</caption>
			<thead>
				<tr align="center" valign="middle" key="mintedEnftHdr">
					<th scope="col"
						title="Address to which the eNFT will be minted">
						Payee
					</th>
					<th scope="col"
						title="Amount shown in ethers units for readability">
						Amount (1e-18)
					</th>
					<th scope="col">Unique ID of eNFT</th>
					<th scope="col"
						title="Recorded on blockchain when minted">
						Details Hash
					</th>
					<th scope="col"
						title="Whether calculated details hash matches MVO's value">
						Valid Hash
					</th>
					<th scope="col"
						title="Encrypted data emitted on event log during minting">
						eNFT Metadata (encrypted)
					</th>
				</tr>
			</thead>
			<tbody>
				{outEnfts.map((eNFTspec) =>
					<tr align="center" valign="middle" key={eNFTspec.id}>
						<td className="text-break"
							title={eNFTspec.address === userAcct ? "Your account" : "Recipient account"}>
							{eNFTspec.address}
						</td>
						<td>{outputAmts.get(eNFTspec.id)}</td>
						<td className="text-break">
							{eNFTspec.id}
						</td>
						<td className="text-break">{eNFTspec.hash}</td>
						<td>
							{detailsMatches.get(eNFTspec.id)
								? <Badge pill bg="success"
									title="Details hash matches MVO's value">
									<Image src="images/check2.svg" fluid rounded
									className="p-2" height={40} width={40} />
								  </Badge>
								: <Badge pill bg="danger"
									title="MVO's details hash appears invalid">
									<Image src="images/x-lg.svg" fluid rounded
									className="p-2" height={40} width={40} />
								  </Badge>
							}
						</td>
						<td className="text-break">
							<DataToast
								variant="info"
								buttonText="eNFT metadata"
								buttonIcon="images/receipt.svg"
								buttonTitle="Display generated ERC-1155 metadata"
								title="This data will be emitted in a URI event"
								index={eNFTspec.id}
								data={JSON.stringify(eNFTspec.metadata)}
							/>
						</td>
					</tr>
				)}
			</tbody>
		</Table>
		<br/>

		{ /* provide loading button to invoke smart contract, greyed on err */ }
		<MakeMintFromDepositButton
			mintMethod={(resolve, reject, opsInfo, mvoInfo, contract) => doDepositMinting(resolve, reject, opsInfo, mvoInfo, contract)}
			opsInfo={OpsInfo}
			mvoInfo={MVOinfo}
			tokenContract={json.asset}
			errCase={fatalFoulup}
		/>
		<br/>
		{mintResult === '' ? errDescrip : mintResult}
		<br/>
		</>
	);
}

/* render for review two tables: spend inputs, and output eNFTs to be minted
 * @param props.opsBlock the JSON object containing the MVO's OB
 * @param props.eNFTList list of downloaded eNFTs
 * @param props.payeeConfigs configs containing the original payee list input
 * to the MVO (which may spread across multiple configs)
 * @param props.resetConfigs method to alter passed payeeConfigs upon success
 */
function SpendENFTtables(props) {
	const { state: { accounts, contracts, web3, chainConn } } = useEth();

	// track result of minting with a DAlert (empty on success)
	const [spendResult, setSpendResult] = useState("");

	// aliases
	const enshProtoContract = contracts["EnshroudProtocol"];
	const json = props.opsBlock.MVOSigned;
	const userAcct = web3.utils.toChecksumAddress(accounts[0]);
	const enftList = props.eNFTList;

	/* We must reconstruct the payee list.  To do this we'll walk through the
	 * payeeConfigs records and observe which ones are marked active.
	 * We also total up the count of payees across all active payeeConfigs.
	 */
	var outputCnt = 0;
	const relevantConfigs = [];
	props.payeeConfigs.forEach(pConfig => {
		if (pConfig.active) {
			relevantConfigs.push(pConfig);
			outputCnt += pConfig.payees.length;
		}
	});

	// method to perform a EnshroudProtocol.spendENFTs()
	const doEnftSpend = async (resolve, reject, opsInfo, mvoInfo) => {
		if (opsInfo === undefined || mvoInfo === undefined) {
			let noInput = new Error("doEnftSpend(): missing input param");
			const errAlert = <DAlert title="eNFT spend failed"
								data={noInput.message} />;
			setSpendResult(errAlert);
			reject(noInput);
			return false;
		}

		// generate the call to do the spend
		var gotErr = false;
		await enshProtoContract.methods.spendENFTs(opsInfo, mvoInfo)
				.send({from: userAcct })
			.then(receipt => {
				// re-render SpendENFTs page with these changes
				props.resetConfigs(relevantConfigs);
				alert("Your eNFT spend succeeded!");
				setSpendResult('');
			})
			.catch(err => {
				console.error("Spend of eNFTs failed: "
							+ err.code + ", " + err.message);
				// clear previous error if any
				if (spendResult !== '') setSpendResult('');
				const spendFail = <DAlert variant="danger"
									title="Your spend of eNFTs did not succeed"
									data={"due to: " + err.message} />;
				alert("Your spend of eNFTs did not succeed, due to: "
						+ err.message);
				setSpendResult(spendFail);
				reject(err);
				gotErr = true;
			});
		if (gotErr) {
			return false;
		}
		resolve(true);
		return true;
	};

	// args for spendENFTs()
	var OpsInfo = {
		recipients: [],		// address[]
		detailsHashes: [],	// bytes32[]
		inputIds: [],		// uint256[]
		outputIds: [],		// uint256[]
		metadata: [],		// string[]
		amount: 0n,			// uint256, not used
		obHash: "0x" + json.argsHash,	// bytes32
	};

	// another arg for spendENFTs()
	var MVOinfo = {
		mvoSigs: [],	// bytes[]
		mvoIDs: [],		// string[]
	}
	// push all MVO obHash sigs
	const nf = new Intl.NumberFormat("en-US", {minimumIntegerDigits: 3});
	let mIdx = 1;
	props.opsBlock.signatures.forEach(signature => {
		const mvoIdx = nf.format(mIdx++);
		MVOinfo.mvoSigs.push('0x' + signature[mvoIdx].argsSig);
		MVOinfo.mvoIDs.push(signature[mvoIdx].signer);
	});

	/* Walk through the inputs[] returned.  Each will contain an ID and a hash.
	 * We must find the Enft with that ID and compute its hash ourselves and
	 * match it to the value returned from the MVO.
	 */
	var badInput = false;
	var idx = 0;
	// create map of input ids to bool for detailsHash matches
	const inputHashMatches = new Map();

	// create list of burnt input eNFTs
	const burntEnfts = [];

	for (const input of json.inputs) {
		// get the .001, .002 etc element, which will be an input spec
		const inIdx = nf.format(idx+1);
		const inputSpec = input[inIdx];
		const inpEnft = enftList.find(elt => elt.id === inputSpec.id);
		if (inpEnft === undefined) {
			badInput = true;
			console.error("No available eNFT found for input Id "
						+ inputSpec.id);
			continue;
		}

		// compute details hash
		const detHash = buildDetailsHash(inpEnft.owner,
										 inpEnft.id,
										 inpEnft.asset,
										 web3.utils.numberToHex(inpEnft.amount),
										 inpEnft.rand,
										 web3);
		if (detHash !== inputSpec.hash) {
			console.error("Details hash for input Id " + inputSpec.id
						+ " does not match");
			badInput = true;
			inputHashMatches.set(inputSpec.id, false);
		} else {
			inputHashMatches.set(inputSpec.id, true);
			const idAsUint256 = "0x" + inpEnft.id;
			OpsInfo.inputIds.push(idAsUint256);
		}

		// record computed detHash as additional field in burned eNFT
		inpEnft.detHash = detHash;
		burntEnfts.push(inpEnft);
		idx++;
	}

	/* Find the corresponding payee item for each output eNFT, compute the
	 * detailsHash, and verify vs. the passed detailsHash value.  This ensures
	 * that the MVOs didn't do anything dirty.
	 * The payees and eNFTs can safely be assumed to occur in the same order.
	 * As we cycle through the eNFTs, we also build the arguments to be passed
	 * to the EnshroudProtocol.spendENFTs() method (via OpsInfo).
	 */
	var fatalFoulup = false;
	var errDescrip = '';
	if (json.outputs.length !== outputCnt) {
		console.error("Unexpected number of eNFTs (" + json.outputs.length
					+ "), should be equal to " + outputCnt);
		const errMsg = "number of output eNFTs is " + json.outputs.length
						+ ", should be " + outputCnt;
		errDescrip = <DAlert variant="warning"
						title="Cannot perform spend"
						data={"due to: " + errMsg} />;
		fatalFoulup = true;
	}

	// create map of ids to bool for output detailsHash matches
	const detailsMatches = new Map();
	// create map of ids to ethers amounts
	const outputAmts = new Map();
	// create map of assets to asset symbol
	const outputSymbols = new Map();
	// create array of output eNFT specs
	const outEnfts = [];

	idx = 0;
	for (const output of json.outputs) {
		// get the .001, .002 etc element, which will be an Enft output spec
		const outIdx = nf.format(idx+1);
		const outputSpec = output[outIdx];
		/* look through all relevantConfigs seeking a payee to this address
		 * (note these may appear in any order vs. payee00x sequences)
		 */
		var candidatePayee = undefined;
		for (const pConfig of relevantConfigs) {
			for (const payee of pConfig.payees) {
				if (payee.address === outputSpec.address) {
					candidatePayee = payee;
					// convert Id from zero-padded hex
					const idAsUint256 = "0x" + outputSpec.id;
					const weiAmt = web3.utils.toWei(payee.amount.toString());
					// compute details hash assuming this is the match
					const detHash = buildDetailsHash(outputSpec.address,
													 outputSpec.id,
													 pConfig.asset,
												 web3.utils.numberToHex(weiAmt),
													 payee.rand,
													 web3);
					if (detHash === outputSpec.hash) {
						// this is the matching payee!
						detailsMatches.set(outputSpec.id, true);
						outputAmts.set(outputSpec.id, payee.amount.toString());
						if (!outputSymbols.has(pConfig.asset)) {
							outputSymbols.set(pConfig.asset, pConfig.symbol);
						}
						// add the asset to the output spec for display
						outputSpec.asset = pConfig.asset;
						OpsInfo.recipients.push(outputSpec.address);
						OpsInfo.outputIds.push(idAsUint256);
						OpsInfo.metadata.push(JSON.stringify(
														outputSpec.metadata));
						OpsInfo.detailsHashes.push("0x" + outputSpec.hash);
						break;
					} else {
						candidatePayee = undefined;
					}
				}
			}
			if (candidatePayee !== undefined) break;
		}
		if (candidatePayee === undefined) {
			// never found
			fatalFoulup = true;
			const eMsg = "No matching payee ever found for output eNFT to "
						+ outputSpec.address;
			console.error(eMsg);
			if (errDescrip === '') {
				errDescrip = <DAlert variant="warning"
								title="Cannot spend eNFTs"
								data={"due to: " + eMsg} />;
			}
			detailsMatches.set(outputSpec.id, false);
		}
		outEnfts.push(outputSpec);
		idx++;
	}

	// test whether the passed obHash comes out correctly when computed here
	const locObEncode = web3.eth.abi.encodeParameters(
						['uint256[]', 'uint256[]', 'bytes32[]'],
				[OpsInfo.inputIds, OpsInfo.outputIds, OpsInfo.detailsHashes]);
	const locObHash = web3.utils.sha3(locObEncode);
	if (locObHash === OpsInfo.obHash) {
		// now check the MVO signatures
		let mIdx = 0;
		MVOinfo.mvoIDs.forEach(mId => {
			const mvoStaking = chainConn.MVOConf.getMVOStaking(mId);
			const mvoSig = MVOinfo.mvoSigs[mIdx++];
			const mvoAcct = web3.eth.accounts.recover(locObHash, mvoSig, true);
			if (mvoAcct !== mvoStaking.signingAddress) {
				console.error("sig of " + mId + " on OBhash failed");
				fatalFoulup = true;
				const eMsg = "bad MVO signature on OperationsBlock hash for "
							+ mId;
				if (errDescrip === '') {
					errDescrip = <DAlert variant="warning"
									title="Cannot mint eNFTs"
									data={"due to: " + eMsg} />;
				}
			}
		});
	} else {
		console.error("spend obHash does not match");
		fatalFoulup = true;
		if (errDescrip === '') {
			errDescrip = <DAlert variant="warning"
							title="Cannot mint eNFTs"
							data="due to: bad OperationsBlock hash value" />;
		}
	}

	// actual rendering
	return (
		<>
		{ /* input eNFT table */ }
		<Table bordered responsive hover variant="dark">
			<caption className="caption-top">
				Input eNFTs You Are Burning:
			</caption>
			<thead>
				<tr align="center" valign="middle" key="burnedEnftHdr">
					<th scope="col"
						title="These eNFTs will be destroyed in the operation">
						ID of Used eNFT
					</th>
					<th scope="col"
						title="Shown in ethers units for readability">
						Amount (1e-18)
					</th>
					<th scope="col">Token</th>
					<th scope="col">Details Hash</th>
					<th scope="col"
						title="Whether the calculated hash matches the MVO's value">
						Hash Verified
					</th>
				</tr>
			</thead>
			<tbody>
				{burntEnfts.map((eNFTspec) =>
					<tr align="center" valign="middle" key={eNFTspec.id}>
						<td className="text-break">{eNFTspec.id}</td>
						<td>
							{web3.utils.fromWei(eNFTspec.amount.toString())}
						</td>
						<td title={eNFTspec.asset}>
							{outputSymbols.get(eNFTspec.asset)}
						</td>
						<td className="text-break">{eNFTspec.detHash}</td>
						<td>
							{inputHashMatches.get(eNFTspec.id)
								? <Badge pill bg="success"
									title="Calculated details hash matches MVO's value">
									<Image src="images/check2.svg" fluid rounded
									className="p-2" height={40} width={40} />
								  </Badge>
								: <Badge pill bg="danger"
								title="Calculated details hash did not match">
									<Image src="images/x-lg.svg" fluid rounded
									className="p-2" height={40} width={40} />
								  </Badge>
							}
						</td>
					</tr>
				)}
			</tbody>
		</Table>
		<br/><br/>

		{ /* output eNFT table */ }
		<Table bordered responsive hover variant="dark">
			<caption className="caption-top">
				Output eNFTs You Are Minting:
			</caption>
			<thead>
				<tr align="center" valign="middle" key="mintedEnftHdr">
					<th scope="col" title="The new eNFT will be minted to this address">
						Payee
					</th>
					<th scope="col"
						title="Shown in ethers units for readability">
						Amount (1e-18)
					</th>
					<th scope="col">Token</th>
					<th scope="col">Unique ID of New eNFT</th>
					<th scope="col">Details Hash</th>
					<th scope="col"
						title="Whether the calculated hash matches the MVO's value">
						Hash Verified
					</th>
					<th scope="col"
						title="Encrypted data emitted on event log during minting">
						eNFT Metadata (encrypted)
					</th>
				</tr>
			</thead>
			<tbody>
				{outEnfts.map((eNFTspec) =>
					<tr align="center" valign="middle" key={eNFTspec.id}>
						<td className="text-break"
							title={eNFTspec.address === userAcct ? "Your account" : "Recipient account"}>
							{eNFTspec.address}
						</td>
						<td>{outputAmts.get(eNFTspec.id)}</td>
						<td title={eNFTspec.asset}>
							{outputSymbols.get(eNFTspec.asset)}
						</td>
						<td className="text-break">{eNFTspec.id}</td>
						<td className="text-break">{eNFTspec.hash}</td>
						<td>
							{detailsMatches.get(eNFTspec.id)
								? <Badge pill bg="success"
									title="Calculated details hash matches MVO's value">
									<Image src="images/check2.svg" fluid rounded
									className="p-2" height={40} width={40} />
								  </Badge>
								: <Badge pill bg="danger"
								title="Calculated details hash did not match">
									<Image src="images/x-lg.svg" fluid rounded
									className="p-2" height={40} width={40} />
								  </Badge>
							}
						</td>
						<td>
							<DataToast
								variant="info"
								buttonText="eNFT metadata"
								buttonIcon="images/receipt.svg"
								buttonTitle="Display generated ERC-1155 metadata"
								title="This data will be emitted in a URI event"
								index={eNFTspec.id}
								data={JSON.stringify(eNFTspec.metadata)}
							/>
						</td>
					</tr>
				)}
			</tbody>
		</Table>
		<br/><br/>

		{ /* provide loading button to invoke smart contract, greyed on err */}
		<MakeSpendButton
			spendMethod={(resolve, reject, opsInfo, mvoInfo) => doEnftSpend(resolve, reject, opsInfo, mvoInfo)}
			opsInfo={OpsInfo}
			mvoInfo={MVOinfo}
			errCase={badInput || fatalFoulup}
		/>
		<br/>
		{spendResult === '' ? errDescrip : spendResult}
		<br/>
		</>
	);
}

/* render for review the table of inputs used for a burn, and the burn data
 * @param props.opsBlock the JSON object containing the MVO's OB
 * @param props.payeeConfigs configs containing the original payee list input
 * @param props.resetConfigs method to alter passed payeeConfig upon success
 * to the MVO (which will all be in the designated asset's config)
 * @param props.eNFTList list of downloaded eNFTs
 */
function BurnENFTtable(props) {
	const { state: { accounts, contracts, web3, chainConn } } = useEth();

	// track result of minting with a DAlert
	const [burnResult, setBurnResult] = useState("");

	// aliases
	const enshProtoContract = contracts["EnshroudProtocol"];
	const json = props.opsBlock.MVOSigned;
	const userAcct = web3.utils.toChecksumAddress(accounts[0]);
	const enftList = props.eNFTList;
	const burnAsset = json.asset;
	const burnTotal = web3.utils.hexToNumberString("0x" + json.amount);

	// find the relevant config (matches asset, should be active)
	const payeeConfig = props.payeeConfigs.find(elt => elt.asset === burnAsset);
	if (payeeConfig === undefined || !payeeConfig.active) {
		// we cannot continue processing if this happens
		const eMsg = "No payeeConfig set and active for " + burnAsset;
		console.error(eMsg);
		alert("Cannot spend eNFTs due to internal error: " + eMsg
				+ "; try reloading page");
		return false;
	}

	// method to perform a EnshroudProtocol.redeemENFTsAndWithdraw()
	const doEnftBurn = async (resolve, reject, opsInfo, mvoInfo, redAmounts,
							  redRands, tokenContract) => {
		if (opsInfo === undefined || mvoInfo === undefined
			|| redAmounts === undefined || redRands === undefined
			|| tokenContract === undefined || tokenContract === '')
		{
			let noInput = new Error("doEnftBurn(): missing input param");
			const errAlert = <DAlert title="Burn deminting failed"
								data={noInput.message} />;
			setBurnResult(errAlert);
			reject(noInput);
			return false;
		}

		// generate the call to do the spend
		var gotErr = false;
		await enshProtoContract.methods.redeemENFTsAndWithdraw(opsInfo,
															   mvoInfo,
															   redAmounts,
															   redRands,
															   tokenContract)
				.send({from: userAcct })
			.then(receipt => {
				// re-render BurnENFTs page with these changes
				props.resetConfigs(payeeConfig);
				alert("Your burn of eNFTs succeeded!");
				setBurnResult('');
			})
			.catch(err => {
				console.error("Burn of eNFTs failed: "
							+ err.code + ", " + err.message);
				// clear previous error if any
				if (burnResult !== '') setBurnResult('');
				const burnFail = <DAlert variant="danger"
								title="Your burn of eNFTs did not succeed"
								data={"due to: " + err.message} />;
				alert("Your burn of eNFTs did not succeed, due to: "
						+ err.message);
				setBurnResult(burnResult => (burnFail));
				reject(err);
				gotErr = true;
			});
		if (gotErr) {
			return false;
		}
		resolve(true);
		return true;
	};

	// args for redeemENFTsAndWithdraw()
	var OpsInfo = {
		recipients: [],		// address[] (for output, if any)
		detailsHashes: [],	// bytes32[] (for output, if any)
		inputIds: [],		// uint256[]
		outputIds: [],		// uint256[] (for output, if any)
		metadata: [],		// string[] (for output, if any)
		amount: burnTotal,	// uint256, total amount withdrawn on-chain
		obHash: "0x" + json.argsHash,	// bytes32
	};

	// another arg for redeemENFTsAndWithdraw()
	var MVOinfo = {
		mvoSigs: [],	// bytes[]
		mvoIDs: [],	// string[]
	}
	// push all MVO obHash sigs
	const nf = new Intl.NumberFormat("en-US", {minimumIntegerDigits: 3});
	let mIdx = 1;
	props.opsBlock.signatures.forEach(signature => {
		const mvoIdx = nf.format(mIdx++);
		MVOinfo.mvoSigs.push('0x' + signature[mvoIdx].argsSig);
		MVOinfo.mvoIDs.push(signature[mvoIdx].signer);
	});

	// also build array of input amounts, in same order as Ids
	var inputAmounts = [];

	// also build array of input rand values as bytes16, in same order as Ids
	var inputRands = [];

	// build list of eNFTs being burned
	const burntEnfts = [];

	// total value represented by all eNFTs being burned (in ethers units)
	var inputTotal = new BigNumber(0);

	/* Walk through the inputs[] returned.  Each will contain an ID, amount,
	 * and rand value.  We must find the Enft with that ID and compute its
	 * hash ourselves and match it to the value returned from the MVO.
	 * At the same time, we build arrays of amounts and rand strings so that
	 * the smart contract can confirm detHashes itself during the operation.
	 */
	var badInput = false;
	var errDescrip = '';
	var idx = 0;
	for (const input of json.inputs) {
		// get the .001, .002 etc element, which will be an Enft input spec
		const inIdx = nf.format(idx+1);
		const inputSpec = input[inIdx];
		const inpEnft = enftList.find(elt => elt.id === inputSpec.id);
		if (inpEnft === undefined) {
			badInput = true;
			const eMsg = "No available eNFT found for input Id " + inputSpec.id;
			console.error(eMsg);
			errDescrip = <DAlert title="eNFT burn failed" variant="warning"
							data={"due to: " + eMsg} />;
			continue;
		}
		const ethersAmt = web3.utils.fromWei(inpEnft.amount.toString());
		inputTotal = inputTotal.plus(ethersAmt);

		// ensure the stipulated details match
		if (inputSpec.asset !== burnAsset
			|| inpEnft.asset !== inputSpec.asset
			|| inpEnft.owner !== userAcct
			|| inputSpec.amount !== inpEnft.amount
			|| inputSpec.rand !== inpEnft.rand)
		{
			badInput = true;
			const errMsg = "eNFT data for input Id " + inputSpec.id
						+ " does not match burn input spec";
			console.error(errMsg);
			errDescrip = <DAlert title="eNFT burn failed" variant="warning"
							data={"due to: " + errMsg} />;
			continue;
		}

		// compute details hash
		const detHash = buildDetailsHash(inpEnft.owner,
										 inpEnft.id,
										 burnAsset,
										 web3.utils.numberToHex(inpEnft.amount),
										 inpEnft.rand,
										 web3);
		// record computed detHash as additional field in burned eNFT
		inpEnft.detHash = detHash;
		burntEnfts.push(inpEnft);

		const idAsUint256 = "0x" + inputSpec.id;
		OpsInfo.inputIds.push(idAsUint256);
		inputAmounts.push(inputSpec.amount);
		// convert rand term to bytes16
		const randBytes = atob(inputSpec.rand);
		const salt = Uint8Array.from(randBytes, x => x.charCodeAt(0));
		const hexSalt = web3.utils.bytesToHex(salt);
		inputRands.push(hexSalt);
		idx++;
	}

	/* If the inputs[] total is greater than the burnTotal, then we're due
	 * a change eNFT.  This is indicated by the presence of an outputs[]
	 * in the MVO's JSON.  If this exists there will be an address (required to
	 * be equal to sender), an ID, a rand value (only case where MVO generates
	 * this), an amount, and the metadata.  From all this we must create the
	 * detailsHash and fill in OpsInfo arrays accordingly.  There will be
	 * exactly one such output in this case.
	 */
	var outputTable = '';
	const burnTot = web3.utils.fromWei(burnTotal);
	if (json.outputs !== undefined) {
		const indx = "001";
		if (json.outputs.length !== 1) {
			const eMsg = "Unexpected number of eNFTs (" + json.outputs.length
						+ "), should be 1";
			console.error(eMsg);
			badInput = true;
			if (errDescrip === '') {
				errDescrip = <DAlert variant="warning"
								title="Cannot burn eNFTs"
								data={"due to: " + eMsg} />;
			}
		} else {
			// get the .001 element, which will be an Enft output spec
			const changeEnft = json.outputs[0][indx];
			// calculate amount this should be
			var overage = inputTotal.minus(burnTot);
			const expAmt = web3.utils.toWei(overage.toString());
			if (expAmt !== changeEnft.amount) {
				console.error("Change amount not as calculated, calc = "
								+ expAmt + ", actual = " + changeEnft.amount);
				if (errDescrip === '') {
					errDescrip = <DAlert variant="warning"
									title="Cannot burn eNFTs"
									data={"Incorrect change amount, should be " + changeEnft.amount} />;
				}
				badInput = true;
			} else if (changeEnft.address !== userAcct) {
				console.error("Change address not sender, came as: "
								+ changeEnft.address);
				badInput = true;
			} else {
				// pass data to OpsInfo
				const idAsUint256 = "0x" + changeEnft.id;
				OpsInfo.recipients[0] = changeEnft.address;	// self
				OpsInfo.outputIds[0] = idAsUint256;
				OpsInfo.metadata[0] = JSON.stringify(changeEnft.metadata);
				const detHash = buildDetailsHash(changeEnft.address,
										 		 changeEnft.id,
										 		 burnAsset,
									web3.utils.numberToHex(changeEnft.amount),
										 		 changeEnft.rand,
												 web3);
				OpsInfo.detailsHashes.push("0x" + detHash);
				// NB: smart contract will verify this details hash
			}
		}

		if (!badInput) {
			const eNFTspec = json.outputs[0][indx];
			// prep the display of output table (length 1)
			outputTable =
			<>
				<Table bordered responsive hover variant="dark">
					<caption className="caption-top">
						Change eNFT You Are Minting:
					</caption>
					<thead>
						<tr align="center" valign="middle" key="mintedEnftHdr">
							<th scope="col">Payee (Yourself)</th>
							<th scope="col"
								title="Shown in ethers units for readability">
								Amount (1e-18)
							</th>
							<th scope="col">Token</th>
							<th scope="col">Unique ID of New eNFT</th>
							<th scope="col"
								title="Recorded on blockchain during minting">
								Details Hash
							</th>
							<th scope="col"
								title="Encrypted metadata emitted on event log during minting">
								eNFT Metadata (encrypted)
							</th>
						</tr>
					</thead>
					<tbody>
						<tr align="center" valign="middle" key="changeOutput">
							<td className="text-break" title="Your account">
								{eNFTspec.address}
							</td>
							<td>
								{web3.utils.fromWei(eNFTspec.amount.toString())}
							</td>
							<td title={burnAsset}>{payeeConfig.symbol}</td>
							<td className="text-break">
								{eNFTspec.id}
							</td>
							<td className="text-break">
								{web3.utils.stripHexPrefix(OpsInfo.detailsHashes[0])}
							</td>
							<td>
								<DataToast
									variant="info"
									buttonText="eNFT metadata"
									buttonIcon="images/receipt.svg"
									buttonTitle="Display generated ERC-1155 metadata"
									title="This data will be emitted in a URI event"
									data={JSON.stringify(eNFTspec.metadata)}
								/>
							</td>
						</tr>
					</tbody>
				</Table>
				<br/><br/>
			</>;
		}
	}

	// test whether the passed obHash comes out correctly when computed here
	var fatalFoulup = false;
	let locObEncode = undefined;
	if (OpsInfo.outputIds.length > 0) {
		// we have a change output eNFT, with an Id and detHash
		locObEncode = web3.eth.abi.encodeParameters(
						['uint256[]', 'uint256', 'uint256[]', 'bytes32[]'],
						[OpsInfo.inputIds, OpsInfo.amount, OpsInfo.outputIds,
						OpsInfo.detailsHashes]);
	} else {
		locObEncode = web3.eth.abi.encodeParameters(
						['uint256[]', 'uint256'],
						[OpsInfo.inputIds, OpsInfo.amount]);
	}
	const locObHash = web3.utils.sha3(locObEncode);
	if (locObHash === OpsInfo.obHash) {
		// now check the MVO signatures
		let mIdx = 0;
		MVOinfo.mvoIDs.forEach(mId => {
			const mvoStaking = chainConn.MVOConf.getMVOStaking(mId);
			const mvoSig = MVOinfo.mvoSigs[mIdx++];
			const mvoAcct = web3.eth.accounts.recover(locObHash, mvoSig, true);
			if (mvoAcct !== mvoStaking.signingAddress) {
				console.error("sig of " + mId + " on OBhash failed");
				fatalFoulup = true;
				const eMsg = "bad MVO signature on OperationsBlock hash for "
							+ mId;
				if (errDescrip === '') {
					errDescrip = <DAlert variant="warning"
									title="Cannot burn eNFTs"
									data={"due to: " + eMsg} />;
				}
			}
		});
	} else {
		console.error("burn obHash does not match");
		fatalFoulup = true;
		if (errDescrip === '') {
			errDescrip = <DAlert variant="warning"
							title="Cannot burn eNFTs"
							data="due to: bad OperationsBlock hash value" />;
		}
	}

	// display the on-chain amount involved (burnTotal, in ethers)
	var onChainSection = '';
	if (!badInput && !fatalFoulup) {
		onChainSection =
		<>
			<Table bordered responsive>
				<caption className="caption-top">
					Withdrawal On-Chain
				</caption>
				<thead>
					<tr align="center" valign="middle" key="mintedEnftHdr">
						<th scope="col">Payee Account (Yourself)</th>
						<th scope="col"
							title="Shown in ethers units for readability">
							Amount (Before Withdrawal Fee)
						</th>
						<th scope="col">Token</th>
					</tr>
				</thead>
				<tbody>
					<tr align="center" valign="middle" key="onChainWithdrawal">
						<td className="text-break" title="Your account">
							{userAcct}
						</td>
						<td title={burnTotal + " wei"}>{burnTot.toString()}</td>
						<td title={burnAsset}>{payeeConfig.symbol}</td>
					</tr>
				</tbody>
			</Table>
			<br/><br/>
		</>;
	}
	
	// actual rendering
	return (
		<>
		{ /* input eNFT table */ }
		<Table bordered responsive hover variant="dark">
			<caption className="caption-top">
				Input eNFTs You Are Burning:
			</caption>
			<thead>
				<tr align="center" valign="middle" key="burnedEnftHdr">
					<th scope="col"
						title="These eNFTs will be destroyed in the operation">
						ID of Used eNFT
					</th>
					<th scope="col"
						title="Shown in ethers units for readability">
						Amount (1e-18)
					</th>
					<th scope="col">Token</th>
					<th scope="col">Details Hash</th>
				</tr>
			</thead>
			<tbody>
				{burntEnfts.map((inpENFT) =>
					<tr align="center" valign="middle" key={inpENFT.id}>
						<td className="text-break">{inpENFT.id}</td>
						<td>
							{web3.utils.fromWei(inpENFT.amount.toString())}
						</td>
						<td title={burnAsset}>{payeeConfig.symbol}</td>
						<td className="text-break">{inpENFT.detHash}</td>
					</tr>
				)}
			</tbody>
		</Table>
		<br/><br/>

		{onChainSection}

		<br/><br/>

		{outputTable}

		{ /* provide loading button to invoke smart contract, greyed on err */}
		<MakeBurnButton
			burnMethod={(resolve, reject, opsInfo, mvoInfo, inputAmounts, inputRands, burnAsset) => doEnftBurn(resolve, reject, opsInfo, mvoInfo, inputAmounts, inputRands, burnAsset)}
			opsInfo={OpsInfo}
			mvoInfo={MVOinfo}
			redAmounts={inputAmounts}
			redRands={inputRands}
			errCase={badInput}
			tokenContract={burnAsset}
		/>
		<br/>
		{burnResult === '' ? errDescrip : burnResult}
		<br/>
		</>
	);
}

/* render a received OperationsBlock's data and prompt for blockchain submission
 * @param props.opsBlock the JSON object containing the MVO's OB (always)
 * @param props.active true if we should render something, else tacit (always)
 * @param props.opcode the opcode context in which the OB was received (always)
 * @param props.payeeConfigs the payee configs for all outputs, possibly inputs
 * (always)
 * @param props.resetConfigs method to reset/clear used PayeeConfigS (always)
 * @param props.deposits held balances being minted against (deposit opcode)
 * @param props.onNewDeposit method to update a deposit we minted from (deposit)
 * @param props.eNFTList all downloaded eNFTs (spend and withdraw opcodes)
 */
function SmartContractSubmitter(props) {
	const { state: { chainConn } } = useEth();

	// content to use when we're not supposed to show anything real
	const noData
		= `No preprocessed ${props.opcode} data from an MVO is available.`;
	const defaultContent = 
		<>
			<br/>
			<Form.Control as="textarea" id="SCsubmit" name="SCsubmit" readOnly
				rows={3} cols={80} defaultValue={noData}
			/>
			<br/>
		</>;

	// see if there's anything to display
	var realDisplay = props.active;
	if (props.opsBlock === null) {
		realDisplay = false;
	}

	// also check for a completed opsBlock, which we do not want to repeat
	if (props.opsBlock !== null && props.opsBlock.done === true) {
		realDisplay = false;
	}

	if (realDisplay) {
		// validate opcode
		if (props.opsBlock.MVOSigned.opcode !== props.opcode) {
			console.error("unexpected opcode, " + props.opcode);
			realDisplay = false;
		}
	}
	var realContent;
	if (realDisplay) {
		if (props.opcode === 'deposit') {
			// amount (total, in hex)
			// asset (contract address, checksum format)
			// for each output eNFT:
			// 	1. address (checksum format)
			// 	2. ID (zero-padded hex, no prefix)
			// 	3. details hash
			// 	(details hash is now computable)
			// 	4. metadata (encrypted base64)
			realContent =
				<>
					<ShowMintTotal opsBlock={props.opsBlock} />

					<MintedENFTtable
						opsBlock={props.opsBlock}
						deposits={props.deposits}
						onNewDeposit={(dep) => props.onNewDeposit(dep)}
						payeeConfigs={props.payeeConfigs}
						resetConfigs={(config) => props.resetConfigs(config)}
					/>
				</>;
		} else if (props.opcode === 'spend') {
			// for each input eNFT:
			// 	1. ID (zero-padded hex, no prefix)
			// 	2. details hash
			// for each output eNFT:
			// 	1. address (checksum format)
			// 	2. ID (zero-padded hex, no prefix)
			// 	3. details hash
			// 	(details hash is now computable)
			// 	4. metadata (encrypted base64)
			realContent =
				<SpendENFTtables
					opsBlock={props.opsBlock}
					eNFTList={props.eNFTList}
					payeeConfigs={props.payeeConfigs}
					resetConfigs={(configs) => props.resetConfigs(configs)}
				/>;
		} else if (props.opcode === 'withdraw') {
			// withdrawer (address, checksum format)
			// asset (contract address, checksum format)
			// amount (total, in hex)
			// for each input eNFT:
			// 	1. ID (zero-padded hex, no prefix)
			// 	2. asset (contract address, checksum format)
			// 	3. amount (decimal string)
			// 	4. rand salt (base64, no prefix)
			// 	(details hash is now computable)
			// for the output (change) eNFT (if one exists)
			// 	1. output address (must match withdrawer)
			// 	2. ID (zero-padded hex, no prefix)
			//	3. amount (decimal string)
			//	4. rand salt (base64, no prefix)
			// 	(details hash is now computable)
			//	5. metadata (encrtyped base64)
			realContent =
				<BurnENFTtable
					opsBlock={props.opsBlock}
					eNFTList={props.eNFTList}
					payeeConfigs={props.payeeConfigs}
					resetConfigs={(config) => props.resetConfigs(config)}
				/>;
		}
	}

	// do actual rendering
	return (
		<div id="boilerplate">
		{
			realDisplay ? 
				// header content displayed regardless of opcode context
			<>
				<br/>
				<Card>
					<Card.Body>
						<Card.Title>Blockchain Submission Preview</Card.Title>
						<Card.Text>
							The Enshroud Layer 2 servers have returned the
							following transaction data generated from your
							request.  Please review it carefully against what
							you entered above.
						</Card.Text>
						<Card.Text>
							If everything looks correct, use
							the <b>Submit</b> button below to transmit your
							request to the <i>EnshroudProtocol</i> smart
							contract.  This will mine your transaction on
							the {chainConn.chainConfig.chain} blockchain.
						</Card.Text>
						<Card.Text>
							N.B.: The Details Hash for an eNFT is formed thus:
							<br/>
							<code>
							  keccak256(abi.encode(owner,ID,asset,amount,rand))
							</code>
						</Card.Text>
						<Card.Text>
							If the hash sent by the MVO matches this value,
							it indicates that the encrypted details in your
							eNFT match the values shown.  This details hash will
							be recorded in the smart contract as part of eNFT
							minting, which ensures that your eNFT can be spent
							or redeemed.  (N.B.: not relevant for burn/withdraw
							context, as we do not need to verify the MVO's
							details hash value.)
						</Card.Text>
					</Card.Body>
				</Card>
				<br/>
				{realContent}
			</>
			: defaultContent
		}
		</div>
	);
}

export default SmartContractSubmitter;
