OIPComponents/MPSingle.js

import {sign, verify} from 'bitcoinjs-message'

/**
 * An ES6 Multipart Single Class
 * @class
 */
class MPSingle {
	/**
	 * Construct a Multipart Single by passing in an object or a valid JSON string
	 * ##### Examples
	 * ```javascript
	 * let myOIPObject = {
	 *     part: 0,
	 *     max: 1,
	 *     reference: `${firstTXIDRef}`,
	 *     address: `${p2pkh}`,
	 *     signature: `${signature}`,
	 *     data: `${data}`
	 * }
	 * let mps = new MPSingle(myOIPObject)
	 * //or
	 * mps = new MPSingle(JSON.stringify(myOIPObject))
	 * ```
	 * @param {String|Object} input - a multipart chunk
	 * @return {MPSingle}
	 */
	constructor(input) {
		// this._source = input
		this.prefix = "oip-mp"

		this.part = undefined
		this.max = undefined
		this.address = undefined
		this.reference = undefined
		this.signature = undefined
		this.data = undefined

		this.meta = {
			complete: undefined,
			stale: undefined,
			time: undefined,
			txid: undefined,
			block: undefined,
			block_hash: undefined,
			assembled: undefined,
			tx: undefined
		}

		this.fromInput(input)
	}

	/**
	 * Get Part Number
	 * @return {number}
	 */
	getPart() {
		return parseInt(this.part)
	}

	/**
	 * Set part number
	 * @param {number} part
	 */
	setPart(part) {
		this.part = part
	}

	/**
	 * Get max number of parts
	 * @return {number}
	 */
	getMax() {
		return parseInt(this.max)
	}

	/**
	 * Set max number of parts
	 * @param {number} max
	 */
	setMax(max) {
		this.max = max
	}

	/**
	 * Get publisher address
	 * @return {string}
	 */
	getAddress() {
		return this.address
	}

	/**
	 * Set Publisher address
	 * @param {string} address
	 */
	setAddress(address) {
		this.address = address
	}

	/**
	 * Get the reference to the first part's TXID
	 * @return {string}
	 */
	getReference() {
		return this.reference
	}

	/**
	 * Set the reference to the first part's TXID
	 * @param {string} reference
	 */
	setReference(reference) {
		this.reference = reference
	}

	/**
	 * Get signature
	 * @return {string}
	 */
	getSignature() {
		return this.signature
	}

	/**
	 * Set signature
	 * @param {string} signature
	 */
	setSignature(signature) {
		this.signature = signature
	}

	/**
	 * Get multipart data
	 * @return {string}
	 */
	getData() {
		return this.data
	}

	/**
	 * Set multipart data
	 * @param {string} data - floData
	 */
	setData(data) {
		this.data = data
	}

	/**
	 * Get the multipart meta data
	 * @return {Object}
	 */
	getMeta() {
		return this.meta
	}

	/**
	 * Check if multipart is complete
	 * @return {Boolean}
	 */
	isComplete() {
		return this.meta.complete
	}

	/**
	 * Set whether mulitpart is complete
	 * @param {boolean} isComplete
	 */
	setIsComplete(isComplete) {
		this.meta.complete = isComplete
	}

	/**
	 * Check if multipart is stale
	 * @return {Boolean}
	 */
	isStale() {
		return this.meta.stale
	}

	/**
	 * Set stale param
	 * @param {Boolean} isStale
	 */
	setIsStale(isStale) {
		this.meta.stale = isStale
	}

	getTime() {
		return this.meta.time
	}

	setTime(time) {
		this.meta.time = time
	}

	/**
	 * Get TXID
	 * @return {string}
	 */
	getTXID() {
		return this.meta.txid
	}

	/**
	 * Set TXID
	 * @param {string} txid
	 */
	setTXID(txid) {
		this.meta.txid = txid
	}

	/**
	 * Get Block Height
	 * @return {number}
	 */
	getBlock() {
		return parseInt(this.meta.block)
	}

	/**
	 * Set Block Height
	 * @param {number|string} block
	 */
	setBlock(block) {
		this.meta.block = block
	}

	/**
	 * Get block hash
	 * @return {string}
	 */
	getBlockHash() {
		return this.meta.block_hash
	}

	/**
	 * Set block hash
	 * @param {string} block_hash
	 */
	setBlockHash(block_hash) {
		this.meta.block_hash = block_hash
	}

	/**
	 * Get assembled multipart
	 * @return {string}
	 */
	getAssembled() {
		return this.meta.assembled
	}

	/**
	 * Set assembled multipart
	 * @param {string} assembled - assembled multipart
	 */
	setAssembled(assembled) {
		this.meta.assembled = assembled
	}

	/**
	 * Get Transaction
	 * @return {Object}
	 * @deprecated
	 */
	// getTX() {
	// 	return this.meta.tx
	// }

	/**
	 * Set Transaction
	 * @param {Object} tx
	 * @deprecated
	 */
	// setTX(tx) {
	// 	this.meta.tx = tx
	// }

	/**
	 * Get original source data
	 * @private
	 * @deprecated
	 */
	// _getSource() {
	// 	return this._source
	// }

	/**
	 * Construct a MPSingle from a JSON string or Object
	 * @param {string|object} input - see constructor example
	 * @return {null}
	 */
	fromInput(input) {
		if (!input)
			return new Error(`No input!`)

		if (typeof input === 'string') {
			try {
				input = JSON.parse(input)
			} catch (err) {
				return new Error(`Input is invalid JSON: ${err}`)
			}
		}

		if (input.part !== undefined) {
			this.setPart(input.part)
		}
		if (input.max !== undefined) {
			this.setMax(input.max)
		}
		if (input.address) {
			this.setAddress(input.address)
		}
		if (input.reference) {
			this.setReference(input.reference)
		}
		if (input.signature) {
			this.setSignature(input.signature)
		}
		if (input.data) {
			this.setData(input.data)
		}

		if (!input.meta) return

		if (input.meta.complete !== undefined) {
			this.setIsComplete(input.meta.complete)
			this.setAssembled(input.meta.assembled)
		}
		if (input.meta.stale !== undefined) {
			this.setIsStale(input.meta.stale)
		}
		if (input.meta.time) {
			this.setTime(input.meta.time)
		}
		if (input.meta.txid) {
			this.setTXID(input.meta.txid)
		}
		if (input.meta.block) {
			this.setBlock(input.meta.block)
		}
		if (input.meta.block_hash) {
			this.setBlockHash(input.meta.block_hash)
		}

		// if (input.meta.tx) {
		// 	this.setTX(input.meta.tx)
		// }
	}

	isValid() {
		if (this.getPart() < 0 || this.getPart() === "") {
			return {success: false, message: "Part number can't be negative, null, or undefined"}
		}
		if (this.getPart() > this.getMax()) {
			return {success: false, message: "Part number too high for total parts!"}
		}
		if (this.getMax() < 1) {
			return {success: false, message: "Must have more than one part to be a MULTIPART message!"}
		}
		if (this.getAddress() === "") {
			return {success: false, message: "Must have a Publisher Address!"}
		}
		if (this.getReference() === "" && this.getPart() !== 0) {
			return {
				success: false,
				message: "Only the first part in a multipart message can have a blank first part TXID!"
			}
		}
		if (isNaN(this.getPart()) || isNaN(this.getPart())) {
			return {success: false, message: "The part number and the total part number must be of NUMBER types"}
		}
		if (!this.getSignature()) {
			return {success: false, message: "Must have a Signature!"}
		}

		//signature validation
		if (!verify(this.getSignatureData(), this.getAddress(), this.getSignature(), '\u001bFlorincoin Signed Message:\n')) {
			return {
				success: false,
				message: 'Signature did not pass validation.',
				signatureData: this.getSignatureData(),
				address: this.getAddress(),
				signature: this.getSignature(),
				messagePrefix: '\u001bFlorincoin Signed Message:\n'
			}
		}


		this.is_valid = true;
		return {success: true}
	}

	/**
	 * Convert MPSingle to string
	 * @return {string}
	 * @example
	 * ```
	 * oip-mp(4,6,FLZXRaHzVPxJJfaoM32CWT4GZHuj2rx63k,8c204c5f39,H9dqFw5Pd//qwHeEQA+ENifGvvs/0X1sLUXLQKj2L5qdI/BIJMBX2w3TKETHeNg3MMhA1i3PYVT2FnC8y/BxvUM=):":"Single Track","duration":268},{"fname":"miltjordan-vanishingbreed.jpg","fsize":40451,"type":"Image","subtype":"album-art"},{"fname":"miltjordan-angelsgettheblues.jpg","fsize":54648,"type":"Image","subtype":"cover"}],"location":"QmWmth4ES4ZH9Wgz6Z7S7dRFF8MzJVGgDhit5KzH5uCvZz"},"payment":{"fiat":"USD","scale":"1000:1","maxdisc":30,"promoter":15,"retailer":15,"sugTip"
	 * ```
	 */
	toString() {
		if (this.isValid().success) {
			return this.prefix + "(" +
				this.getPart() + "," +
				this.getMax() + "," +
				this.getAddress() + "," +
				this.getReference() + "," +
				this.getSignature() + "):" +
				this.getData();
		} else return new Error(`Invalid multipart: ${this.isValid().message}`)
	}

	/**
	 * Get Signature Data (the message parameter for the signing function)
	 * @return {string} signatureData
	 */
	getSignatureData() {
		let part = this.getPart()
		let max = this.getMax()
		let address = this.getAddress()
		let reference = this.getPart() ? this.getReference() : ''
		let data = this.getData()
		return `${part}-${max}-${address}-${reference}-${data}`
	}

	/**
	 * Get the signature of a specific message that can be verified by others
	 * @param   {Object} ECPair - Elliptic Curve Key Pair (bitcoinjs-lib/ecpair)
	 * @return  {Object} success - Returns a success object
	 * ```javascript
	 * {success: true, signature: 'base64', error: undefined}
	 *
	 * //or something like
	 *
	 * {success: false, signature: undefined, error: "Missing address for signature}
	 *```
	 *
	 * ```javascript
	 * //nice little trick
	 * let {success, signature, error} = mp.signSelf(ECPair)
	 *
	 * if (success) {
	 *     mp.setSignature(signature)
	 * } else {
	 *     handle(error)
	 * }
	 * ```
	 */
	signSelf(ECPair) {
		if (!ECPair)
			return {success: false, error: "No Private Key available! Unable to sign message!"}

		if (this.getPart() === undefined) {
			return {success: false, error: "Missing part number! Unable to sign message!"}
		}

		if (this.getMax() === undefined) {
			return {success: false, error: "Missing maximum part number! Unable to sign message!"}
		}

		if (this.getReference() === undefined) {
			return {success: false, error: "Missing first txid reference! Unable to sign message!"}
		}

		if (this.getData() === undefined) {
			return {success: false, error: "Missing data! Unable to sign message!"}
		}

		let privateKeyBuffer = ECPair.privateKey;

		let compressed = ECPair.compressed || true;

		let signature_buffer
		try {
			signature_buffer = sign(this.getSignatureData(), privateKeyBuffer, compressed, ECPair.network.messagePrefix)
		} catch (e) {
			return {success: false, error: e}
		}

		return {success: true, signature: signature_buffer.toString('base64')}
	}
}

export default MPSingle