/*
 * last modified---
 * 	04-14-25 when 0 eNFTs or receipts are found, say so in <MVOerrText/>
 * 	03-13-25 add parseEnftBackup(), pass to RestoreWallet
 * 	03-06-25 add DownloadWallet and RestoreWallet sections
 * 	02-25-25 pass chainConfig to EnshNavbar
 * 	07-12-24 add hooks for Staking pages
 * 	01-29-24 add link to Explainer page
 *  12-29-23 add Greylist Admin to admin menu (greylistAdmin key)
 * 	10-17-23 replace eNFTlist Map with eNFTList[]; remove state.depBalances and
 * 			 state.payeeConfigs and associated methods, plus all passing to subs
 * 	10-05-23 move SmartContractSubmitter usage into sub-pages
 * 	09-14-23 make depBalances, payeeConfigs Arrays instead of Maps
 * 	08-31-23 add BurnENFTs section
 * 	08-15-23 implement handleOBReply() and associated changes
 * 	08-09-23 add depBalances mapping; pass it to Home, Deposits, Spend, Mint
 * 	08-03-23 remove sendWalletReqToMVO(), was replaced by EIP-712 version
 * 	07-24-23 add DepositAssets section
 * 	07-20-23 add AssetPage section
 * 	07-18-23 use ChainConnection from EthProvider state
 * 	07-10-23 add admin/EnshroudMinting page
 * 	06-02-23 add validateMVOsig() to validate MVO signatures on replies
 * 	05-30-23 use MVOs methods; pass chainConfig as props.chainConnection
 * 	05-24-23 move Enft, SpendPayee, PayeeConfig to Enft.js
 *
 * purpose---
 * 	top level Component for Enshroud dApp
 */

import React from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import './Enshroud.css';
import { EthProvider } from './EthContext';
import Container from 'react-bootstrap/Container';
import Form from 'react-bootstrap/Form';
import EnshFooter from './footer.jsx';
import Header from './header.jsx';
import EnshNavbar from './navbar.jsx';
import MVOComm from './MVOComm.js';
import HomePage from './homePage.jsx';
import SpendENFTs from './spendENFTs.jsx';
import AssetPage from './assetConfig.jsx';
import DepositAssets from './depositAssets.jsx';
import TransactionHistory, { Receipt } from './receipts.jsx';
import MintENFTs from './mintENFTs.jsx';
import BurnENFTs from './burnENFTs.jsx';
import DownloadWallet from './downloadWallet.jsx';
import RestoreWallet from './restoreWallet.jsx';
import ChainConnection from './ChainConnection.js';
import Enft from './Enft.js';
import Explainer from './explainer.jsx';
import Web3 from 'web3';
import MVOConfig from './admin/MVOConfig.jsx';
import EnshroudMinting from './admin/EnshroudMinting.jsx';
import EnshroudGreylisting from './admin/EnshroudGreylisting.jsx';
import EnshroudCrowdsale from './staking/EnshroudCrowdsale.jsx';
import UserStaking from './staking/UserStaking.jsx';
import MVOStakingUX from './staking/MVOStakingUX.jsx';


// main Component for App display and state
class Enshroud extends React.Component {
	constructor(props) {
		super(props);
		// prep to create a MVO reply parse callback with 'this' bound to it
		const unboundReplyCallback = this.parseMVOreply;
		// prep to create a eNFT restore parse callback with 'this' bound to it
		const unboundRestoreCallback = this.parseEnftBackup;
		this.state = {
			// current page display state (internal flag)
			dispPage: 'home',

			// eNFTs belonging to the account (after fetch/decrypt)
			eNFTList: [],

			// listed, fetched/decrypted, or deleted receipts received from MVO
			receiptList: [],

			// current chain config (init to dummy value)
			chainConfig: new ChainConnection(),

			// current account address (tracked so we know when it changes)
			userAccount: '',

			// latest MVO communication object (an MVOComm)
			MVOinterface: null,

			// latest raw reply from an MVO (an XMLHttpRequest object)
			MVOreply: null,

			// latest successful reply from an MVO (a JSON object)
			MVOreplyJSON: null,

			// latest error return code from an MVO (int)
			MVOerrCode: 200,

			// latest error message text from an MVO
			MVOerrText: '',

			// flag to activate <SmartContractSubmitter /> (uses MVOreplyJSON)
			SCsubmitActivated: false,

			/* callback to interpret MVO replies.  This is declared here in
			 * order to limit the number of listeners generated.
			 */
			boundReplyCallback: unboundReplyCallback.bind(this),

			// callback to interpret uploaded eNFT wallet backups
			boundRestoreCallback: unboundRestoreCallback.bind(this),
		};
	}

	// shorthand getters
	get eNFTs() { return this.state.eNFTList; }
	get receipts() { return this.state.receiptList; }
	get chConfig() { return this.state.chainConfig; }

	/* change the chain connection (called when EthProvider sees a chain or
	 * acct switch, via the "props.updateChain(chainConn)" call in EnshNavbar)
	 * @param chainConfig the new chain connection
	 * @param userAcct the account address selected (web3 accounts[0])
	 */
	updateChainConfig(chainConfig, userAcct) {
		var chainChanged = false;
		var acctChanged = false;
		if (chainConfig != null && chainConfig !== this.state.chainConfig) {
			chainChanged = true;
			/* We need access to the current ChainConfig for use in
			 * validateMVOsig() below.  To avoid specious React warnings about
			 * updating state in a render() context, we use Object.assign().
			 */
			//this.setState({chainConfig: chainConfig};
			Object.assign(this.state.chainConfig, chainConfig);
		}

		if (userAcct !== '' && userAcct !== this.state.userAccount) {
			acctChanged = true;
			/* this won't loop rendering, but does cause an error for updating
			 * state during render because it's triggered by a listener in
			 * the EthProvider:
			 */
			//this.setState({userAccount: userAcct});
			// eslint-disable-next-line react/no-direct-mutation-state
			this.state.userAccount = userAcct;
		}
		// Metamask (or other wallet app) has changed the chain or account
		if (chainChanged || acctChanged) {
			// to avoid holdover data from previous chain or account, reset:
			// [these will trigger a render loop if we use setState()]
			// eslint-disable-next-line react/no-direct-mutation-state
			this.state.eNFTList = [];
			// eslint-disable-next-line react/no-direct-mutation-state
			this.state.receiptList = [];
			// eslint-disable-next-line react/no-direct-mutation-state
			this.state.MVOreplyJSON = null;
			// eslint-disable-next-line react/no-direct-mutation-state
			this.state.SCsubmitActivated = false;
		}
	}

	/* parse the MVO reply value
	 * @param reply the MVO's returned value (an XMLHttpRequest)
	 * @param comm the communications object (an MVOComm)
	 */
	parseMVOreply(reply, comm) {
		if (!(reply instanceof XMLHttpRequest)) {
			console.error("parseMVOreply(): bad input reply, got a "
						+ typeof(reply));
			return;
		}
		this.setState({MVOreply: reply});
		this.setState({MVOerrCode: reply.status});
		if (comm instanceof MVOComm) {
			this.setState({MVOinterface: comm});
		}
		else {
			console.error("parseMVOreply(): bad input MVOComm");
			return;
		}

		// check for an error return
		if (reply.status !== 200) {
			var dparser = new DOMParser();
			var errdoc = dparser.parseFromString(reply.response, "text/html");
			if (errdoc == null) {
				console.error("DOMParser failed, capturing full error text");
				this.setState({MVOerrText: reply.response});
			}
			else {
				// MVO passes everything we need in the title
				const errMsg = comm.mvo + " replies:\n("
								+ reply.statusText + "): " + errdoc.title;
				this.setState({MVOerrText: errMsg});
			}
			this.setState({MVOreplyJSON: null});
			alert('Layer2 returned error response; see bottom of page');
		}
		else {
			// normal reply, attempt to parse text as JSON
			try {
				// if connection is encrypted, use comm object's decrypted reply
				var jsonObj = '';
				if (comm.encrypted) {
					jsonObj = JSON.parse(comm.reply);
				}
				else {
					// use unencrypted reply found in XMLHttpRequest
					jsonObj = JSON.parse(reply.response);
				}
				this.setState({MVOreplyJSON: jsonObj});
				this.setState({MVOerrText: ""});

				// pass to correct sub-handler based on purpose of connection
				switch (comm.operation) {
					case 'wallet':
						this.handleWalletReply(jsonObj, comm);
						break;
					case 'receipts':
						this.handleReceiptReply(jsonObj, comm);
						break;
					case 'deposit':
					case 'withdraw':
					case 'spend':
						this.handleOBReply(jsonObj, comm);
						break;
					default:
						console.error("Unsupported reply operation, "
									+ comm.operation);
						break;
				}
			}
			catch (e) {
				// e.name should be SyntaxError
				console.error(e.name);
				console.error(e.message);
				console.error(e.lineNumber);
				console.error(e.columnNumber);
				console.error(e.stack);
				const errMsg = comm.mvo + " reply did not parse:\n"
								+ e.name + ": " + e.message;
				this.setState({MVOerrText: errMsg});
				alert('Parse error on Layer2 response; see bottom of page');
			}
		}
	}

	/* process a JSON object giving us a wallet download response from an MVO
	 * @param jsonObj the decrypted and parsed JSON describing downloaded eNFTs
	 * @param mvoComm the object which talks to the MVO
	 */
	handleWalletReply(jsonObj, mvoComm) {
		if (jsonObj === null || jsonObj === undefined
			|| !(mvoComm instanceof MVOComm))
		{
			console.error("handleWalletReply: missing input");
			return;
		}
		const rawReply = JSON.stringify(jsonObj);
		//console.log("handleWalletReply sees: " + rawReply);

		// ensure that the MVO returned success
		const statRet = jsonObj.status;
		if (statRet !== "success") {
			const errMsg = mvoComm.mvo + " returned error:\n" + statRet;
			this.setState({MVOerrText: errMsg});
			return;
		}

		// get the sig spec for this MVO
		const signerMVO = jsonObj.signature.signer;
		const mvoSig = '0x' + jsonObj.signature.sig;
		
		// signed data consists of everything from '"status":' to ']'
		let endList = rawReply.lastIndexOf(']');
		if (endList !== -1) {
			const signedData = rawReply.substring(1, endList+1);
			if (!this.validateMVOsig(signedData, signerMVO, mvoSig, true)) {
				const errMsg = mvoComm.mvo + " reply failed signature check";
				console.error(errMsg);
				this.setState({MVOerrText: errMsg});
				return;
			}
		}
		
		// get the list of eNFTs returned, along with their AES keys
		const eNFTlist = jsonObj.eNFTs;
		if (!(eNFTlist instanceof Array)) {
			console.error("eNFTlist is not an array");
			this.setState({MVOerrText: "no eNFT list received"});
			return;
		}

		// add all eNFTs to the list
		const nf = new Intl.NumberFormat("en-US", {minimumIntegerDigits: 3});
		const parsedENFTs = [];
		eNFTlist.forEach((eNFT, index) => {
			let enftIdx = "eNFT" + nf.format(index+1);
			let enftData = eNFT[enftIdx];
			let enftEnshData = enftData.enshrouded;
			const enftObj = new Enft();
			// config standard fields
			enftObj.config(enftEnshData.id, enftEnshData.schema,
						   enftEnshData.owner, enftEnshData.asset,
						   enftEnshData.amount, enftEnshData.generation,
						   enftEnshData.rand,
						   enftEnshData.signer, enftEnshData.signature,
						   enftData.key);
			// add optional fields
			if (enftEnshData.expiration !== undefined) {
				enftObj.addExpiration(enftEnshData.expiration);
			}
			if (enftEnshData.growth !== undefined) {
				enftObj.addGrowth(enftEnshData.growth);
			}
			if (enftEnshData.cost !== undefined) {
				enftObj.addCost(enftEnshData.cost);
			}
			if (enftEnshData.memo !== undefined) {
				enftObj.addMemo(enftEnshData.memo);
			}
			parsedENFTs.push(enftObj);
		});
		this.setState({eNFTList: parsedENFTs});

		// deactivate the <SmartContractSubmitter> object's display
		this.setState({SCsubmitActivated: false});

		// if no eNFTs were seen, note this in the L2 error area
		if (parsedENFTs.length === 0) {
			this.setState({MVOerrText: "No eNFTs were found for your address"});
		}
	}

	/* process a JSON object giving us a receipt operation response from an MVO
	 * @param jsonObj the decrypted/parsed JSON describing downloaded receipts
	 * @param mvoComm the object which talks to the MVO
	 */
	handleReceiptReply(jsonObj, mvoComm) {
		if (jsonObj === null || jsonObj === undefined
			|| !(mvoComm instanceof MVOComm))
		{
			console.error("handleReceiptReply: missing input");
			return;
		}
		const rawReply = JSON.stringify(jsonObj);
		//console.log("handleReceiptReply sees: " + rawReply);

		// ensure that the MVO returned success
		const statRet = jsonObj.status;
		if (statRet !== "success") {
			const errMsg = mvoComm.mvo + " returned error:\n" + statRet;
			this.setState({MVOerrText: errMsg});
			return;
		}

		// get the sig spec for this MVO
		const signerMVO = jsonObj.signature.signer;
		const mvoSig = '0x' + jsonObj.signature.sig;
		
		// signed data consists of everything from '"opcode":' to ']'
		let endList = rawReply.lastIndexOf(']');
		if (endList !== -1) {
			const signedData = rawReply.substring(1, endList+1);
			if (!this.validateMVOsig(signedData, signerMVO, mvoSig, true)) {
				const errMsg = mvoComm.mvo + " reply failed signature check";
				console.error(errMsg);
				this.setState({MVOerrText: errMsg});
				return;
			}
		}

		// examine the opcode to determine the expected format of the reply
		const nf = new Intl.NumberFormat("en-US", {minimumIntegerDigits: 3});
		const opcode = jsonObj.opcode;
		switch (opcode) {
			case 'list':
				// get the list of receipts returned
				const fileList = jsonObj.filespecs;
				if (!(fileList instanceof Array)) {
					console.error("Receipt fileList is not an array");
					this.setState({MVOerrText: "no receipt list received"});
					return;
				}

				// add all listed receipts to the list
				const receiptArray = [];
				fileList.forEach(receiptFile => {
					const jsonSuffix = receiptFile.indexOf('.json');
					const receiptId = receiptFile.substring(0, jsonSuffix);
					const receiptObj = new Receipt();
					// this is all we can do here, until receipt is decrypted:
					receiptObj.init(receiptId,
									receiptFile,
									this.chConfig.chainConfig.chainId);
					receiptArray.push(receiptObj);
				});

				/* merge the listed receipts into the global list, keeping any
				 * copies which are decrypted rather than replacing them
				 */
				const fullReceiptList = [];
				receiptArray.forEach(receipt => {
					// loop through stored receipts looking for match
					let found = false;
					for (const sReceipt of this.receipts) {
						if (sReceipt.id === receipt.id) {
							if (sReceipt.decrypted) {
								// retain the decrypted one
								fullReceiptList.push(sReceipt);
								found = true;
							}
							break;
						}
					}
					if (!found) {
						// use the new unencrypted receipt
						fullReceiptList.push(receipt);
					}
				});
				this.setState({receiptList: fullReceiptList});

				// if no receipts were seen, note this in the L2 error area
				if (receiptArray.length === 0) {
					this.setState({MVOerrText: "No receipts were found for your address"});
				}
				break;

			case 'get':
				// here we expect an array of maps {seq : {filename=,{receipt}}
				const rctList = jsonObj.receipts;
				if (!(rctList instanceof Array)) {
					console.error("Receipt list is not an array");
					this.setState({MVOerrText: "no receipt list received"});
					return;
				}

				// move through the list, creating a decrypted Receipt for each
				const decryptedReceipts = [];
				let rctIdx = 1;
				let currChId = this.chConfig.chainConfig.chainId.toString();
				for (const receipt of rctList) {
					const sequence = "receipt" + nf.format(rctIdx++);
					const rctSpec = receipt[sequence];
					if (rctSpec === undefined) {
						console.error("No receipt spec found for seq "
									+ sequence);
						continue;
					}
					/* this item should be a map with two pieces, the filename
					 * (receiptId.json) and the spec (map) of the receipt itself
					 */
					const fileName = rctSpec.filename;
					const rctBlk = rctSpec.receiptData;
					if (rctBlk === undefined) {
						console.error("No receiptData found for seq "
									+ sequence);
						continue;
					}

					/* this rctBlk mapping looks like this:
					 * {ReceiptBlock: { receiptId: '<ID>', receipt: {<map>}}},
					 * where "map" contains: source: '<addr>', chainId: N, etc.
					 */
					const receiptBlock = rctBlk.ReceiptBlock;
					//console.log(receiptBlock);
					const rctId = receiptBlock.receiptId;
					const rctObj = receiptBlock.receipt;
					if (rctObj.chainId !== currChId) {
						console.error("ReceiptId " + rctId + " is for chain "
									+ rctObj.chainId + ", while we're on "
									+ currChId);
						continue;
					}
					const receiptObj = new Receipt();
					const rctSig = rctObj.signature;
					if (rctSig === undefined) {
						console.error("ReceiptId " + rctId + " is unsigned!");
						continue;
					}
					receiptObj.init(rctId, fileName, rctObj.chainId);
					receiptObj.config(rctObj.receiptType,
									  rctObj.block,
									  rctObj.source,
									  rctSig.signer,
									  rctSig.sig);

					// append the receipt's payees
					const payees = rctObj.destinations;
					if (!(payees instanceof Array)) {
						console.error("Missing payees for receipt Id " + rctId);
						continue;
					}
					payees.forEach((payee, index) => {
						const payeeSeq = "payee" + nf.format(index+1);
						const payeeSpec = payee[payeeSeq];
						receiptObj.addPayee(payeeSeq,
											payeeSpec.address,
											payeeSpec.asset,
											payeeSpec.amount,
											payeeSpec.id,
											payeeSpec.memo);
					});
					decryptedReceipts.push(receiptObj);
				}

				/* Merge the decrypted receipts into the global list, some of
				 * which might still be encrypted (if not selected for get).
				 * To do this, we must pass the list to the receipts page,
				 * which will handle the merging.
				 */
				this.setState({receiptList: decryptedReceipts});

				break;

			case 'delete':
				// get the list of receipts returned
				const delList = jsonObj.filespecs;
				if (!(delList instanceof Array)) {
					console.error("Receipt delList is not an array");
					this.setState({MVOerrText: "no receipt delete list"});
					return;
				}
				if (delList.length === 0) {
					return;
				}

				// create dummy receipts having the indicated filename
				const purgedReceipts = [];
				delList.forEach(receiptfile => {
					const delReceipt = new Receipt();
					delReceipt.file = receiptfile;
					const jsonIdx = receiptfile.lastIndexOf('.json');
					delReceipt.id = receiptfile.substring(0, jsonIdx);
					purgedReceipts.push(delReceipt);
				});

				// pass through to receipts page, which will do list removal
				this.setState({receiptList: purgedReceipts});
				break;

			default:
				const errMsg = mvoComm.mvo + " returned unknown receipt "
								+ "opcode:\n" + opcode;
				console.error(errMsg);
				this.setState({MVOerrText: errMsg});
				return;
		}

		// deactivate the <SmartContractSubmitter> object's display
		this.setState({SCsubmitActivated: false});
	}

	/* process a JSON object giving us a OperationsBlock response from an MVO
	 * @param jsonObj the decrypted/parsed JSON describing an OperationsBlock
	 * @param mvoComm the object which talks to the MVO
	 */
	handleOBReply(jsonObj, mvoComm) {
		if (jsonObj === null || jsonObj === undefined
			|| !(mvoComm instanceof MVOComm))
		{
			console.error("handleOBReply: missing input");
			return;
		}
	/*
		const rawReply = JSON.stringify(jsonObj);
		console.log("handleOBReply sees: " + rawReply);
	 */

		// signed data consists of the MVOSigned element
		const signedElt = jsonObj.MVOSigned;
		if (signedElt === undefined) {
			const errMsg = mvoComm.mvo + " reply did not contain signed data";
			console.error(errMsg);
			this.setState({MVOerrText: errMsg});
			return;
		}
		const signedData = JSON.stringify(signedElt);

		// get the sig spec for the MVOs
		const allSigs = jsonObj.signatures;
		if (allSigs === undefined || allSigs.length === 0) {
			const errMsg = mvoComm.mvo + " reply was not signed by any MVOs";
			console.error(errMsg);
			this.setState({MVOerrText: errMsg});
			return;
		}

		// loop through all signatories
		const nf = new Intl.NumberFormat("en-US", {minimumIntegerDigits: 3});
		let signer = 1;
		allSigs.forEach(sigSpec => {
			// fetch "001", "002" map entry from array element
			const signerIdx = nf.format(signer++);
			const MVOsigBlock = sigSpec[signerIdx];
			const signerMVO = MVOsigBlock.signer;
			const mvoSig = '0x' + MVOsigBlock.sig;
			
			// validate signature on entire OperationsBlock
			if (!this.validateMVOsig(signedData, signerMVO, mvoSig, true)) {
				const errMsg = mvoComm.mvo + " reply failed signature check on "
								+ "sig by MVO " + signerMVO;
				console.error(errMsg);
				this.setState({MVOerrText: errMsg});
				return;
			}

			// validate signature on arguments hash
			const argsHashSig = '0x' + MVOsigBlock.argsSig;
			const argsHash = '0x' + signedElt.argsHash;
			if (!this.validateMVOsig(argsHash, signerMVO, argsHashSig, false)) {
				const errMsg = mvoComm.mvo + " argsHash failed signature check "
								+ "on sig by MVO " + signerMVO;
				console.error(errMsg);
				this.setState({MVOerrText: errMsg});
				return;
			}
		});

		// validate reply opcode
		const opcode = signedElt.opcode;
		if (opcode === undefined || !(opcode === 'deposit'
										|| opcode === 'spend'
										|| opcode === 'withdraw'))
		{
			const errMsg
				= mvoComm.mvo + " replied with improper opcode, " + opcode;
			console.error(errMsg);
			this.setState({MVOerrText: errMsg});
			return;
		}

		// activate the <SmartContractSubmitter> object's display
		this.setState({SCsubmitActivated: true});
	}

	/* process a JSON string giving us a list of eNFTs uploaded from a wallet
	 * backup file
	 * @param jsonObj the JSON object, an array of supposed eNFTs
	 */
	parseEnftBackup(jsonObj) {
		// get the list of eNFTs returned, along with their AES keys
		const eNFTlist = jsonObj;
		if (!(eNFTlist instanceof Array)) {
			// NB: caller also checked for this
			console.error("eNFTlist is not an array");
			return;
		}

		// add all eNFTs to the list
		const parsedENFTs = [];
		let eCnt = 1;
		for (const eNFT of eNFTlist) {
			const enftData = eNFT;
			// look for some common required fields
			if (enftData.id === undefined || enftData.schema === undefined
				|| enftData.signature === undefined)
			{
				alert("Item " + eCnt + " in wallet backup is not a valid eNFT");
				break;
			}
			const enftObj = new Enft();
			// config standard fields
			enftObj.config(enftData.id, enftData.schema, enftData.owner,
						   enftData.asset, enftData.amount, enftData.generation,
						   enftData.rand, enftData.signer, enftData.signature,
						   enftData.key);
			// add optional fields
			if (enftData.expiration !== undefined) {
				enftObj.addExpiration(enftData.expiration);
			}
			if (enftData.growth !== undefined) {
				enftObj.addGrowth(enftData.growth);
			}
			if (enftData.cost !== undefined) {
				enftObj.addCost(enftData.cost);
			}
			if (enftData.memo !== undefined) {
				enftObj.addMemo(enftData.memo);
			}
			parsedENFTs.push(enftObj);
			eCnt++;
		}
		this.setState({eNFTList: parsedENFTs});
	}

	/* utility method to validate the MVO's signature on a reply (any type)
	 * @param signedData the text of the reply data which the MVO signed
	 * @param signingMvo the MVO's Id
	 * @param mvoSig the signature data
	 * @param doHash true if we need to hash the signed data first, false if
	 * the signed data is already a hash
	 */
	validateMVOsig(signedData, signingMvo, mvoSig, doHash = true) {
		// get the signing address for this MVO
		const mvoStaking = this.chConfig.MVOConf.getMVOStaking(signingMvo);
		if (mvoStaking === undefined) {
			console.error("Could not find staking for mvoId " + signingMvo);
			this.setState({MVOerrText:
						"MVO " + signingMvo + " signing address not found"});
			return false;
		}
		const signingAddress = mvoStaking.signingAddress;

		// validate the MVO's signature on the data
		const web3 = new Web3(Web3.givenProvider || "ws://localhost:8545");
		var dataHash = signedData;
		if (dataHash === undefined) {
			console.error("validateMVOsig() passed no data for sig check");
			return false;
		}
		if (doHash) {
			// replace with hash
			dataHash = web3.utils.keccak256(signedData);
		}
		const acct = web3.eth.accounts.recover(dataHash, mvoSig, true);
		if (acct !== signingAddress) {
			const sigerr = "Signature on MVO reply did not verify";
			console.error("MVO sig did not verify, acct = " + acct);
			this.setState({MVOerrText: sigerr});
			return false;
		}
		return true;
	}

	/* update the displayed page value
	 * @param page the name of the page we're to switch to (re-render)
	 */
	updateDisplayPage(page) {
		if (page !== null) {
			// this is a display state
			this.setState({dispPage: page});
		}
	}

	// main display method (required)
	render() {
		var pageName = '';
		switch (this.state.dispPage) {
			case 'home':
				pageName = 'Home';
				break;
			case 'downloadWallet':
				pageName = 'Download Wallet';
				break;
			case 'depositAssets':
				pageName = 'Deposit Assets';
				break;
			case 'assetConfig':
				pageName = 'Asset Config';
				break;
			case 'mintENFTs':
				pageName = 'Mint eNFTs';
				break;
			case 'spendENFTs':
				pageName = 'Spend eNFTs';
				break;
			case 'burnENFTs':
				pageName = 'Burn eNFTs';
				break;
			case 'restoreWallet':
				pageName = 'Restore Wallet';
				break;
			case 'receipts':
				pageName = 'Transaction History';
				break;
			case 'explainer':
				pageName = 'Enshroud Explainer';
				break;

			// staking pages
			case 'daoStaking':
				pageName = 'DAO User Staking';
				break;
			case 'mvoStaking':
				pageName = 'Managing Your MVO Staking';
				break;
			case 'crowdSale':
				pageName = 'Purchase $ENSHROUD Tokens';
				break;

			// admin pages
			case 'mvoConfig':
				pageName = 'MVO Configuration';
				break;
			case 'enshMinting':
				pageName = 'ENSHROUD Minting';
				break;
			case 'greylistAdmin':
				pageName = 'Greylist Admin';
				break;

			default:
				console.log("Unsupported dispPage, " + this.state.dispPage);
		}

		// render the individual sub-page
		return (
			// enclose in EthProvider for web3js support
			<EthProvider>
			<div className="App">
				<Header
					pageName={pageName}
				/>

				<EnshNavbar
					onSelect={(page) => this.updateDisplayPage(page)}
					activePage={this.state.dispPage}
					chainConfig={this.state.chainConfig}
					updateChain={(config) => this.updateChainConfig(config)}
				/>
				<br/><br/>

			{ /* main protocol pages */ }
			{this.state.dispPage === 'home' &&
				// render the HomePage
				<Container>
					<HomePage
						parseReplyMethod={(httpReq, mvoComm) => this.state.boundReplyCallback(httpReq, mvoComm)}
						onSelect={(page) => this.updateDisplayPage(page)}
						eNFTList={this.eNFTs}
						deposits={this.deposits}
					/>

					<MVOErrorDisplay
						value={this.state.MVOerrText}
					/>
				</Container>
			}
			{this.state.dispPage === 'receipts' &&
				// render the TransactionHistory page
				<Container>
					<TransactionHistory
						parseReplyMethod={(httpReq, mvoComm) => this.state.boundReplyCallback(httpReq, mvoComm)}
						onSelect={(page) => this.updateDisplayPage(page)}
						receiptList={this.receipts}
						lastReceiptReply={this.state.MVOreplyJSON}
					/>

					<MVOErrorDisplay
						value={this.state.MVOerrText}
					/>
				</Container>
			}
			{this.state.dispPage === 'depositAssets' &&
				// render the DepositAssets page
				<Container>
					<DepositAssets
						onSelect={(page) => this.updateDisplayPage(page)}
				/>
				</Container>
			}
			{this.state.dispPage === 'assetConfig' &&
				// render the AssetPage
				<Container>
					<AssetPage
						onSelect={(page) => this.updateDisplayPage(page)}
					/>
				</Container>
			}
			{this.state.dispPage === 'mintENFTs' &&
				// render the MintENFTs page
				<Container>
					<MintENFTs
						parseReplyMethod={(httpReq, mvoComm) => this.state.boundReplyCallback(httpReq, mvoComm)}
						onSelect={(page) => this.updateDisplayPage(page)}
						opsBlock={this.state.MVOreplyJSON}
						active={this.state.SCsubmitActivated}
					/>

					<MVOErrorDisplay
						value={this.state.MVOerrText}
					/>
				</Container>
			}
			{this.state.dispPage === 'spendENFTs' &&
				// render the SpendENFTs page
				<Container>
					<SpendENFTs
						parseReplyMethod={(httpReq, mvoComm) => this.state.boundReplyCallback(httpReq, mvoComm)}
						onSelect={(page) => this.updateDisplayPage(page)}
						eNFTList={this.eNFTs}
						opsBlock={this.state.MVOreplyJSON}
						active={this.state.SCsubmitActivated}
					/>

					<MVOErrorDisplay
						value={this.state.MVOerrText}
					/>
				</Container>
			}
			{this.state.dispPage === 'burnENFTs' &&
				// render the BurnENFTs page
				<Container>
					<BurnENFTs
						parseReplyMethod={(httpReq, mvoComm) => this.state.boundReplyCallback(httpReq, mvoComm)}
						eNFTList={this.eNFTs}
						opsBlock={this.state.MVOreplyJSON}
						active={this.state.SCsubmitActivated}
					/>

					<MVOErrorDisplay
						value={this.state.MVOerrText}
					/>
				</Container>
			}
			{this.state.dispPage === 'downloadWallet' &&
				// render the downloadWallet page
				<Container>
					<DownloadWallet
						parseReplyMethod={(httpReq, mvoComm) => this.state.boundReplyCallback(httpReq, mvoComm)}
						eNFTList={this.eNFTs}
					/>
				</Container>
			}

			{this.state.dispPage === 'restoreWallet' &&
				// render the restoreWallet page
				<Container>
					<RestoreWallet
						parseRestoreMethod={(jsonObj) => this.state.boundRestoreCallback(jsonObj)}
						onSelect={(page) => this.updateDisplayPage(page)}
					/>
				</Container>
			}

			{ /* explainer info page */ }
			{this.state.dispPage === 'explainer' &&
				// render the Explainer page
				<Container>
					<Explainer
						onSelect={(page) => this.updateDisplayPage(page)}
					/>
				</Container>
			}

			{ /* staking pages */ }
			{this.state.dispPage === 'daoStaking' &&
				// render the User Staking page
				<Container>
					<UserStaking />
				</Container>
			}
			{this.state.dispPage === 'mvoStaking' &&
				// render the MVO Staking page
				<Container>
					<MVOStakingUX />
				</Container>
			}
			{this.state.dispPage === 'crowdSale' &&
				// render the Purchase Tokens page
				<Container>
					<EnshroudCrowdsale />
				</Container>
			}

			{ /* admin pages */ }
			{this.state.dispPage === 'mvoConfig' &&
				// render the MVO Config page (admin)
				<Container>
					<MVOConfig />
				</Container>
			}
			{this.state.dispPage === 'enshMinting' &&
				// render the ENSHROUD minting page (admin)
				<Container>
					<EnshroudMinting />
				</Container>
			}
			{this.state.dispPage === 'greylistAdmin' &&
				// render the Greylisting page (admin)
				<Container>
					<EnshroudGreylisting />
				</Container>
			}
			{this.state.dispPage !== 'home'
				&& this.state.dispPage !== 'receipts'
				&& this.state.dispPage !== 'spendENFTs'
				&& this.state.dispPage !== 'assetConfig'
				&& this.state.dispPage !== 'depositAssets'
				&& this.state.dispPage !== 'mintENFTs'
				&& this.state.dispPage !== 'burnENFTs'
				&& this.state.dispPage !== 'daoStaking'
				&& this.state.dispPage !== 'mvoStaking'
				&& this.state.dispPage !== 'crowdSale'
				&& this.state.dispPage !== 'mvoConfig'
				&& this.state.dispPage !== 'enshMinting'
				&& this.state.dispPage !== 'greylistAdmin'
				&& this.state.dispPage !== 'explainer'
				&& this.state.dispPage !== 'downloadWallet'
				&& this.state.dispPage !== 'restoreWallet' &&

				// render a default page, configured only with labeling
				<Container>
					<h3>
						TBD: {pageName} Page (Not yet implemented)
					</h3>
					<br/><br/>
				</Container>
			}
				<hr/>
				<br/>
				<EnshFooter />
				<br/><br/>
			</div>
			</EthProvider>
		);
	}
}

// end of class =========================================

// object to display error messages received from an MVO
function MVOErrorDisplay(props) {
	return (
		<Container fluid>
			<p className="text-lead">
				<i>If Layer2 returns any errors, they will be shown here:</i>
			</p>
			<Form.Control as="textarea" name="MVOerrors" rows={3} cols={80}
				readOnly className="event-log" id="MVOerrs" value={props.value}
			/>
			<br/><br/>
		</Container>
	);
}

// exports defined herein
export default Enshroud;
export { MVOErrorDisplay };

