Account.js

import { Wallet, util } from 'oip-hdmw';
import EventEmitter from 'eventemitter3'

import ArtifactPaymentBuilder from './ArtifactPaymentBuilder';
import { isValidEmail, isValidIdentifier, isValidSharedKey } from './util';

import MemoryStorageAdapter from './StorageAdapters/MemoryStorageAdapter';
import LocalStorageAdapter from './StorageAdapters/LocalStorageAdapter';
import KeystoreStorageAdapter from './StorageAdapters/KeystoreStorageAdapter';
import {AccountNotFoundError} from "./Errors";

class Account {
	/**
	 * Create a new Account
	 * @param  {string} username - Pass in your Email, Account ID, or a BIP39 Mnemonic
	 * @param  {string} password - Your Accounts password
	 * @param  {Object} [options]  - Options about the Account being spawned
	 * @param  {Boolean} [options.store_memory=false] - If the wallet should be stored only in the Memory and wiped completely on logout
	 * @param  {Boolean} [options.store_in_keystore=false] - If the wallet should be stored on a Keystore server
	 * @param  {string} [options.keystore_url="https://keystore.oip.li/"] - Keystore to use to store the Account
	 * @param  {Boolean} [options.discover=false] - set discovery
	 * @return {Account}
	 */
	constructor(username, password, options){
		this._username = username
		this._password = password

		this._account = {
			identifier: undefined,
			wallet: {

			},
			settings: {

			},
			history: {

			},
			paymentHistory: {

			}
		};

		if (util.isMnemonic(this._username)){
			this._account.wallet.seed = this._username;
			this._username = undefined;
		}

		if (isValidEmail(this._username))
			this._account.email = this._username

		if (isValidIdentifier(this._username))
			this._account.identifier = this._username

		// Detect what kind of Username we are being passed.
		if (options && options.store_memory) {
			this._storageAdapter = new MemoryStorageAdapter(this._account);
		} else if (options && options.store_in_keystore) {
			this._storageAdapter = new KeystoreStorageAdapter(this._username, this._password, options.keystore_url);
		} else {
			this._storageAdapter = new LocalStorageAdapter(username, this._password);
		}

		this.event_emitter = new EventEmitter()

		this.discover = true;

		if (options && options.discover !== undefined)
			this.discover = options.discover
	}
	/**
	 * Create a new Wallet and save it to the Storage Adapter
	 *
	 * @async
	 * @return {Promise<Object>} Returns a Promise that resolves if the wallet is created successfully.
	 */
	async create(){
		try {
			var identifier = await this._storageAdapter.check()
		} catch (e) {

			// If an error was thrown in `check()` then it means the account does not exist, go ahead and create it then
			this.wallet = new Wallet(this._account.wallet.seed, {discover: this.discover});

			// Subscribe to Websocket Updates
			this.wallet.onWebsocketUpdate(this._handleWalletWebsocketUpdate.bind(this))

			this._account.wallet = this.wallet.serialize()

			var account_data = await this._storageAdapter.create(this._account, this._account.email)

			this._account = account_data

			return this._account	
		}

		throw new Error("Account already exists!")
	}
	/**
	 * Login to the Selected Account. This spawns and creates the oip-hdmw account.
	 * @return {Promise} Returns a Promise that resolves after logging in successfully.
	 */
	async login(){
		// We pass in this._account to the load() on the StorageAdapter in case we are using the Memory Storage Adapter
		let account_info;
		try {
			account_info = await this._storageAdapter.load(this._account)
		} catch (err) {
			throw new Error(err)
			// if (err instanceof AccountNotFoundError) {
			// 	try {
			// 		account_info = await this.create()
			// 	} catch (err) {
			// 		throw new Error(`Login and New Account creation failed: ${err}`)
			// 	}
			// } else {
			// 	throw new Error(err)
			// }
		}

		this._account = account_info;

		if (!this._account.wallet.seed)
			throw new Error("Accounts not containing a Wallet Seed are NOT SUPPORTED!")

		this.wallet = new Wallet(this._account.wallet.seed, {
			discover: this.discover,
			serialized_data: this._account.wallet
		})

		this._account.wallet = this.wallet.serialize()

		this.wallet.onWebsocketUpdate(this._handleWalletWebsocketUpdate.bind(this))

		return JSON.parse(JSON.stringify(this._account))
	}
	/**
	 * Logout of the currently logged in Account
	 */
	logout(){
		this._account.wallet = undefined;
		this._account = undefined;
	}
	/**
	 * Store changed information about the account to the StorageAdapter
	 * @return {Promise<Object>} Returns a Promise that will resolve to the Account Data if the account is saved successfully, or rejects if there was an error storing.
	 */
	async store(){
		// Always save the latest wallet state :)
		this._account.wallet = this.wallet.serialize()

		return await this._storageAdapter.save(this._account, this._account.identifier)
	}
	/**
	 * Set a setting on the Account
	 *
	 * @async
	 * @param {string} setting_node - The Setting you wish to set
	 * @param {Object} setting_info - What you wish to set the setting to
	 * @return {Promise<Object>} Returns a Promise that will resolve with the Account Data after the new setting is saved to the StorageAdapter
	 */
	async setSetting(setting_node, setting_info){
		if (!setting_node)
			throw new Error("setting_node is a required parameter!")

		if (!setting_info && setting_info !== false)
			throw new Error("setting_info is a required parameter!")

		this._account.settings[setting_node] = setting_info

		return JSON.parse(JSON.stringify(await this.store()));
	}
	/**
	 * Get a specific setting
	 * @param {string} setting_node - The Setting you wish to get
	 * @return {Object} Returns the requested setting_info
	 */
	getSetting(setting_node){
		if (!setting_node)
			throw new Error("setting_node is a required parameter!")

		return this._account.settings[setting_node]
	}
	/**
	 * Internal function used to handle Websocket updates streaming in from the Wallet
	 * @param  {Address} address - The Address that was updated
	 */
	_handleWalletWebsocketUpdate(address){
		this.event_emitter.emit("wallet_websocket_update", this, address)

		this._account.wallet = this.wallet.serialize()
		this.store()
	}
	/**
	 * Subscribe to Wallet Websocket updates
	 * @param  {function} subscriberFunction - The function you want called when a Websocket update happens
	 */
	onWalletWebsocketUpdate(subscriberFunction){
		this.event_emitter.on("wallet_websocket_update", subscriberFunction)
	}
	/**
	 * Pay to View or Buy and Artifact File. This makes the purchase as well as saving that info to the wallet.
	 * @param  {Artifact} artifact      - The Artifact from which you got the ArtifactFile from. This is used to lookup payment percentage information.
	 * @param  {ArtifactFile} artifact_file - The specific ArtifactFile that you wish to pay for
	 * @param  {string} purchase_type - Either `view` or `buy`
	 * @param  {string} [coin]   - The Coin you wish to pay with
	 * @param  {string} [fiat]     - A string containing information about the users source currency (i.e. "usd")
	 * @return {Promise<Transaction>} Returns a Promise that will resolve to the payment transaction, or rejects if there is a payment error.
	 */
	payForArtifactFile(artifact, artifact_file, purchase_type, coin, fiat){
		return new Promise((resolve, reject) => {
			let builder = new ArtifactPaymentBuilder(this.wallet, artifact, artifact_file, purchase_type, coin, fiat);

			builder.pay().then(resolve).catch(reject)
		}) 
	}
	/**
	 * Send a tip to the Publisher for a specific Artifact
	 * @param  {Artifact} artifact - The Artifact you wish to tip
	 * @param  {number} amount   - The Amount in `fiat` you wish to tip
	 * @param  {string} [coin]   - The Coin you wish to pay with
	 * @param  {string} [fiat="usd"]     - A string containing information about the users source currency (i.e. "usd")
	 * @return {Promise<Transaction>} Returns a Promise that will resolve to the payment transaction, or rejects if there is a payment error.
	 */
	sendArtifactTip(artifact, amount, coin, fiat){
		return new Promise((resolve, reject) => {
			let builder = new ArtifactPaymentBuilder(this.wallet, artifact, amount, 'tip', coin, fiat)
			
			builder.pay().then(resolve).catch(reject)
		})
	}

	/**
	 * Instantiate and Artifact payment builder so you can do cool stuff! All params are optional. Include what you need to use. Put undefined for those you don't need.
	 * @param {Wallet} [wallet] - oip-HDMW
	 * @param {Artifact} [artifact] - oip-Artifact
	 * @param {(ArtifactFile|number)} [file] - an oip-ArtifactFile or the amount you want to pat
	 * @param {string} [type] - 'view', 'buy', or 'tip'
	 * @param {string} [coin] - the coin you prefer to pay with (currently only supports one coin
	 * @param {string} [fiat="usd"] - the fiat currency you deal with
	 * @returns {ArtifactPaymentBuilder}
	 */
	getPaymentBuilder(wallet, artifact, file, type, coin, fiat) {
		return new ArtifactPaymentBuilder(wallet, artifact, file, type, coin, fiat)
	}
}

module.exports = Account