js-oip
Version:
A Javascript Library to fully interact with the Open Index Protocol
784 lines (637 loc) • 21.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
require("core-js/modules/web.dom.iterable");
require("core-js/modules/es6.regexp.to-string");
var _bitcoinjsMessage = require("bitcoinjs-message");
var _bitcoinjsLib = require("bitcoinjs-lib");
var _coinselect = _interopRequireDefault(require("coinselect"));
var _insightExplorer = _interopRequireDefault(require("insight-explorer"));
var _tx = _interopRequireDefault(require("fcoin/lib/primitives/tx"));
var _util = require("../../util");
var _config = require("../../config");
var _FLOTransactionBuilder = _interopRequireDefault(require("../flo/FLOTransactionBuilder"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// This dependency was not found:
//
// * fs in ./node_modules/bindings/bindings.js
if (typeof window === 'undefined' || typeof window.localStorage === 'undefined') {
if (typeof localStorage === 'undefined') {
// eslint-disable-line
// var is needed her for the javascript hoisting effect or else localstorage won't be scoped
var LocalStorage = require('node-localstorage').LocalStorage;
var localStorage = new LocalStorage('./localStorage');
}
} else {
localStorage = window.localStorage;
}
/**
* @typedef {Object} utxo
* @param {string} address - pay to public key hash (pub address)
* @param {TXID} txid - transaction id
* @param {number} vout - index of output in transaction
* @param {string} scriptPubKey - script which ensures that the script supplied in the redeeming transaction hashes to the script used to create the address
* @param {number} amount - the amount spent
* @param {number} satoshis - the amount spent in satoshis
* @param {number} height - the block height of the transaction
* @param {number} confirmations - number of blocks that have been confirmed after the transaction's block
*
* @example
* {
* address: 'ofbB67gqjgaYi45u8Qk2U3hGoCmyZcgbN4',
* txid: '40bf49a02731b04b71951d2e7782b93bd30678c5f5608f0cfe9cdaed6d392903',
* vout: 1,
* scriptPubKey: '76a914f93aef4f4ef998b7ae44bd5bc8f6627b79cdc07588ac',
* amount: 659.9999325,
* satoshis: 65999993250,
* height: 295680,
* confirmations: 6933
* }
*/
/**
* Class to use a web explorer wallet
*/
class ExplorerWallet {
/**
* ##### Example
* ```javascript
* import {OIP} from 'js-oip'
*
* let wif = "cRVa9rNx5N1YKBw8PhavegJPFCiYCfC4n8cYmdc3X1Y6TyFZGG4B"
* let oip = new OIP(wif, "testnet")
* ```
* @param {string} options.wif - private key in Wallet Import Format (WIF) see: {@link https://en.bitcoin.it/wiki/Wallet_import_format}
* @param {string} [options.network="mainnet"] - Use "testnet" for testnet
*/
// ToDo:: Switch to mainnet for prod
constructor(options) {
let network = _config.floMainnet;
if (options.network === 'testnet') {
network = _config.floTestnet;
}
if (options.explorerUrl) {
network.explorer = new _insightExplorer.default(options.explorerUrl);
}
if (!(0, _util.isValidWIF)(options.wif, network.network)) {
return {
success: false,
message: 'Invalid WIF',
wif: options.wif,
network: network.network
};
}
this.coin = network;
this.network = network.network;
this.explorer = network.explorer;
this.ECPair = _bitcoinjsLib.ECPair.fromWIF(options.wif, this.network);
this.p2pkh = _bitcoinjsLib.payments.p2pkh({
pubkey: this.ECPair.publicKey,
network: this.network
}).address;
this.spentTransactions = [];
this.history = [];
this.deserialize();
}
onConfirmation(_ref, options) {
let txids = _ref.txids,
record = _ref.record;
console.warn(`[ExplorerWallet] Error! ExplorerWallet does not support the onConfirmation function!`);
}
signMessage(message) {
let privateKeyBuffer = this.ECPair.privateKey;
let compressed = this.ECPair.compressed || true;
let signatureBuffer;
try {
signatureBuffer = (0, _bitcoinjsMessage.sign)(message, privateKeyBuffer, compressed, this.ECPair.network.messagePrefix);
} catch (e) {
throw new Error(e);
}
let signature = signatureBuffer.toString('base64');
return signature;
}
/**
* Send string 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
* @example
* let oip = new OIP(wif, "testnet")
* let txid = await oip.sendDataToChain('Hello, world')
*/
async sendDataToChain(data) {
if (typeof data !== 'string') {
throw new Error(`Data must be of type string. Got: ${typeof data}`);
}
if (data.length > 1040) {
throw new Error(`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;
}
/**
* Build a valid FLO Raw TX Hex containing floData
* @param {String} [floData=""] - String data to send with tx. Defaults to an empty string
* @param {object|Array.<object>} [output] - Custom output object
* @return {Promise<string>} hex - Returns raw transaction hex
* @example
* //if no output is designed, it will send 0.0001 * 1e8 FLO to yourself
* let output = {
* address: "ofbB67gqjgaYi45u8Qk2U3hGoCmyZcgbN4",
* value: 1e8 //satoshis
* }
* let op = new OIP(wif, "testnet")
* let hex = await op.buildTXHex("floData", output)
*/
async buildTXHex() {
let floData = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
let output = arguments.length > 1 ? arguments[1] : undefined;
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 _selected = selected,
inputs = _selected.inputs,
outputs = _selected.outputs; // 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 _FLOTransactionBuilder.default(this.network);
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);
});
txb.setFloData(floData);
for (let i in inputs) {
if (this.p2pkh !== inputs[i].address) throw new Error(`Invalid inputs. Addresses don't match: ${inputs} & ${this.p2pkh}`);
txb.sign(parseInt(i), this.ECPair);
}
let builtHex;
try {
builtHex = txb.build().toHex();
} catch (err) {
throw new Error(`Unable to build Transaction Hex!: ${err}`);
}
return builtHex;
}
/**
* Builds the inputs and outputs to form a valid transaction hex for the FLO Chain
* @param {string} [floData=""] - defaults to an empty string
* @param {object|Array.<object>} [outputs] - Output or an array of Outputs to send to
* @return {Promise<Object>} Returns the selected inputs, outputs, and fee to use for the transaction hex
* @example
* //basic
* let oip = new OIP(wif, "testnet")
* let selected = await oip.buildInputsAndOutputs("floData") //returns selected inputs, outputs, and fee
* @example
* //with custom output and object destructuring
* let oip = new OIP(wif, "testnet")
* let output = {
* address: "ofbB67gqjgaYi45u8Qk2U3hGoCmyZcgbN4",
* value: 1e8 //in satoshis
* }
* let {inputs, outputs, fee} = await oip.buildInputsAndOutputs("floData", output)
*/
async buildInputsAndOutputs() {
let floData = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
let outputs = arguments.length > 1 ? arguments[1] : undefined;
let utxo;
try {
utxo = await this.getUTXO();
} catch (err) {
throw new Error(`Failed to get utxo: ${err}`);
} // backup in case insight api hasn't given us updated responses
if (utxo.length === 0) {
let start = Date.now();
let finish = 0;
console.log('Insight API returned stale results. Waiting on Insight API for update...');
while (utxo.length === 0 && finish < 6000) {
// console.log('while', finish)
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
await delay(500);
try {
utxo = await this.getUTXO();
} catch (err) {
throw new Error(`Failed to get utxo from insight explorer: ${err}`);
}
finish = Date.now() - start;
}
}
let formattedUtxos;
if (utxo.length === 0) {
// second backup in case insight really is not liking us
console.log('Insight API failed to update. Attempting to manually create utxos...');
formattedUtxos = this.createManualUtxos();
if (formattedUtxos.length === 0) {
throw new Error(`P2PKH: ${this.p2pkh} has no unspent transaction outputs`);
}
} else {
formattedUtxos = utxo.map(utxo => {
return {
address: utxo.address,
txId: utxo.txid,
vout: utxo.vout,
scriptPubKey: utxo.scriptPubKey,
value: utxo.satoshis,
confirmations: utxo.confirmations
};
});
} // console.log('formatted utxos', formattedUtxos)
outputs = outputs || {
address: this.p2pkh,
value: Math.floor(0.0001 * this.coin.satPerCoin)
};
if (!Array.isArray(outputs)) {
outputs = [outputs];
}
let targets = outputs;
let extraBytes = this.coin.getExtraBytes({
floData
});
let extraBytesLength = extraBytes.length; // console.log(formattedUtxos)
// let utxosNoUnconfirmed = formattedUtxos.filter(utx => utx.confirmations > 0) //ToDo
// console.log(utxosNoUnconfirmed)
let selected = (0, _coinselect.default)(formattedUtxos, targets, Math.ceil(this.coin.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 = (0, _coinselect.default)(formattedUtxos, targets, Math.ceil(this.coin.feePerByte), extraBytesLength);
}
return selected;
}
/**
* Get Unspent Transaction Outputs for the given keypair
* @return {Promise<Array.<utxo>>}
* @example
* const wif = 'cRVa9rNx5N1YKBw8PhavegJPFCiYCfC4n8cYmdc3X1Y6TyFZGG4B'
* let oip = new OIP(wif, "testnet")
* let utxos = await oip.getUTXO()
* for (let tx of utxos) {
* console.log(tx)
* // [ { address: 'ofbB67gqjgaYi45u8Qk2U3hGoCmyZcgbN4',
* // txid: '40bf49a02731b04b71951d2e7782b93bd30678c5f5608f0cfe9cdaed6d392903',
* // vout: 1,
* // scriptPubKey: '76a914f93aef4f4ef998b7ae44bd5bc8f6627b79cdc07588ac',
* // amount: 659.9999325,
* // satoshis: 65999993250,
* // height: 295680,
* // confirmations: 5706
* // } ]
* }
*/
async getUTXO() {
let utxo;
try {
utxo = await this.explorer.getAddressUtxo(this.p2pkh);
} catch (err) {
throw new Error(`Error fetching UTXOs: ${err}`);
} // console.log('preutxo: ', utxo)
// console.log(utxo, this.getSpentTransactions())
return this.removeSpent(utxo);
}
/**
* Removes already spent transactions (that are kept in local memory)
* @param {Array.<utxo>} unspentTransactions - An array of utxos
* @return {Array.<utxo>}
* @example
* //shouldn't ever have to write this. Use `OIP.getUTXO()` instead
* let oip = new OIP(wif, 'testnet')
* let utxo
* try {
* utxo = await oip.explorer.getAddressUtxo(pubAddr)
* } catch (err) {
* throw new Error(`${err}`)
* }
* return oip.removeSpent(utxo)
*/
removeSpent(unspentTransactions) {
if (!unspentTransactions || !Array.isArray(unspentTransactions)) {
return;
}
let unspent = [];
for (let tx of unspentTransactions) {
let spent = false;
for (let txid of this.getSpentTransactions()) {
if (txid === tx.txid) {
spent = true;
}
}
if (!spent) {
unspent.push(tx);
}
}
return unspent;
}
/**
* Manually create Unspent Transaction Outputs from previous known transactions.
* Loops through spent transaction IDs in localStorage and created txs to find outputs to use.
* @return {Array.<utxo>}
*/
createManualUtxos() {
// console.log('manually creating utxos')
let unspents = [];
for (let txObj of this.history) {
let match = false;
for (let tx of this.getSpentTransactions()) {
for (let txid in txObj) {
if (txid === tx) {
match = true;
}
}
}
if (!match) {
// console.log(txObj)
unspents.push(txObj);
}
}
let floTxs = [];
for (let txObj of unspents) {
for (let txid in txObj) {
floTxs.push(_tx.default.fromRaw(txObj[txid], 'hex'));
}
} // console.log(floTxs)
let utxos = [];
for (let f of floTxs) {
// console.log(f)
let outputs = f.outputs;
for (let i = 0; i < outputs.length; i++) {
let addr = outputs[i].getAddress(); // console.log(addr)
if (Array.isArray(addr)) {
throw new Error(`Can't handle array output`);
} // convert mainnet addr -> testnet addr
addr = addr.toBase58();
let _address$fromBase58Ch = _bitcoinjsLib.address.fromBase58Check(addr),
hash = _address$fromBase58Ch.hash;
let testnetAddr = _bitcoinjsLib.address.toBase58Check(hash, 115);
if (testnetAddr === this.p2pkh) {
let tmpObj = {
address: testnetAddr,
txId: f.txid(),
vout: i,
value: outputs[i].value,
scriptPubKey: outputs[i].script.toRaw().toString('hex'),
confirmations: 0 // console.log(tmpObj)
};
utxos.push(tmpObj);
}
}
}
return utxos;
}
/**
* Add a spent transaction to local memory
* @param {TXID} txid - transaction id
* @return {void}
* @example
* let oip = new OIP(wif, "testnet")
* let output = {
* address: "oNAydz5TjkhdP3RPuu3nEirYQf49Jrzm4S",
* value: Math.floor(0.001 * floTestnet.satPerCoin)
* }
* let txid = await oip.createAndSendFloTx(output, "sending floData to testnet")
* oip.addSpentTransaction(txid)
* let spentTxs = oip.getSpentTransactions()
* spentTxs === [txid] //true
*/
addSpentTransaction(txid) {
this.spentTransactions.push(txid);
}
/**
* Returns an array of spent transaction ids
* @return {Array.<TXID>}
* @example
* let oip = new OIP(wif, "testnet")
* oip.addSpentTransaction(txid)
* let txids = oip.getSpentTransactions()
* txids = [txid] //true
*/
getSpentTransactions() {
return this.spentTransactions;
}
/**
* 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;
}
/**
* Create and send a FLO tx with a custom output
* @param {object|Array.<object>} outputs
* @param {string} floData
* @return {Promise<TXID>}
* @example
* let oip = new OIP(wif, "testnet")
* let output = {
* address: "oNAydz5TjkhdP3RPuu3nEirYQf49Jrzm4S",
* value: 100000000
* }
* let txid = await oip.createAndSendFloTx(output, "to testnet")
*/
async sendTx(outputs) {
let floData = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
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, outputs);
} 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
* @example
* let oip = new OIP(wif)
* oip.save(`${txid}`, `${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
* @example
* let oip = new OIP(wif)
* oip.serialize() //saves this.spentTransactions and this.history to localStorage memory
*/
serialize() {
let serialized = {
spentTransactions: this.spentTransactions,
history: this.history
};
localStorage.setItem('tx_history', JSON.stringify(serialized));
}
/**
* Imports publisher history from localStorage
* @example
* let oip = new OIP(wif)
* oip.deserialize() //sets this.spentTransactions and this.history from localStorage memory variables
*/
deserialize() {
let deserialized = JSON.parse(localStorage.getItem('tx_history'));
if (!deserialized) {
deserialized = {};
}
if (deserialized.spentTransactions) {
this.spentTransactions = deserialized.spentTransactions;
}
if (deserialized.history) {
this.history = deserialized.history;
}
}
/**
* Returns tx history variables
* @example
* let oip = new OIP(wif)
* oip.getTxHistory()
* //returns
* // {
* // history: this.history,
* // spentTransactions: this.spentTransactions
* // }
* @return {{history: Array, spentTransactions: Array}}
*/
getTxHistory() {
return {
history: this.history,
spentTransactions: this.spentTransactions
};
}
/**
* WARNING!!! Deleting history may cause publishing to temporary fail as it might attempt to use spent transactions. Deletes the publisher history from localStorage
* @example
* let oip = new OIP(wif)
* oip.deleteHistory()
*/
deleteHistory() {
localStorage.removeItem('tx_history');
this.spentTransactions = [];
this.history = [];
}
/**
* Returns the pay-to-pubkey-hash address generated from the given wif
* @return {string}
*/
getPubAddress() {
return this.p2pkh;
}
/**
* Returns the ECPair (private/public key pair) generated from the given wif
* @return {object}
*/
getECPair() {
return this.ECPair;
}
/**
* Returns information about the current coin (either FLO or FLO_Testnet)
* @return {CoinInfo}
*/
getCoinInfo() {
return this.coin;
}
/**
* Returns coin network information needed for address generation
* @return {CoinNetwork}
*/
getNetwork() {
return this.network;
}
}
var _default = ExplorerWallet;
exports.default = _default;