// Use strict
"use strict";


// Classes

// MQS class
class Mqs {

	// Public
		
		// Public key to MQS address
		static publicKeyToMqsAddress(publicKey, isMainnet) {
		
			// Check it public key isn't the correct length
			if(publicKey["length"] !== Crypto.SECP256K1_PUBLIC_KEY_LENGTH) {
			
				// Throw error
				throw "Invalid public key.";
			}
			
			// Get version
			var version = Mqs.getAddressVersion(isMainnet);
		
			// Get MQS address from the version and public key
			var mqsAddress = Base58.encodeWithChecksum(Common.mergeArrays([
			
				// Version
				version,
				
				// Public key
				publicKey
			]));
			
			// Return MQS address
			return mqsAddress;
		}
		
		// MQS address to public key
		static mqsAddressToPublicKey(mqsAddress, isMainnet) {
		
			// Check it MQS address isn't the correct length
			if(mqsAddress["length"] !== Mqs.ADDRESS_LENGTH) {
			
				// Throw error
				throw "Invalid MQS address.";
			}
			
			// Try
			try {
			
				// Decode MQS address
				var decodedAddress = Base58.decodeWithChecksum(mqsAddress);
			}
			
			// Catch errors
			catch(error) {
			
				// Throw error
				throw "Invalid MQS address.";
			}
			
			// Get version
			var version = Mqs.getAddressVersion(isMainnet);
			
			// Check if decoded address's length is invalid
			if(decodedAddress["length"] !== version["length"] + Crypto.SECP256K1_PUBLIC_KEY_LENGTH) {
			
				// Throw error
				throw "Invalid MQS address.";
			}
			
			// Check if decoded address's version doesn't match the expected version
			if(Common.arraysAreEqual(decodedAddress.subarray(0, version["length"]), version) === false) {
			
				// Throw error
				throw "Invalid MQS address.";
			}
			
			// Check if decoded address isn't a valid public key
			if(Secp256k1Zkp.isValidPublicKey(decodedAddress.subarray(version["length"])) !== true) {
			
				// Throw error
				throw "Invalid MQS address.";
			}
			
			// Return decoded address without the version
			return decodedAddress.subarray(version["length"]);
		}
		
		// Is valid address with host
		static isValidAddressWithHost(url, isMainnet) {
		
			// Check if URL doesn't contain a host separator
			if(url.indexOf(Mqs.ADDRESS_HOST_SEPARATOR) !== Mqs.ADDRESS_LENGTH) {
			
				// Return false
				return false;
			}
			
			// Try
			try {
			
				// Parse beginning of the URL as an MQS address
				Mqs.mqsAddressToPublicKey(url.substring(0, Mqs.ADDRESS_LENGTH), isMainnet);
			}
			
			// Catch errors
			catch(error) {
			
				// Return false
				return false;
			}
			
			// Check if the end of the URL isn't a valid host
			if(Mqs.ADDRESS_HOST_PATTERN.test(url.substring(Mqs.ADDRESS_LENGTH + Mqs.ADDRESS_HOST_SEPARATOR["length"])) === false || Common.isValidUrl(url.substring(Mqs.ADDRESS_LENGTH + Mqs.ADDRESS_HOST_SEPARATOR["length"])) === false) {
			
				// Return false
				return false;
			}
			
			// Return true
			return true;
		}
		
		// Send request
		static sendRequest(url, slate, secretKey, isMainnet, cancelOccurred = Common.NO_CANCEL_OCCURRED) {
		
			// Return promise
			return new Promise(function(resolve, reject) {
			
				// Check if cancel didn't occur
				if(cancelOccurred === Common.NO_CANCEL_OCCURRED || cancelOccurred() === false) {
				
					// Check if getting sender address from the secret key failed
					var senderPublicKey = Secp256k1Zkp.publicKeyFromSecretKey(secretKey);
					
					if(senderPublicKey === Secp256k1Zkp.OPERATION_FAILED) {
					
						// Reject error
						reject(Message.createText(Language.getDefaultTranslation('Creating slate failed.')));
					}
					
					// Otherwise
					else {
					
						// Get sender address from sender public key
						var senderAddress = Mqs.publicKeyToMqsAddress(senderPublicKey, isMainnet);
			
						// Get receiver address from URL
						var receiverAddress = url.substring(0, Mqs.ADDRESS_LENGTH);
						
						// Get host from URL
						var host = url.substring(Mqs.ADDRESS_LENGTH + Mqs.ADDRESS_HOST_SEPARATOR["length"]);
						
						// Check if host contains a port
						var portIndex = host.indexOf(Mqs.HOST_PORT_SEPARATOR);
						
						if(portIndex !== Common.INDEX_NOT_FOUND) {
						
							// Get port from the host
							var port = parseInt(host.substring(portIndex + Mqs.HOST_PORT_SEPARATOR["length"]), Common.DECIMAL_NUMBER_BASE);
						}
						
						// Otherwise
						else {
						
							// Set port to no port
							var port = Mqs.NO_PORT;
						}
						
						// Initialize connection attempts
						var connectionAttempts = 0;
						
						// Initialize slate posted
						var slatePosted = false;
						
						// Connect to host
						var connectToHost = function() {
						
							// Initialize connected
							var connected = false;
							
							// Initialize reconnect
							var reconnect = true;
							
							// Initialize subscription requested
							var subscriptionRequested = false;
							
							// Initialize post slate requested
							var postSlateRequested = false;
							
							// Create connection
							var connection = new WebSocket(((port === Common.DEFAULT_HTTP_PORT) ? Common.WEBSOCKET_PROTOCOL : Common.WEBSOCKET_SECURE_PROTOCOL) + "//" + ((portIndex !== Common.INDEX_NOT_FOUND) ? host.substring(0, portIndex) : host));
							
							// Disconnect and reject
							var disconnectAndReject = function(messageOrError) {
							
								// Set reconnect to false
								reconnect = false;
							
								// Check if check if canceled interval exists
								if(checkIfCanceledInterval !== Mqs.NO_CHECK_IF_CANCELED_INTERVAL) {
								
									// Clear check if canceled internal
									clearInterval(checkIfCanceledInterval);
									
									// Set check if canceled interval to no interval
									checkIfCanceledInterval = Mqs.NO_CHECK_IF_CANCELED_INTERVAL;
								}
							
								// Try
								try {
							
									// Close connection
									connection.close();
								}
								
								// Catch errors
								catch(error) {
								
								}
								
								// Reject message or error
								reject(messageOrError);
							};
							
							// Set check if canceled interval
							var checkIfCanceledInterval = setInterval(function() {
							
								// Check if cancel occurred
								if(cancelOccurred !== Common.NO_CANCEL_OCCURRED && cancelOccurred() === true) {
								
									// Disconnect and reject
									disconnectAndReject(Common.CANCELED_ERROR);
								}
								
							}, Mqs.CHECK_IF_CANCELED_INTERVAL_MILLISECONDS);
							
							// Connection open event
							$(connection).on("open", function() {
							
								// Check if cancel didn't occur
								if(cancelOccurred === Common.NO_CANCEL_OCCURRED || cancelOccurred() === false) {
							
									// Set connected
									connected = true;
									
									// Reset connection attempts
									connectionAttempts = 0;
								}
								
								// Otherwise
								else {
								
									// Disconnect and reject
									disconnectAndReject(Common.CANCELED_ERROR);
								}
								
							// Connection error event
							}).on("error", function() {
							
								// Check if cancel didn't occur
								if(cancelOccurred === Common.NO_CANCEL_OCCURRED || cancelOccurred() === false) {
							
									// Check if check if canceled interval exists
									if(checkIfCanceledInterval !== Mqs.NO_CHECK_IF_CANCELED_INTERVAL) {
									
										// Clear check if canceled internal
										clearInterval(checkIfCanceledInterval);
										
										// Set check if canceled interval to no interval
										checkIfCanceledInterval = Mqs.NO_CHECK_IF_CANCELED_INTERVAL;
									}
								
									// Try
									try {
								
										// Close connection
										connection.close();
									}
									
									// Catch errors
									catch(error) {
									
									}
								}
								
								// Otherwise
								else {
								
									// Disconnect and reject
									disconnectAndReject(Common.CANCELED_ERROR);
								}
								
							// Connection close event
							}).on("close", function() {
							
								// Check if check if canceled interval exists
								if(checkIfCanceledInterval !== Mqs.NO_CHECK_IF_CANCELED_INTERVAL) {
								
									// Clear check if canceled internal
									clearInterval(checkIfCanceledInterval);
									
									// Set check if canceled interval to no interval
									checkIfCanceledInterval = Mqs.NO_CHECK_IF_CANCELED_INTERVAL;
								}
								
								// Check if cancel didn't occur
								if(cancelOccurred === Common.NO_CANCEL_OCCURRED || cancelOccurred() === false) {
								
									// Check if reconnecting
									if(reconnect === true) {
							
										// Check if not connected and too many unsuccessful connections have been attempted
										if(connected === false && ++connectionAttempts >= Mqs.CONNECTION_ATTEMPTS_THRESHOLD) {
										
											// Reject error
											reject(Message.createText(Language.getDefaultTranslation('Connecting to the host failed.')));
										}
										
										// Otherwise
										else {
										
											// Clear connected
											connected = false;
									
											// Set timeout
											setTimeout(function() {
											
												// Check if cancel didn't occur
												if(cancelOccurred === Common.NO_CANCEL_OCCURRED || cancelOccurred() === false) {
												
													// Check if still reconnecting
													if(reconnect === true) {
											
														// Connect to host
														connectToHost();
													}
												}
												
												// Otherwise
												else {
												
													// Reject canceled error
													reject(Common.CANCELED_ERROR);
												}
												
											}, Mqs.BEFORE_RECONNECT_DELAY_MILLISECONDS);
										}
									}
									
									// Otherwise
									else {
									
										// Clear connected
										connected = false;
									}
								}
								
								// Otherwise
								else {
								
									// Clear connected
									connected = false;
								
									// Reject canceled error
									reject(Common.CANCELED_ERROR);
								}
							
							// Connection message event
							}).on("message", function(event) {
							
								// Check if cancel didn't occur
								if(cancelOccurred === Common.NO_CANCEL_OCCURRED || cancelOccurred() === false) {
							
									// Try
									try {
									
										// Parse message as JSON
										var message = JSON.parse(event["originalEvent"]["data"]);
									}
									
									// Catch errors
									catch(error) {
									
										// Disconnect and reject
										disconnectAndReject(Message.createText(Language.getDefaultTranslation('Invalid message from the host.')));
										
										// Return
										return;
									}
									
									// Check if message is invalid
									if(Object.isObject(message) === false || "type" in message === false) {
									
										// Disconnect and reject
										disconnectAndReject(Message.createText(Language.getDefaultTranslation('Invalid message from the host.')));
									}
									
									// Otherwise
									else {
									
										// Check message's type
										switch(message["type"]) {
										
											// Challenge
											case "Challenge":
											
												// Check if message is invalid
												if("str" in message === false || typeof message["str"] !== "string") {
												
													// Disconnect and reject
													disconnectAndReject(Message.createText(Language.getDefaultTranslation('Invalid message from the host.')));
												}
												
												// Otherwise
												else {
												
													// Get challenge hash
													var challengeHash = new Uint8Array(sha256.arrayBuffer((new TextEncoder()).encode(message["str"])));
													
													// Check if signing challenge with secret key failed
													var challengeSignature = Secp256k1Zkp.createMessageHashSignature(challengeHash, secretKey);
													
													if(challengeSignature === Secp256k1Zkp.OPERATION_FAILED) {
													
														// Disconnect and reject
														disconnectAndReject(Message.createText(Language.getDefaultTranslation('Creating slate failed.')));
													}
													
													// Otherwise
													else {
													
														// Send subscribe request to the host
														connection.send(JSON.stringify({
														
															// Type
															"type": "Subscribe",
															
															// Address
															"address": senderAddress,
															
															// Signature
															"signature": Common.toHexString(challengeSignature)
														}));
														
														// Securely clear challenge signature
														challengeSignature.fill(0);
														
														// Set scription requested
														subscriptionRequested = true;
													}
												}
											
												// Break
												break;
											
											// Ok
											case "Ok":
											
												// Check if subscription was requested
												if(subscriptionRequested === true) {
												
													// Clear subscription request
													subscriptionRequested = false;
													
													// Check if slate hasn't been posted yet
													if(slatePosted === false) {
													
														// Encrypt slate
														Mqs.encrypt(secretKey, Mqs.mqsAddressToPublicKey(receiverAddress, isMainnet), (new TextEncoder()).encode(JSONBigNumber.stringify(slate))).then(function(encryptedData) {
														
															// Check if cancel didn't occur
															if(cancelOccurred === Common.NO_CANCEL_OCCURRED || cancelOccurred() === false) {
															
																// Check if still connected
																if(connected === true) {
														
																	// Create payload
																	var payload = JSON.stringify({
																	
																		// Destination
																		"destination": {
																		
																			// Public key
																			"public_key": receiverAddress,
																			
																			// Domain
																			"domain": (portIndex !== Common.INDEX_NOT_FOUND) ? host.substring(0, portIndex) : host,
																			
																			// Port
																			"port": port
																		},
																		
																		// Nonce
																		"nonce": Common.toHexString(encryptedData[Mqs.ENCRYPTED_DATA_NONCE_INDEX]),
																		
																		// Salt
																		"salt": Common.toHexString(encryptedData[Mqs.ENCRYPTED_DATA_SALT_INDEX]),
																		
																		// Encrypted message
																		"encrypted_message": Common.toHexString(encryptedData[Mqs.ENCRYPTED_DATA_DATA_INDEX])
																	});
																	
																	// Get payload hash
																	var payloadHash = new Uint8Array(sha256.arrayBuffer((new TextEncoder()).encode(payload)));
																	
																	// Check if signing payload with secret key failed
																	var payloadSignature = Secp256k1Zkp.createMessageHashSignature(payloadHash, secretKey);
																	
																	if(payloadSignature === Secp256k1Zkp.OPERATION_FAILED) {
																	
																		// Disconnect and reject
																		disconnectAndReject(Message.createText(Language.getDefaultTranslation('Creating slate failed.')));
																	}
																	
																	// Otherwise
																	else {
																	
																		// Send post slate request to the host
																		connection.send(JSON.stringify({
																		
																			// Type
																			"type": "PostSlate",
																			
																			// From
																			"from": senderAddress,
																			
																			// To
																			"to": receiverAddress,
																			
																			// String
																			"str": payload,
																			
																			// Signature
																			"signature": Common.toHexString(payloadSignature)
																		}));
																		
																		// Set post slate request
																		postSlateRequested = true;
																	}
																}
															}
															
															// Otherwise
															else {
															
																// Disconnect and reject
																disconnectAndReject(Common.CANCELED_ERROR);
															}
														
														// Catch errors
														}).catch(function(error) {
														
															// Check if cancel didn't occur
															if(cancelOccurred === Common.NO_CANCEL_OCCURRED || cancelOccurred() === false) {
														
																// Disconnect and reject
																disconnectAndReject(Message.createText(Language.getDefaultTranslation('Creating slate failed.')));
															}
															
															// Otherwise
															else {
															
																// Disconnect and reject
																disconnectAndReject(Common.CANCELED_ERROR);
															}
														});
													}
												}
												
												// Otherwise check if post slate was request
												else if(postSlateRequested === true) {
												
													// Clear post slate request
													postSlateRequested = false;
													
													// Set slate posted
													slatePosted = true;
												}
												
												// Otherwise
												else {
												
													// Disconnect and reject
													disconnectAndReject(Message.createText(Language.getDefaultTranslation('Invalid message from the host.')));
												}
											
												// Break
												break;
											
											// Slate
											case "Slate":
											
												// Check if slate was posted
												if(slatePosted === true) {
												
													// Set reconnect to false
													reconnect = false;
												
													// Check if check if canceled interval exists
													if(checkIfCanceledInterval !== Mqs.NO_CHECK_IF_CANCELED_INTERVAL) {
													
														// Clear check if canceled internal
														clearInterval(checkIfCanceledInterval);
														
														// Set check if canceled interval to no interval
														checkIfCanceledInterval = Mqs.NO_CHECK_IF_CANCELED_INTERVAL;
													}
												
													// Try
													try {
												
														// Close connection
														connection.close();
													}
													
													// Catch errors
													catch(error) {
													
													}
											
													// Check if message is invalid
													if("from" in message === false || message["from"] !== receiverAddress || "signature" in message === false || Common.isHexString(message["signature"]) === false || "str" in message === false || typeof message["str"] !== "string") {
													
														// Disconnect and reject
														disconnectAndReject(Message.createText(Language.getDefaultTranslation('Invalid message from the host.')));
													}
													
													// Otherwise
													else {
													
														// Try
														try {
														
															// Get receiver public key
															var receiverPublicKey = Mqs.mqsAddressToPublicKey(message["from"], isMainnet);
														}
														
														// Catch errors
														catch(error) {
														
															// Disconnect and reject
															disconnectAndReject(Message.createText(Language.getDefaultTranslation('Invalid message from the host.')));
															
															// Return
															return;
														}
													
														// Get payload hash
														var payloadHash = new Uint8Array(sha256.arrayBuffer((new TextEncoder()).encode(message["str"])));
														
														// Check if payload signature doesn't verify the payload
														if(Secp256k1Zkp.verifyMessageHashSignature(Common.fromHexString(message["signature"]), payloadHash, receiverPublicKey) !== true) {
														
															// Disconnect and reject
															disconnectAndReject(Message.createText(Language.getDefaultTranslation('Invalid message from the host.')));
														}
														
														// Otherwise
														else {
														
															// Try
															try {
															
																// Parse payload as JSON
																var payload = JSON.parse(message["str"]);
															}
															
															// Catch errors
															catch(error) {
															
																// Disconnect and reject
																disconnectAndReject(Message.createText(Language.getDefaultTranslation('Invalid response from the recipient.')));
																
																// Return
																return;
															}
															
															// Check if payload's destination is invalid
															if(Object.isObject(payload) === false || "destination" in payload === false || Object.isObject(payload["destination"]) === false || "public_key" in payload["destination"] === false || payload["destination"]["public_key"] !== senderAddress) {
															
																// Disconnect and reject
																disconnectAndReject(Message.createText(Language.getDefaultTranslation('Invalid response from the recipient.')));
															}
															
															// Otherwise check if payload's nonce is invalid
															else if("nonce" in payload === false || Common.isHexString(payload["nonce"]) === false || Common.hexStringLength(payload["nonce"]) !== Mqs.NONCE_LENGTH) {
															
																// Disconnect and reject
																disconnectAndReject(Message.createText(Language.getDefaultTranslation('Invalid response from the recipient.')));
															}
															
															// Otherwise check if payload's salt is invalid
															else if("salt" in payload === false || Common.isHexString(payload["salt"]) === false || Common.hexStringLength(payload["salt"]) !== Mqs.SALT_LENGTH) {
															
																// Disconnect and reject
																disconnectAndReject(Message.createText(Language.getDefaultTranslation('Invalid response from the recipient.')));
															}
															
															// Otherwise check if payload's encrypted message is invalid
															else if("encrypted_message" in payload === false || Common.isHexString(payload["encrypted_message"]) === false) {
															
																// Disconnect and reject
																disconnectAndReject(Message.createText(Language.getDefaultTranslation('Invalid response from the recipient.')));
															}
															
															// Otherwise
															else {
															
																// Decrypt slate response
																Mqs.decrypt(secretKey, receiverPublicKey, Common.fromHexString(payload["encrypted_message"]), Common.fromHexString(payload["salt"]), Common.fromHexString(payload["nonce"])).then(function(data) {
																
																	// Check if cancel didn't occur
																	if(cancelOccurred === Common.NO_CANCEL_OCCURRED || cancelOccurred() === false) {
																
																		// Try
																		try {
																		
																			// Parse slate response as JSON
																			var slateResponse = JSONBigNumber.parse((new TextDecoder("utf-8", {"fatal": true})).decode(data));
																		}
																		
																		// Catch errors
																		catch(error) {
																		
																			// Disconnect and reject
																			disconnectAndReject(Message.createText(Language.getDefaultTranslation('Invalid response from the recipient.')));
																			
																			// Return
																			return;
																		}
																		
																		// Resolve slate response
																		resolve(slateResponse);
																	}
																	
																	// Otherwise
																	else {
																	
																		// Disconnect and reject
																		disconnectAndReject(Common.CANCELED_ERROR);
																	}
																
																// Catch errors
																}).catch(function(error) {
																
																	// Check if cancel didn't occur
																	if(cancelOccurred === Common.NO_CANCEL_OCCURRED || cancelOccurred() === false) {
																
																		// Disconnect and reject
																		disconnectAndReject(Message.createText(Language.getDefaultTranslation('Invalid response from the recipient.')));
																	}
																	
																	// Otherwise
																	else {
																	
																		// Disconnect and reject
																		disconnectAndReject(Common.CANCELED_ERROR);
																	}
																});
															}
														}
													}
												}
												
												// Otherwise
												else {
												
													// Disconnect and reject
													disconnectAndReject(Message.createText(Language.getDefaultTranslation('Invalid message from the host.')));
												}
											
												// Break
												break;
											
											// Default
											default:
											
												// Disconnect and reject
												disconnectAndReject(Message.createText(Language.getDefaultTranslation('Invalid message from the host.')));
												
												// Break
												break;
										}
									}
								}
								
								// Otherwise
								else {
								
									// Disconnect and reject
									disconnectAndReject(Common.CANCELED_ERROR);
								}
							});
						};
						
						// Connect to host
						connectToHost();
					}
				}
				
				// Otherwise
				else {
				
					// Reject canceled error
					reject(Common.CANCELED_ERROR);
				}
			});
		}
		
		// Address length
		static get ADDRESS_LENGTH() {
		
			// Return address length
			return 52;
		}
		
		// Encrypted data salt index
		static get ENCRYPTED_DATA_SALT_INDEX() {
		
			// Return encrypted data salt index
			return 0;
		}
		
		// Encrypted data nonce index
		static get ENCRYPTED_DATA_NONCE_INDEX() {
		
			// Return encrypted data nonce index
			return Mqs.ENCRYPTED_DATA_SALT_INDEX + 1;
		}
		
		// Encrypted data data index
		static get ENCRYPTED_DATA_DATA_INDEX() {
		
			// Return encrypted data data index
			return Mqs.ENCRYPTED_DATA_NONCE_INDEX + 1;
		}
		
		// Default challenge
		static get DEFAULT_CHALLENGE() {
		
			// Return default challenge
			return "7WUDtkSaKyGRUnQ22rE3QUXChV8DmA6NnunDYP4vheTpc";
		}
		
	// Private
	
		// Get address version
		static getAddressVersion(isMainnet) {
		
			// Check wallet type
			switch(Consensus.getWalletType()) {
			
				// MWC wallet
				case Consensus.MWC_WALLET_TYPE:
		
					// Check if mainnet
					if(isMainnet === true) {
					
						// Return address version
						return new Uint8Array([1, 69]);
					}
					
					// Otherwise
					else {
					
						// Return address version
						return new Uint8Array([1, 121]);
					}
					
					// Break
					break;
				
				// EPIC wallet
				case Consensus.EPIC_WALLET_TYPE:
		
					// Check if mainnet
					if(isMainnet === true) {
					
						// Return address version
						return new Uint8Array([1, 0]);
					}
					
					// Otherwise
					else {
					
						// Return address version
						return new Uint8Array([1, 136]);
					}
					
					// Break
					break;
			}
		}
		
		// Encrypt
		static encrypt(secretKey, publicKey, data) {
		
			// Return promise
			return new Promise(function(resolve, reject) {
		
				// Check if creating shared secret key failed
				var sharedSecretKey = Secp256k1Zkp.publicKeyTweakMultiply(publicKey, secretKey);
				
				if(sharedSecretKey === Secp256k1Zkp.OPERATION_FAILED) {
				
					// Reject error
					reject("Creating shared secret key failed.");
				}
				
				// Otherwise
				else {
				
					// Check if shared secret key isn't zero
					if(Common.arraysAreEqualTimingSafe(sharedSecretKey, (new Uint8Array(sharedSecretKey["length"])).fill(0)) === false) {
				
						// Return creating base key from shared secret key
						return crypto["subtle"].importKey("raw", sharedSecretKey.subarray(1), {
						
							// Name
							"name": Mqs.IMPORT_ALGORITHM
						
						}, false, [
						
							// Derive key
							"deriveKey"
							
						]).then(function(baseKey) {
						
							// Securely clear shared secret key
							sharedSecretKey.fill(0);
						
							// Create random salt
							var salt = new Uint8Array(Mqs.SALT_LENGTH);
							
							crypto.getRandomValues(salt);
							
							// Return deriving key from base key
							return crypto["subtle"].deriveKey({
							
								// Name
								"name": Mqs.IMPORT_ALGORITHM,
								
								// Salt
								"salt": salt,
								
								// Iterations
								"iterations": Mqs.DIGEST_NUMBER_OF_ITERATIONS,
								
								// Hash
								"hash": Mqs.DIGEST_ALGORITHM
								
							}, baseKey, {
							
								// Name
								"name": Mqs.ENCRYPTION_ALGORITHM,
								
								// Length
								"length": Mqs.ENCRYPTION_KEY_LENGTH
								
							}, true, [
							
								// Encrypt
								"encrypt",
								
								// Decrypt
								"decrypt"
							
							]).then(function(derivedKey) {
							
								// TODO Securely clear baseKey
							
								// Return exporting the raw key
								return crypto["subtle"].exportKey("raw", derivedKey).then(function(rawKey) {
								
									// TODO Securely clear derivedKey
									
									// Get raw key in the correct format
									rawKey = new Uint8Array(rawKey);
								
									// Check if data is valid
									if(data["length"] !== 0) {
								
										// Create random nonce
										var nonce = new Uint8Array(Mqs.NONCE_LENGTH);
										
										crypto.getRandomValues(nonce);
										
										// Try
										try {
										
											// Create cipher from raw key and nonce
											var cipher = chacha.createCipher(rawKey, nonce);
											
											// Set cipher's AAD
											cipher.setAAD(Mqs.AAD_VALUE);
											
											// Encrypt data with the cipher
											var encryptedData = cipher.update(data);
											
											// Finish encrypting data
											encryptedData = Common.mergeArrays([
											
												// Encrypted data
												encryptedData,
												
												// Finish encrypting data
												cipher.final()
											]);
											
											// Get tag from the cipher
											var tag = cipher.getAuthTag();
											
											// Append tag to the encrypted data
											encryptedData = Common.mergeArrays([
											
												// Encrypted data
												encryptedData,
												
												// Tag
												tag
											]);
										}
										
										// Catch errors
										catch(error) {
										
											// Securely clear raw key
											rawKey.fill(0);
										
											// Reject error
											reject("Encrypting data failed.");
											
											// Return
											return;
										}
										
										// Securely clear raw key
										rawKey.fill(0);
									
										// Resolve
										resolve([
										
											// Salt
											salt,
											
											// Nonce
											nonce,
											
											// Encrypted data
											encryptedData
										]);
									}
									
									// Otherwise
									else {
									
										// Securely clear raw key
										rawKey.fill(0);
									
										// Reject error
										reject("Invalid data.");
									}
									
								// Catch errors
								}).catch(function(error) {
								
									// TODO Securely clear derivedKey
								
									// Reject error
									reject(error);
								});
							
							// Catch errors
							}).catch(function(error) {
							
								// TODO Securely clear baseKey
							
								// Reject error
								reject(error);
							});
						
						// Catch errors
						}).catch(function(error) {
						
							// Securely clear shared secret key
							sharedSecretKey.fill(0);
						
							// Reject error
							reject(error);
						});
					}
					
					// Otherwise
					else {
					
						// Securely clear shared secret key
						sharedSecretKey.fill(0);
					
						// Reject error
						reject("Invalid shared secret key");
					}
				}
			});
		}
		
		// Decrypt
		static decrypt(secretKey, publicKey, encryptedData, salt, nonce) {
		
			// Return promise
			return new Promise(function(resolve, reject) {
		
				// Check if creating shared secret key failed
				var sharedSecretKey = Secp256k1Zkp.publicKeyTweakMultiply(publicKey, secretKey);
				
				if(sharedSecretKey === Secp256k1Zkp.OPERATION_FAILED) {
				
					// Reject error
					reject("Creating shared secret key failed.");
				}
				
				// Otherwise
				else {
				
					// Check if shared secret key isn't zero
					if(Common.arraysAreEqualTimingSafe(sharedSecretKey, (new Uint8Array(sharedSecretKey["length"])).fill(0)) === false) {
				
						// Return creating base key from shared secret key
						return crypto["subtle"].importKey("raw", sharedSecretKey.subarray(1), {
						
							// Name
							"name": Mqs.IMPORT_ALGORITHM
						
						}, false, [
						
							// Derive key
							"deriveKey"
							
						]).then(function(baseKey) {
						
							// Securely clear shared secret key
							sharedSecretKey.fill(0);
						
							// Return deriving key from base key
							return crypto["subtle"].deriveKey({
							
								// Name
								"name": Mqs.IMPORT_ALGORITHM,
								
								// Salt
								"salt": salt,
								
								// Iterations
								"iterations": Mqs.DIGEST_NUMBER_OF_ITERATIONS,
								
								// Hash
								"hash": Mqs.DIGEST_ALGORITHM
								
							}, baseKey, {
							
								// Name
								"name": Mqs.ENCRYPTION_ALGORITHM,
								
								// Length
								"length": Mqs.ENCRYPTION_KEY_LENGTH
								
							}, true, [
							
								// Encrypt
								"encrypt",
								
								// Decrypt
								"decrypt"
							
							]).then(function(derivedKey) {
							
								// TODO Securely clear baseKey
							
								// Return exporting the raw key
								return crypto["subtle"].exportKey("raw", derivedKey).then(function(rawKey) {
								
									// TODO Securely clear derivedKey
									
									// Get raw key in the correct format
									rawKey = new Uint8Array(rawKey);
								
									// Check if encrypted data is valid
									if(encryptedData["length"] > Mqs.TAG_LENGTH) {
								
										// Try
										try {
										
											// Create decipher from raw key and nonce
											var decipher = chacha.createDecipher(rawKey, nonce);
											
											// Set decipher's AAD
											decipher.setAAD(Mqs.AAD_VALUE);
											
											// Set decipher's tag
											decipher.setAuthTag(encryptedData.subarray(encryptedData["length"] - Mqs.TAG_LENGTH));
											
											// Decrypt encrypted data with the decipher
											var decryptedData = decipher.update(encryptedData.subarray(0, encryptedData["length"] - Mqs.TAG_LENGTH));
											
											// Finish decrypting data
											decryptedData = Common.mergeArrays([
											
												// Decrypted data
												decryptedData,
												
												// Finish decrypting data
												decipher.final()
											]);
										}
										
										// Catch errors
										catch(error) {
										
											// Securely clear raw key
											rawKey.fill(0);
										
											// Reject error
											reject("Decrypting data failed.");
											
											// Return
											return;
										}
										
										// Securely clear raw key
										rawKey.fill(0);
										
										// Resolve decrypted data
										resolve(decryptedData);
									}
									
									// Otherwise
									else {
									
										// Securely clear raw key
										rawKey.fill(0);
									
										// Reject error
										reject("Invalid encrypted data.");
									}
									
								// Catch errors
								}).catch(function(error) {
								
									// TODO Securely clear derivedKey
								
									// Reject error
									reject(error);
								});
							
							// Catch errors
							}).catch(function(error) {
							
								// TODO Securely clear baseKey
							
								// Reject error
								reject(error);
							});
						
						// Catch errors
						}).catch(function(error) {
						
							// Securely clear shared secret key
							sharedSecretKey.fill(0);
						
							// Reject error
							reject(error);
						});
					}
					
					// Otherwise
					else {
					
						// Securely clear shared secret key
						sharedSecretKey.fill(0);
					
						// Reject error
						reject("Invalid shared secret key");
					}
				}
			});
		}
		
		// Import algorithm
		static get IMPORT_ALGORITHM() {
		
			// Return import algorithm
			return "PBKDF2";
		}
		
		// Encryption algorithm
		static get ENCRYPTION_ALGORITHM() {
		
			// Return encryption algorithm
			return "AES-GCM";
		}
		
		// Encryption key length
		static get ENCRYPTION_KEY_LENGTH() {
		
			// Return encryption key length
			return 256;
		}
		
		// Digest algorithm
		static get DIGEST_ALGORITHM() {
		
			// Return digest algorithm
			return "SHA-512";
		}
		
		// Digest number of iterations
		static get DIGEST_NUMBER_OF_ITERATIONS() {
		
			// Return digest number of iterations
			return 100;
		}
		
		// Salt length
		static get SALT_LENGTH() {
		
			// Return salt length
			return 8;
		}
		
		// Nonce length
		static get NONCE_LENGTH() {
		
			// Return nonce length
			return 12;
		}
		
		// Tag length
		static get TAG_LENGTH() {
		
			// Return tag length
			return 16;
		}
		
		// AAD value
		static get AAD_VALUE() {
		
			// Return AAD value
			return new Uint8Array([]);
		}
		
		// Address host separator
		static get ADDRESS_HOST_SEPARATOR() {
		
			// Return address host separator
			return "@";
		}
		
		// Address host patterm
		static get ADDRESS_HOST_PATTERN() {
		
			// Return address host pattern
			return /^[a-z0-9](?:[a-z0-9\.]*[a-z0-9])*(?::[1-9]\d*)?$/ui;
		}
		
		// Host port separator
		static get HOST_PORT_SEPARATOR() {
		
			// Return host port separator
			return ":";
		}
		
		// No port
		static get NO_PORT() {
		
			// Return no port
			return null;
		}
		
		// No check if canceled interval
		static get NO_CHECK_IF_CANCELED_INTERVAL() {
		
			// Return no check if canceled interval
			return null;
		}
		
		// Check if canceled interval milliseconds
		static get CHECK_IF_CANCELED_INTERVAL_MILLISECONDS() {
		
			// Return check if canceled interval milliseconds
			return 50;
		}
		
		// Connection attempts threshold
		static get CONNECTION_ATTEMPTS_THRESHOLD() {
		
			// Return connection attempts threshold
			return 5;
		}
		
		// Before reconnect delay milliseconds
		static get BEFORE_RECONNECT_DELAY_MILLISECONDS() {
		
			// Return before reconnect delay milliseconds
			return 250;
		}
}


// Main function

// Set global object's MQS
globalThis["Mqs"] = Mqs;
