/*
 * last modified---
 * 	06-10-26 add DOM ids for automation support
 * 	02-27-25 implement display of downloaded receipts to permit copy/paste
 * 	01-18-24 indicate where source is own wallet; clear selections on changes;
 * 			 sort receipts ascending by block number
 * 	01-15-24 prevent roundoff display errors by using BigNumber
 * 	10-02-23 add local state storage for receipt list
 * 	09-28-23 properly pass idList to sendReceipt[Get|Del]ToMVO() as arrays
 * 	08-30-23 flesh out validateSig()
 * 	08-08-23 use LoadingButton for signed MVO queries
 * 	07-18-23 pull chainConn from useEth() state
 * 	06-08-23 add sendReceipt*ToMVO(), moved from Enshroud.jsx
 *
 * purpose---
 * 	retrieve, store and display receipt records
 */

import React, { useState } from 'react';
import Container from 'react-bootstrap/Container';
import Table from 'react-bootstrap/Table';
import Button from 'react-bootstrap/Button';
import Badge from 'react-bootstrap/Badge';
import Image from 'react-bootstrap/Image';
import Form from 'react-bootstrap/Form';
import useEth from './EthContext/useEth';
import MVOComm from './MVOComm.js';
import LoadingButton from './LoadingButton.jsx';
import { md } from 'node-forge';
import Web3 from 'web3';
import DataToast from './DataToast.jsx';
const BigNumber = require("bignumber.js");


// class to represent a single Receipt
class Receipt {
	constructor() {
		this.id = '';
		this.file = '';
		this.type = '';
		this.decrypted = false;
		this.chain = '';
		this.block = '';
		this.source = '';
		this.payees = [];
		this.signer = '';
		this.signature = '';
		this.valid = false;
	}

	// constants for receipt types
	static M_Recipient = "recipient";
	static M_Sender = "sender";

	/* set id, filename, and chainId (known prior to decryption)
	 * @param id the unique ID of the receipt
	 * @param file the filename of the receipt (ID.json)
	 * @param chain the chain ID the receipt is for
	 */
	init(id, file, chain) {
		this.id = id;
		this.file = file;
		this.chain = chain;
		this.decrypted = false;
	}

	/* configure other mandatory fields (following decryption)
	 * @param type M_Recipient or M_Sender
	 * @param block the block number in which the transaction occurred
	 * @param source where the value came from (msg.sender for transaction)
	 * @param signer the ID of the MVO which generated the receipt
	 * @param sig the MVO's signature
	 */
	config(type, block, source, signer, sig) {
		this.type = type;
		this.block = block;
		this.source = source;
		this.signer = signer;
		this.signature = sig;
		this.decrypted = true;
	}

	/* add a payee to this receipt
	 * @param seq the payee label (payee001, payee002, etc.)
	 * @param addr the address that owns the receipt record
	 * @param asset the token contract involved
	 * @param amt the amount (in wei)
	 * @param id the unique ID of the eNFT issued to the recipient
	 * @param memo optional message string supplied by payer
	 */
	addPayee(seq, addr, asset, amt, id, memo) {
		const payee = {sequence: seq, address: addr, asset: asset, amount: amt};
		// NB: for Sender receipts, the specific eNFT issued is not returned
		if (this.type === Receipt.M_Recipient) {
			payee.eNFTid = id;
		}
		if (memo !== undefined && memo !== "") {
			payee.memo = memo;
		} else {
			payee.memo = '';
		}
		this.payees.push(payee);
	}

	// null out payees list
	clearPayees() {
		this.payees = [];
	}

	/* validate signature (bool)
	 * @param chainConn the chain's configuration (a ChainConnection)
	 * @return true iff valid
	 */
	validateSig(chainConn) {
		// must be decrypted first
		if (!this.decrypted) {
			console.error("Receipt cannot be validated before decryption");
			return false;
		}
		const len = this.payees.length;
		if (len === 0) {
			// at least 1 payee is required
			console.error("Receipt needs at least one payee record");
			return false;
		}
		if (chainConn == null) {
			console.error("validateSig() on Id " + this.id + " without a "
						+ "ChainConnection");
			return false;
		}

		// must be emitted in same order as MVO's ReceiptBlock.buildSignedData()
		let sigData = `{"source":"${this.source}",`;
		sigData += `"receiptType":"${this.type}",`;
		sigData += `"chainId":"${this.chain}",`;
		sigData += `"block":"${this.block}",`;
		sigData += `"destinations":[`;
		let outSeq = 1;
		const nf = new Intl.NumberFormat("en-US", {minimumIntegerDigits: 3});
		this.payees.forEach((payee) => {
			if (payee.sequence === undefined || payee.sequence === '') {
				payee.sequence = nf.format(outSeq);
			}
			sigData += `{"${payee.sequence}":{`;
			sigData += `"address":"${payee.address}",`;
			sigData += `"asset":"${payee.asset}",`;
			sigData += `"amount":"${payee.amount}"`;
			if (this.type === Receipt.M_Recipient) {
				sigData += `,"id":"${payee.eNFTid}"`;
			}
			if (payee.memo !== undefined && payee.memo !== '') {
				sigData += `,"memo":"${payee.memo}"`;
			}
			// end the payee json with a comma unless it's the last one
			if (outSeq++ < len) {
				sigData += "}},";
			}
			else {
				sigData += "}}]";
			}
		});
		sigData += "}";
		if (sigData === "") {
			return false;
		}

		// obtain signature address of this.signer
		const mvoConf = chainConn.MVOConf;
		const mvoStaking = mvoConf.getMVOStaking(this.signer);
		if (mvoStaking === undefined) {
			console.error("validateSig() on Id " + this.id + " but no MVO "
						+ "staking available for signer " + this.signer);
			return false;
		}
		const signingAddr = mvoStaking.signingAddress;

		// confirm sig was made by signer on Ethereum-prefixed hash
		const web3 = new Web3(Web3.givenProvider || "ws://localhost:8545");
		const dataHash = web3.eth.accounts.hashMessage(sigData);
		const sig = "0x" + this.signature;
		const acct = web3.eth.accounts.recover(dataHash, sig, true);
		this.valid = acct === signingAddr;
		return this.valid;
	}
}

/* method to render a payee for a sender receipt
 * @param props.receipt the receipt record (a Receipt)
 * @param props.payee the payee specification
 */
function PayeeRenderer(props) {
	const { state: { accounts } } = useEth();

	// shorthands
	const dispAmt = BigNumber(props.payee.amount).dividedBy(1e18);
	const truncAddr = props.payee.address.substring(0,5) + '...'
					+ props.payee.address.substring(38);
	let payeeAddr = props.payee.address;
	if (payeeAddr === accounts[0]) {
		payeeAddr += " (Self)";
	}
	const detailsTitle=`Details for payee ${props.payee.sequence}`;
	const payeeNo = props.payee.sequence.substring(5);
	const end = Math.min(props.payee.memo.length, 16);
	let truncMemo = props.payee.memo.substring(0, end);
	if (props.payee.memo.length > 16) {
		truncMemo += '...';
	}
	const truncAsset = props.payee.asset.substring(0,6) + '...'
					+ props.payee.asset.substring(38);

	return (
		<tr align="center" valign="middle">
			<td>&nbsp;</td>
			<td title={detailsTitle}>Payee #{payeeNo}</td>
			<td title="ditto, part of same receipt">""</td>
			<td title={payeeAddr}>{truncAddr}</td>
			<td>{dispAmt.toString()}</td>
			<td title={props.payee.asset}>{truncAsset}</td>
			<td>(redacted)</td>
			<td title="ditto, done in same block">""</td>
			<td title={props.payee.memo}>{truncMemo}</td>
			<td>&nbsp;</td>
		</tr>
	);
}

/* method to render a receipt, including its payees, as a table row
 * @param props.key display key (React)
 * @param props.receipt the receipt record (a Receipt)
 * @param props.rctId the index (same as key but in 3-digit form)
 */
function ReceiptRenderer(props) {
	const { state: { accounts } } = useEth();

	// shorthands
	const receipt = props.receipt;
	const selectId = `select-${props.rctId}`;
	const ariaLabel = `select receipt ${props.rctId}`;
	const truncId = receipt.id.substring(0,4) + '...'
					+ receipt.id.substring(60);

	if (receipt.decrypted) {
		const truncSrc = receipt.source.substring(0,6) + '...'
						+ receipt.source.substring(38);
		let receiptSource = receipt.source;
		if (receiptSource === accounts[0]) {
			receiptSource += " (Self)";
		}
		if (receipt.type === Receipt.M_Recipient) {
			// there will be a single payee entry in the array
			const payee = receipt.payees[0];
			const dispAmt = BigNumber(payee.amount).dividedBy(1e18);
			const truncEid = payee.eNFTid.substring(0,4) + '...'
							+ payee.eNFTid.substring(60);
			const end = Math.min(payee.memo.length, 16);
			let truncMemo = payee.memo.substring(0, end);
			if (payee.memo.length > 16) {
				truncMemo += '...';
			}
			const truncAsset = payee.asset.substring(0,6) + '...'
							+ payee.asset.substring(38);

			// emit a decrypted Recipient receipt (sent to wallet owner)
			return (
				<tr align="center" valign="middle">
					<td>
						{props.rctId}
						<Form.Check type="checkbox" id={selectId}
							name="rctSelect" className="m-2"
							value={receipt.file}
							aria-label={ariaLabel}
						/>
					</td>
					<td title="Paid to you">Recipient</td>
					<td className="text-break" title={receipt.id}>
						{truncId}
					</td>
					<td className="text-break" title={receiptSource}>
						{truncSrc}
					</td>
					<td>{dispAmt.toString()}</td>
					<td title={payee.asset}>{truncAsset}</td>
					<td title={payee.eNFTid}>{truncEid}</td>
					<td>{receipt.block}</td>
					<td title={payee.memo}>{truncMemo}</td>
					<td>
						{receipt.valid
							? <Badge pill bg="success"
								title="MVO signature verified">
								<Image src="images/check2.svg"
									fluid rounded className="p-2"
									height={40} width={40}/>
							  </Badge>
							: <Badge pill bg="danger"
								title="MVO signature check failed">
								<Image src="images/x-lg.svg"
									fluid rounded className="p-2"
									height={40} width={40}/>
							  </Badge>
						}
					</td>
				</tr>
			);
		}
		else {
			/* emit a decrypted Sender receipt (sent by wallet owner)
			 * -- there will be an array of payee entries
			 */
			return (
				<React.Fragment key={`frag${receipt.id}`}>
				{ /* first we emit the header row */ }
				<tr align="center" valign="middle">
					<td>
						{props.rctId}
						<Form.Check type="checkbox" id={selectId}
							name="rctSelect" className="m-2"
							value={receipt.file}
							aria-label={ariaLabel}
						/>
					</td>
					<td title="Paid by you">Sender</td>
					<td className="text-break" title={receipt.id}>
						{truncId}
					</td>
					<td className="text-break" title={receiptSource}>
						{truncSrc}
					</td>
					<td>&nbsp;</td>
					<td>&nbsp;</td>
					<td>&nbsp;</td>
					<td>{receipt.block}</td>
					<td>&nbsp;</td>
					<td>
						{receipt.valid
							? <Badge pill bg="success"
								title="MVO signature verified">
								<Image src="images/check2.svg"
									fluid rounded className="p-2"
									height={40} width={40} />
							  </Badge>
							: <Badge pill bg="danger"
								title="MVO signature check failed">
								<Image src="images/x-lg.svg"
									fluid rounded className="p-2"
									height={40} width={40} />
							  </Badge>
						}
					</td>
				</tr>

				{ /* now emit all payees */ }
				{receipt.payees.map((payee) =>
					<PayeeRenderer key={`${receipt.id}-${payee.sequence}`}
						receipt={receipt} payee={payee}
					/>
				)};
				</React.Fragment>
			);
		}
	}
	else {
		// emit an encrypted receipt (either type, show select + ID only)
		return (
			<tr align="center" valign="middle">
				<td>
					{props.rctId}
					<Form.Check type="checkbox" id={selectId}
						name="rctSelect" className="m-2"
						value={receipt.file}
						aria-label={ariaLabel}
					/>
				</td>
				<td title="Select this receipt and use Download to decrypt">
					(encrypted)
				</td>
				<td className="text-break" title={receipt.id}>
					{truncId}
				</td>
				<td>&nbsp;</td>
				<td>&nbsp;</td>
				<td>&nbsp;</td>
				<td>&nbsp;</td>
				<td>&nbsp;</td>
				<td>&nbsp;</td>
				<td>&nbsp;</td>
			</tr>
		);
	}
}

/* method to render a table of receipt details
 * @param props.receiptData list of stored receipts to display in table
 */
function ReceiptDetailsTable(props) {
	let rctIdx = 1;
	const nf = new Intl.NumberFormat("en-US", {minimumIntegerDigits: 3});
	const hdr = "header" + nf.format(rctIdx);

	// sort data table by block number ascensing
	const sortedReceipts = props.receiptData.toSorted(
								(a, b) => Number(a.block) - Number(b.block));

	return (
		<Table striped bordered hover responsive variant="dark">
			<caption className="caption-top">
				Saved Receipts:<br/>
				<i>
					(Use Download and Decrypt button below to decrypt selected
					details.)
				</i>
			</caption>
			<thead>
				<tr align="center" key={hdr}>
					<th scope="col">
						<Button className="bg-dark" variant="link" role="button"
							title="This selects every receipt checkbox"
							onClick={() => selectAll()} id="selectAll"
						>
							Select All
						</Button>
						<br/>
						<Button className="bg-dark" variant="link" role="button"
							title="This unselects every receipt checkbox"
							onClick={() => deselectAll()} id="deselectAll"
						>
							Select None
						</Button>
					</th>
					<th scope="col"
						title="Whether paid by you (Sender) or to you (Recipient)">
						Type
					</th>
					<th scope="col" title="Unique ID of the receipt">
						Receipt ID
					</th>
					<th scope="col"
						title="The other party, whom you paid or who paid you">
						Counterparty Address
					</th>
					<th scope="col"
						title="Shown in ethers units for readability, converted from wei">
						Amount (1e-18)
					</th>
					<th scope="col"
						title="The token involved in the transaction">
						Asset (Contract Address)
					</th>
					<th scope="col"
						title="The unique ID of the eNFT minted to you">
						eNFT ID
					</th>
					<th scope="col"
						title="The block number in which the transaction occurred">
						Block Number
					</th>
					<th scope="col" title="Message from payer to payee, if any">
						Memo
					</th>
					<th scope="col"
						title="Results of verifying the signature of the MVO that issued the receipt">
						Sig OK
					</th>
				</tr>
			</thead>
			<tbody>
				{sortedReceipts.map((receipt) =>
					<ReceiptRenderer key={"rct" + nf.format(rctIdx)}
						receipt={receipt}
						rctId={nf.format(rctIdx++)}
					/>
				)}
			</tbody>
		</Table>
	);

}

// select every available receipt checkbox
function selectAll() {
	const checkboxList = document.querySelectorAll('[id^="select-"]');
	for (const checkbox of checkboxList) {
		checkbox.checked = true;
	}
}

// de-select every available receipt checkbox
function deselectAll() {
	const checkboxList = document.querySelectorAll('[id^="select-"]');
	for (const checkbox of checkboxList) {
		checkbox.checked = false;
	}
}

/* renderer for entire transaction history page including control buttons
 * @param props.parseReplyMethod the method to call to process MVO replies
 * @param props.onSelect method to switch to other pages
 * @param props.receiptList the list of downloaded and decrypted receipts
 * @param props.lastReceiptReply the last JSON object received from an MVO
 */
function TransactionHistory(props) {
	// enable use of our contracts and accounts
	const { state: { contracts, accounts, web3, chainConn } } = useEth();

	/* Local storage for receipts passed down from Enshroud.  Required because
	 * when we make an async request to an MVO (e.g. via sendReceiptListToMVO())
	 * when props.parseReplyMethod() is invoked, stupid React will gratuitously
	 * reset the state of Enshroud.state.receiptList to an empty array, and
	 * we need to be able to deal with that.  To do so we keep a local version
	 * here and merge it with what is passed down to us in props.receiptList.
	 */
	const [userReceipts, setUserReceipts] = useState([]);

	// local state for receipt DataToast contents
	const [toastData, setToastData] = useState("");

	const rctList = props.receiptList;
	if (props.lastReceiptReply !== undefined && rctList.length > 0) {
		const opcode = props.lastReceiptReply.opcode;

		// this invocation could be from a List, a Get, or a Delete operation
		if (opcode === 'list') {
			/* The receipts passed in props.receiptList are encrypted, which
			 * means only the id field is set.  A receipt with this Id might
			 * already exist in our list, in which case we do nothing as we
			 * don't want to replace a decrypted object with an encrypted one.
			 */
			const receiptsToAdd = [];
			rctList.forEach(inpRct => {
				const weHave = userReceipts.find(elt => elt.id === inpRct.id);
				if (weHave === undefined) {
					receiptsToAdd.push(inpRct);
				}
			});

			// add any new ones all at once so that we re-render only once
			if (receiptsToAdd.length > 0) {
				setUserReceipts([...userReceipts, ...receiptsToAdd]);
				deselectAll();
			}
		} else if (opcode === 'get') {
			/* The receipts passed in props.receiptList are decrypted.  This
			 * means that we want to replace any we already have in our list,
			 * whether those are decrypted or not.  (They will be encrypted if
			 * they're present only because of a prior List, but might also be
			 * decrypted if user has made several Get requests in a row.)
			 */
			const receiptsToReplace = [];
			rctList.forEach(inpRct => {
				const weHave = userReceipts.find(elt => elt.id === inpRct.id);
				if (weHave === undefined) {
					console.error("Saw Get for receiptId " + inpRct.id
								+ " which was not already in fetched list");
				} else if (inpRct.decrypted && !weHave.decrypted) {
					receiptsToReplace.push(inpRct);
				}
			});

			if (receiptsToReplace.length > 0) {
				// do the replacements in one call so we re-render only once
				setUserReceipts(userReceipts.map(r => {
					const onReplaceList
						= receiptsToReplace.find(elt => elt.id === r.id);
					if (onReplaceList !== undefined) {
						return onReplaceList;
					} else {
						return r;
					}
				}));
				deselectAll();
			}
		} else if (opcode === 'delete') {
			/* The receipts passed in props.receiptList are effectively
			 * encrypted, in that only the id and file fields will be set.
			 * we must purge all matching entries.
			 */
			const receiptsToDelete = [];
			rctList.forEach(inpRct => {
				const weHave = userReceipts.some(elt => elt.id === inpRct.id);
				// do not do twice when called on re-render
				if (weHave) {
					receiptsToDelete.push(inpRct);
				}
			});

			if (receiptsToDelete.length > 0) {
				// do the purge in one filter call so we re-render only once
				setUserReceipts(userReceipts.filter(elt =>
					!receiptsToDelete.some(del => del.id === elt.id))
				);
				deselectAll();
			}
		} else {
			console.error("illegal receipt reply opcode, " + opcode);
		}
	
		// parse receipts into Json for toast display
		var receiptJsonData = '';
		if (userReceipts.length > 0) {
			// count the number of decrypted and sig-verified receipts
			let rctCnt = 0;
			userReceipts.forEach(inpRct => {
				if (inpRct.decrypted && inpRct.validateSig(chainConn)) rctCnt++;
			});
			let rctIdx = 0;
			receiptJsonData += "[";
			userReceipts.forEach(inpRct => {
				// only decrypted and sig-valid receipts are shown
				if (inpRct.decrypted && inpRct.valid) {
					let json = JSON.stringify(inpRct);
					if (++rctIdx < rctCnt) {
						json += ",\n";
					}
					receiptJsonData += json;
				}
			});
			receiptJsonData += "]";
		}
		if (toastData !== receiptJsonData) {
			setToastData(receiptJsonData);
		}
	}

	/* method to send a signed receipt list/get/delete request to an MVO
	 * @param resolve Promise to resolve on success
	 * @param reject Promise to reject on fail
	 * @return true on success, else false
	 */
	async function sendReceiptListToMVO(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) {
			const inputErr = new Error("No MVOs listed for chainId " + chId
					+ "; is " + chainConn.chainConfig.chain + " connected?");
			alert(inputErr.message);
			reject(inputErr);
			return false;
		}

		// access msg.sender and verifying contract address
		const sender = accounts[0];
		const enshContract = contracts["EnshroudProtocol"];
		const enshAddress = enshContract.options.address;

		// build capability hash
		var hasher = md.sha256.create();
		hasher.update(sender.toLowerCase());
		const capability = hasher.digest().toHex();

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

		// 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 = 'receiptRequest={"chainId":"' + chId
					+ '","sender":"' + sender + '","opcode":"list",'
					+ '"capability":"' + capability + '"}';

			// 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 list the receipts in your wallet',
					to: {
						MVOId: mvoComm.mvo,
						URL: mvoComm.mvoURL,
					},
					requestJson: {
						receiptRequest: {
							chainId: `${chId}`,
							sender: sender,
							capability: capability,
							opcode: 'list',
							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: 'ReceiptRequest' },
					],
					// not an EIP712Domain definition
					MVO: [
						{ name: 'MVOId', type: 'string' },
						{ name: 'URL', type: 'string' },
					],
					// not an EIP712Domain definition
					ReceiptRequest: [
						{ name: 'receiptRequest', type: 'Payload' },
					],
					// not an EIP712Domain definition
					Payload: [
						{ name: 'chainId', type: 'string' },
						{ name: 'sender', type: 'address' },
						{ name: 'capability', type: 'string' },
						{ name: 'opcode', 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) {
						const 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;
	};

	/* method to send a signed receipt download (get) request to an MVO
	 * @param resolve Promise to resolve on success
	 * @param reject Promise to reject on fail
	 * @param idList specific list of receipt IDs to fetch (set by checkboxes)
	 * @return true on request successfully sent to an MVO (reply is async)
	 * @throws Error on bad input or account signature failure
	 */
	async function sendReceiptGetToMVO(resolve, reject, idList) {
		if (idList === undefined || idList.length === 0) {
			const noIds = new Error("No receipts selected for download");
			alert(noIds.message);
			throw noIds;
		}

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

		// access msg.sender and verifying contract address
		const sender = accounts[0];
		const enshContract = contracts["EnshroudProtocol"];
		const enshAddress = enshContract.options.address;

		// build capability hash
		var hasher = md.sha256.create();
		hasher.update(sender.toLowerCase());
		const capability = hasher.digest().toHex();

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

		// 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)
			let allIds = '';
			let iIdx = 0;
			idList.forEach(id => {
				allIds += ('{"receiptID":"' + id + '"}');
				if (++iIdx < idList.length) allIds += ',';
			});
			payload = 'receiptRequest={"chainId":"' + chId
					+ '","sender":"' + sender + '","opcode":"get",'
					+ '"capability":"' + capability + '"'
					+ '"filespecs":[' + allIds + ']}';

			// send plain data unencrypted and unsigned
			mvoComm.sendToMVO(payload, props.parseReplyMethod);
		}
		else {
			// generate Filespec[] from idList[]
			var fileSpecs = [];
			idList.forEach(id => {
				const fileSpec = { receiptID: id };
				fileSpecs.push(fileSpec);
			});

			// 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 selected receipts',
					to: {
						MVOId: mvoComm.mvo,
						URL: mvoComm.mvoURL,
					},
					requestJson: {
						receiptRequest: {
							chainId: `${chId}`,
							sender: sender,
							capability: capability,
							opcode: 'get',
							filespecs: fileSpecs,
							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: 'ReceiptRequest' },
					],
					// not an EIP712Domain definition
					MVO: [
						{ name: 'MVOId', type: 'string' },
						{ name: 'URL', type: 'string' },
					],
					// not an EIP712Domain definition
					ReceiptRequest: [
						{ name: 'receiptRequest', type: 'Payload' },
					],
					// not an EIP712Domain definition
					Payload: [
						{ name: 'chainId', type: 'string' },
						{ name: 'sender', type: 'address' },
						{ name: 'capability', type: 'string' },
						{ name: 'opcode', type: 'string' },
						{ name: 'filespecs', type: 'Filespec[]' },
						{ name: 'replyKey', type: 'string' },
					],
					Filespec: [
						{ name: 'receiptID', 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) {
						const 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);
				}
			);
		}
		return true;
	}

	/* method to send a signed receipt delete request to an MVO
	 * @param resolve Promise to resolve on success
	 * @param reject Promise to reject on fail
	 * @param idList specific list of receipt IDs to delete (set by checkboxes)
	 * @return true on request successfully sent to an MVO (reply is async)
	 * @throws Error on bad input or account signature failure
	 */
	async function sendReceiptDelToMVO(resolve, reject, idList) {
		if (idList === undefined || idList.length === 0) {
			const noIds = new Error("No receipts selected for deletion");
			alert(noIds.message);
			throw noIds;
		}

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

		// access msg.sender and verifying contract address
		const sender = accounts[0];
		const enshContract = contracts["EnshroudProtocol"];
		const enshAddress = enshContract.options.address;

		// build capability hash
		var hasher = md.sha256.create();
		hasher.update(sender.toLowerCase());
		const capability = hasher.digest().toHex();

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

		// 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)
			let allIds = '';
			let iIdx = 0;
			idList.forEach(id => {
				allIds += ('{"receiptID":"' + id + '"}');
				if (++iIdx < idList.length) allIds += ',';
			});
			payload = 'receiptRequest={"chainId":"' + chId
					+ '","sender":"' + sender + '","opcode":"delete",'
					+ '"capability":"' + capability + '"'
					+ '"filespecs":[' + allIds + ']}';

			// send plain data unencrypted and unsigned
			mvoComm.sendToMVO(payload, props.parseReplyMethod);
		}
		else {
			// generate Filespec[] from idList[]
			var fileSpecs = [];
			idList.forEach(id => {
				const fileSpec = { receiptID: id };
				fileSpecs.push(fileSpec);
			});

			// 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 permanently delete selected receipts',
					to: {
						MVOId: mvoComm.mvo,
						URL: mvoComm.mvoURL,
					},
					requestJson: {
						receiptRequest: {
							chainId: `${chId}`,
							sender: sender,
							capability: capability,
							opcode: 'delete',
							filespecs: fileSpecs,
							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: 'ReceiptRequest' },
					],
					// not an EIP712Domain definition
					MVO: [
						{ name: 'MVOId', type: 'string' },
						{ name: 'URL', type: 'string' },
					],
					// not an EIP712Domain definition
					ReceiptRequest: [
						{ name: 'receiptRequest', type: 'Payload' },
					],
					// not an EIP712Domain definition
					Payload: [
						{ name: 'chainId', type: 'string' },
						{ name: 'sender', type: 'address' },
						{ name: 'capability', type: 'string' },
						{ name: 'opcode', type: 'string' },
						{ name: 'filespecs', type: 'Filespec[]' },
						{ name: 'replyKey', type: 'string' },
					],
					Filespec: [
						{ name: 'receiptID', 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) {
						const 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);
				}
			);
		}
		return true;
	}

	// process passed-in ReceiptS
	return (
		<div className="TransactionHistory">
			<Container fluid align="center">
				<h2>Your Transaction History</h2>
				<br/><br/>

				{ /* Refresh button for receipts (grabs list from MVO) */ }
				<h4>List Your Receipts:
					<LoadingButton variant="primary"
						buttonStyle="m-3"
						buttonTitle="This fetches the receipt list from an MVO, and populates the table below with available receipt IDs"
						netMethod={(resolve, reject) => sendReceiptListToMVO(resolve, reject)}
						buttonText="Refresh"
						buttonIcon="images/receipt.svg"
					/>
					<i>(signature required)</i>
				</h4>

				{ /* table of receipts as it currently exists */ }
				<ReceiptDetailsTable receiptData={userReceipts} />
				<br/><br/>

				<h3>Actions with Selected Receipts:</h3>
				<br/>

				{ /* button to populate table with decrypted receipts */ }
				<h4>
					1. <LoadingButton variant="primary"
						buttonTitle="This downloads and decrypts selected receipts from an MVO"
						netMethod={(resolve, reject) => buildDownloadIdList(resolve, reject)}
						buttonText="Download and Decrypt"
						buttonIcon="images/download.svg"
						buttonStyle="m-3"
					/>
					<i>(signature required)</i>
				</h4>
				<br/><br/>

				{ /* display selected receipts as JSON */ }
				<div className="row mb-3">
					<h5>
						After downloading and decrypting your selected
						receipts, if you wish to save them to a local file, use
						the following procedure.  First, click the "Show Receipt
						Json" button below to display the JSON data.  Then
						either copy and paste this data into a file, print the
						current window contents, take a screenshot, or otherwise
						save the displayed data.
					</h5>
					<p className="text-muted">
						<b>Note that for security reasons, it is not permissible
						for the Enshroud dApp to write to your local disk.</b>
					</p>
				</div>
				<div aria-live="polite" aria-atomic="true"
					className="row mb-3 position-relative"
				>
					<h4>
						2. &nbsp;<DataToast variant="info"
							title="Decrypted receipt data"
							buttonText="Show Receipt Json"
							buttonIcon="images/key.svg"
							buttonTitle="Display decrypted receipt data for selected receipts (JSON format)"
							minWidth="768"
							data={toastData}
						/>
					</h4>
				</div>
				<br/><br/>

				{ /* delete selected receipts forever */ }
				<h4>
					3. <LoadingButton variant="warning"
						buttonStyle="m-3"
						buttonTitle="This permanently(!) deletes your checked receipts (only downloaded / decrypted receipts can be purged)"
						buttonText="Delete"
						buttonIcon="images/file-x.svg"
						netMethod={(resolve, reject) => buildDeleteIdList(resolve, reject)}
					/>
					<i>(signature required)</i>
				</h4>
				<h3><b>Deletion is permanent and irrevocable!</b></h3>
				<h4>
					It also purges the AES keys which decrypt those receipts.
					<br/>Save anything you want to keep before you delete!
				</h4>
				<br/>
			</Container>
		</div>
	);

	// method to build a comma-separated list of receipt IDs to be downloaded
	async function buildDownloadIdList(resolve, reject) {
		// determine which receipt select boxes are checked
		const selectedIds = [];
		const checkboxList = document.querySelectorAll('[id^="select-"]');
		for (const checkbox of checkboxList) {
			if (checkbox.checked) {
				selectedIds.push(checkbox.value);
			}
		}
		if (selectedIds.length === 0) {
			const noSel = new Error("No receipt IDs selected");
			alert(noSel.message);
			reject(noSel);
			return undefined;
		}
		try {
			const sent
				= await sendReceiptGetToMVO(resolve, reject, selectedIds);
			sent ? resolve(true) : reject(false);
		}
		catch (mvoErr) {
			reject(mvoErr);
		}
	}

	// method to build a comma-separated list of receipt IDs to be deleted
	async function buildDeleteIdList(resolve, reject) {
		// determine which receipt select boxes are checked
		const selectedIds = [];
		const checkboxList = document.querySelectorAll('[id^="select-"]');
		for (const checkbox of checkboxList) {
			if (checkbox.checked) {
				selectedIds.push(checkbox.value);
			}
		}
		if (selectedIds.length === 0) {
			const noSel = new Error("No receipt IDs selected");
			alert(noSel.message);
			reject(noSel);
			return undefined;
		}
		let idList = [];
		let delCnt = 0;
		selectedIds.forEach(rId => {
			// verify that this receipt is decrypted before including it
			for (const receipt of props.receiptList) {
				if (receipt.file === rId) {
					if (receipt.decrypted) {
						idList.push(rId);
						delCnt++;
					}
					break;
				}
			}
		});
		if (idList.length === 0 || delCnt < selectedIds.length) {
			const notDown = new Error(
						"Only downloaded/decrypted receipts can be deleted");
			alert(notDown.message);
			reject(notDown);
			return undefined;
		}
		try {
			const sent = await sendReceiptDelToMVO(resolve, reject, idList);
			sent ? resolve(true) : reject(false);
		}
		catch (mvoErr) {
			reject(mvoErr);
		}
	}
}

export default TransactionHistory;
export { Receipt };
