import bitcoin from 'bitcoinjs-lib'
import coinselect from 'coinselect'
import {isValidWIF} from './util'
import {sign} from './Functions/TXSigner'
import MultipartX from './OIPComponents/MultipartX'
import Artifact from './Artifacts/Artifact'
import {flo, flo_testnet} from './networks'
if (typeof window === "undefined" || typeof window.localStorage === "undefined") {
if (typeof localStorage === "undefined") {
var LocalStorage = require('node-localstorage').LocalStorage;
var localStorage = new LocalStorage('./localStorage');
}
} else {
localStorage = window.localStorage
}
const CHOP_MAX_LEN = 890;
const FLODATA_MAX_LEN = 1040;
/**
* Easily publish data onto the FLO chain (mainnet or testnet)
*/
class OIPPublisher {
/**
* Create a new Publisher. Use in conjuction with the Artifact class to publish valid OIP Records or just post random data onto the chain
*
* ##### Example
* Instantiate and use a Publisher
* ```
* let wif = "cRVa9rNx5N1YKBw8PhavegJPFCiYCfC4n8cYmdc3X1Y6TyFZGG4B"
* network = "testnet" //defaults to mainnet
* let publisher = new OIPPublisher(wif, network)
*
* //Publish arbitrary data
* publisher.publishData('Hello, Testnet)').then(txid => txid).catch(err => err)
*
* //Publish data when using the OIP Spec
* let artifact = new Artifact()
* publisher.publish(artifact.toString()).then(response => response).catch(err => err)
* ```
*
* @class
* @param {string} wif - private key in Wallet Import Format (WIF)
* @param {string} [network="mainnet"] - Use "testnet" for testnet
*
* @return {OIPPublisher|Object}
*/
//ToDo:: Switch to mainnet for prod
constructor(wif, network = "testnet") {
if (network === "testnet")
network = flo_testnet
else network = flo
if (!isValidWIF(wif, network.network)) {
return {success: false, message: "Invalid WIF", wif, network: network.network}
}
this.coininfo = network
this.network = network.network
this.explorer = network.explorer
this.ECPair = bitcoin.ECPair.fromWIF(wif, this.network)
this.p2pkh = bitcoin.payments.p2pkh({pubkey: this.ECPair.publicKey, network: this.network}).address
this.spentTransactions = []
this.history = []
this.deserialize()
}
//ToDo::
/**
* Publish OIP Objects to the FLO Chain (will format it as best it can to the protocol spec)
* @param {string} data - the string data you wish to publish !!Make sure to stringify your objects/classes
* @return {Promise<string|Array<string>>} txid - the txid(s) of the broadcasted messages
*/
//Use Cases:
// Publish Artifact
// Publish arbitrary short data
// Publish arbitrary long data
// Publish other OIP Objects
//So essentially: Publish OIPObject
//or: Publish random data
async publish(data) {
if (typeof data !== 'string') {
throw new Error(`Data must be of type string. Got: ${typeof data}`)
}
//ToDo:: Should this append the oip042 here? Or should that be done when stringifying OIP Objects?
//ToDo:: Artifacts need to get timestamped and signed... where should that happen? probably not here...
let broadcast_string = `{oip042:${data}}`
if (broadcast_string.length > FLODATA_MAX_LEN) {
let txids
try {
txids = await this.publishMultiparts(broadcast_string)
} catch (err) {
throw new Error(`Failed to publish multiparts: ${err}`)
}
return txids
} else {
let txid
try {
//ToDo: Make sure Artifacts get json: prefix when stringified
txid = await this.publishData(broadcast_string)
} catch (err) {
throw new Error(`Failed to broadcast message: ${err}`)
}
return txid
}
}
/**
* Publish arbitrary data to the FLO chain
* @param {string} data - String data. Must be below or equal to 1040 characters
* @return {Promise<string>} txid - Returns the id of the transaction that contains the published data
*/
async publishData(data) {
if (typeof data !== 'string') {
throw new Error(`Data must be of type string. Got: ${typeof data}`)
}
if (data.length > 1040) {
return `Error: data length exceeds 1040 characters. Try using OIPPublisher.publish(data) instead.`
}
let hex
try {
hex = await this.buildTXHex(data)
} catch (err) {
throw new Error(`Error building TX Hex: ${err}`)
}
let txid
try {
txid = await this.broadcastRawHex(hex)
} catch (err) {
throw new Error(`Error broadcasting TX Hex: ${err}`)
}
// Add txid to spentTransactions for each spent input
for (let inp of this.selected.inputs) {
if (this.p2pkh === inp.address) {
this.addSpentTransaction(inp.txId)
}
}
this.save(txid, hex)
return txid
}
/**
* Publish data that exceeds the maximum floData length in multiple parts
* @param {string} data - The data you wish to publish
* @return {Promise<Array.<String>>} txids - An array of transaction IDs
*/
async publishMultiparts(data) {
if (typeof data !== 'string') {
throw new Error(`Data must be of type string. Got: ${typeof data}`)
}
let mpx = new MultipartX(data)
let mps = mpx.toMultiParts()
let txids = []
for (let mp of mps) {
if (txids.length > 0) {
//set reference, addr, and sign
mp.setReference(txids[0])
mp.setAddress(this.p2pkh)
let {success, signature, error} = mp.signSelf(this.ECPair)
if (success) {
mp.setSignature(signature)
} else {
throw new Error(error)
}
}
let txid
try {
txid = await this.publishData(mp.toString())
} catch (err) {
throw new Error(`Failed to broadcast mp single: ${err}`)
}
txids.push(txid)
}
return txids
}
/**
* Build a valid FLO Raw TX Hex containing floData
* @param {string} [floData=""] - defaults to an empty string
* @param {Object} output - custom output object
* @return {Promise<string>} hex - Returns raw transaction hex
*
* @example
* ```
* let output = {
* address: `{p2pkh}`,
* value: 100000 //satoshis
* }
* ```
*/
async buildTXHex(floData = "", output) {
let selected
try {
selected = await this.buildInputsAndOutputs(floData, output)
} catch (err) {
throw new Error(`Failed to build inputs and outputs: ${err}`)
}
this.selected = selected
// console.log('selected: ', selected)
let {inputs, outputs, fee} = selected
// inputs and outputs will be undefined if no solution was found
if (!inputs || !outputs) {
throw new Error("No Inputs or Outputs selected! Fail!")
}
let txb = new bitcoin.TransactionBuilder(this.network)
txb.setVersion(this.coininfo.txVersion) //1: w/o floData, 2: w/ floData
inputs.forEach(input => txb.addInput(input.txId, input.vout))
// Check if we are paying to ourself, if so, merge the outputs to just a single output.
// Check if we have two outputs (i.e. pay to and change)
if (outputs.length === 2) {
// If the first input is sending to the from address, and there is a change output,
// then merge the outputs.
if (outputs[0].address === this.p2pkh && !outputs[1].address) {
let totalToSend = outputs[0].value + outputs[1].value
outputs = [{
address: this.p2pkh,
value: totalToSend
}]
} else {
//send the original amount to the first address and send the rest to yourself as change
if (outputs[0].address !== this.p2pkh && !outputs[1].address) {
outputs[1].address = this.p2pkh
}
}
}
outputs.forEach(output => {
if (!output.address) {
throw new Error(`Missing output address: ${outputs}`)
}
txb.addOutput(output.address, output.value)
})
let extraBytes = this.coininfo.getExtraBytes({floData})
for (let i in inputs) {
if (this.p2pkh !== inputs[i].address) throw new Error(`Invalid inputs. Addresses don't match: ${inputs} & ${this.p2pkh}`)
sign(txb, extraBytes, parseInt(i), this.ECPair)
}
let builtHex
try {
builtHex = txb.build().toHex();
} catch (e) {
throw new Error("Unable to build Transaction Hex! \n" + e)
}
builtHex += extraBytes
return builtHex
}
/**
* Builds the inputs and outputs to form a valid transaction hex
* @param {string} [floData=""] - defaults to an empty string
* @param {Object} output - custom output object
* @return {Promise<Object>} selected - Returns the selected inputs to use for the transaction hex
*/
async buildInputsAndOutputs(floData = "", output) {
let utxo
try {
utxo = await this.getUTXO()
} catch (err) {
throw err
}
if (utxo.length === 0) {
throw new Error(`P2PKH: ${this.p2pkh} has no unspent transaction outputs.`)
}
let formattedUtxos = utxo.map(utxo => {
return {
address: utxo.address,
txId: utxo.txid,
vout: utxo.vout,
scriptPubKey: utxo.scriptPubKey,
value: utxo.satoshis,
confirmations: utxo.confirmations
}
})
output = output || {
address: this.p2pkh,
value: Math.floor(0.0001 * this.coininfo.satPerCoin)
}
let targets = [output]
let extraBytes = this.coininfo.getExtraBytes({floData});
let extraBytesLength = extraBytes.length
// console.log(formattedUtxos)
let utxosNoUnconfirmed = formattedUtxos.filter(utx => utx.confirmations > 0)
// console.log(utxosNoUnconfirmed)
let selected = coinselect(utxosNoUnconfirmed, targets, Math.ceil(this.coininfo.feePerByte), extraBytesLength)
// Check if we are able to build inputs/outputs off only unconfirmed transactions with confirmations > 0
if (!selected.inputs || selected.inputs.length === 0 || !selected.outputs || selected.outputs.length === 0 || !selected.fee) {
selected = coinselect(formattedUtxos, targets, Math.ceil(this.coininfo.feePerByte), extraBytesLength)
}
return selected
}
/**
* Get Unspent Transaction Outputs for the given keypair
* @return {Promise<Array.<Object>>} utxo - Returns unspent transaction outputs
*/
async getUTXO() {
let utxo
try {
utxo = await this.explorer.getAddressUtxo(this.p2pkh)
} catch (err) {
throw new Error(`Error fetching UTXO: ${err}`)
}
return this.removeSpent(utxo)
}
/**
* Removes already spent transactions (that are kept in local memory)
* @param unspentTransactions
* @return {Array.<Object>}
*/
removeSpent(unspentTransactions) {
if (!unspentTransactions || !Array.isArray(unspentTransactions))
return
let unspent = [];
for (let tx of unspentTransactions) {
let spent = false
for (let txid of this.spentTransactions) {
if (txid === tx.txid) {
spent = true;
}
}
if (!spent)
unspent.push(tx);
}
return unspent;
}
/**
* Add a spent transaction to local memory
* @param {string} txid - transaction id
* @return {void}
*/
addSpentTransaction(txid) {
this.spentTransactions.push(txid);
}
/**
* Broadcast raw transaction hex to the FLO chain
* @param hex
* @return {Promise<string>} txid - Returns a transaction id
*/
async broadcastRawHex(hex) {
let response
try {
response = await this.explorer.broadcastRawTransaction(hex)
} catch (err) {
throw new Error(`Failed to broadcast TX Hex: ${err}`)
}
let txid
/** Handle { txid: "txid" } */
if (response && typeof response.txid === "string")
txid = response.txid
/**
* Handle
* {
* txid: {
* result: '05d2dd88d69cc32717d315152bfb474b0b1b561ae9a477aae091714c4ab216ac',
* error: null,
* id: 47070
* }
* }
*/
if (response && response.txid && response.txid.result) {
txid = response.txid.result
}
/**
* Handle
* {
* result: '05d2dd88d69cc32717d315152bfb474b0b1b561ae9a477aae091714c4ab216ac',
* error: null,
* id: 47070
* }
*/
if (response && response.result) {
txid = response.result
}
return txid
}
async sendTX(output, floData) {
if (floData && typeof floData !== 'string') {
throw new Error(`Data must be of type string. Got: ${typeof floData}`)
}
if (floData.length > 1040) {
return `Error: data length exceeds 1040 characters.`
}
let hex
try {
hex = await this.buildTXHex(floData, output)
} catch (err) {
throw new Error(`Error building TX Hex: ${err}`)
}
let txid
try {
txid = await this.broadcastRawHex(hex)
} catch (err) {
throw new Error(`Error broadcasting TX Hex: ${err}`)
}
// Add txid to spentTransactions for each spent input
for (let inp of this.selected.inputs) {
if (this.p2pkh === inp.address) {
this.addSpentTransaction(inp.txId)
}
}
this.save(txid, hex)
return txid
}
/**
* Saves a transaction to localStorage and memory
* @param {string} txid
* @param {string} hex
*/
save(txid, hex) {
let tmpObj = {}
tmpObj[txid] = hex
this.history.push(tmpObj)
this.serialize()
}
/**
* Stores important local variables to localStorage such as spent transactions and publish history
*/
serialize() {
let serialized = {
spentTransactions: this.spentTransactions,
history: this.history
}
localStorage.setItem('publisher_history', JSON.stringify(serialized))
}
/**
* Imports publisher history from localStorage
*/
deserialize() {
let deserialized = JSON.parse(localStorage.getItem('publisher_history'))
if (!deserialized)
deserialized = {}
if (deserialized.spentTransactions) {
this.spentTransactions = deserialized.spentTransactions
}
if (deserialized.history) {
this.history = deserialized.history
}
}
/**
* Returns publisher history variables
* @return {{history: Array, spentTransactions: Array}}
*/
getHistory() {
return {
history: this.history,
spentTransactions: this.spentTransactions
}
}
/**
* Deletes the publisher history from localStorage
*/
deleteHistory() {
localStorage.removeItem('publisher_history')
}
}
export default OIPPublisher