UNPKG

js-oip

Version:

A Javascript Library to fully interact with the Open Index Protocol

951 lines (710 loc) 40.2 kB
"use strict"; 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 _axios = _interopRequireDefault(require("axios")); var _bitcoinjsMessage = require("bitcoinjs-message"); var _bitcoinjsLib = require("bitcoinjs-lib"); var _config = require("../../config"); var _FLOTransactionBuilder = _interopRequireDefault(require("../flo/FLOTransactionBuilder")); var _FLOTransaction = require("../flo/FLOTransaction"); var _Peer = _interopRequireDefault(require("../flo/Peer")); var _util = require("../../util"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } // Helper const const ONE_MB = 1000000; const ONE_SECOND = 1000; const ONE_MINUTE = 60 * ONE_SECOND; // 1 satoshis per byte (1000 satoshi per kb) (100 million satoshi in 1 FLO) const TX_FEE_PER_BYTE = 0.00000001; // Average size of tx data (without floData) to calculate min txFee const TX_AVG_BYTE_SIZE = 200; const SAT_PER_FLO = 100000000; // The minimum amount a utxo is allowed to be for us to use const MIN_UTXO_AMOUNT = 1; // Prevent chaining over ancestor limit const MAX_MEMPOOL_ANCESTORS = 1225; const MAX_MEMPOOL_ANCESTOR_SIZE = 1.50 * ONE_MB; // Timer lengths used to track and fix the Ancestor chain const UPDATE_ANCESTOR_STATUS = 5 * ONE_SECOND; const REPAIR_ANCESTORS_AFTER = 1 * ONE_MINUTE; // One FLO Block on average const PEER_CONNECT_LENGTH = 2 * ONE_SECOND; const PEER_DESTROY_LENGTH = 5 * ONE_SECOND; const REBROADCAST_LENGTH = 10 * ONE_SECOND; const CONFIRMATION_CHECK_INTERVAL = 20 * ONE_SECOND; const CONFIRMATION_CHECK_UPDATE_ANCESTORS_AFTER = 5 * ONE_MINUTE; const CONFIRMATION_REBROADCAST_DELAY = 2 * ONE_MINUTE; const REPAIR_MIN_TX = 100; // List of all Wallet-Only RPC Methods const WALLET_RPC_METHODS = ['selectwallet', 'getwalletinfo', 'fundrawtransaction', 'resendwallettransactions', 'abandontransaction', 'addmultisigaddress', 'addwitnessaddress', 'backupwallet', 'dumpprivkey', 'dumpwallet', 'encryptwallet', 'getaccountaddress', 'getaccount', 'getaddressesbyaccount', 'getbalance', 'getnewaddress', 'getrawchangeaddress', 'getreceivedbyaccount', 'getreceivedbyaddress', 'gettransaction', 'getunconfirmedbalance', 'importprivkey', 'importwallet', 'importaddress', 'importprunedfunds', 'importpubkey', 'keypoolrefill', 'listaccounts', 'listaddressgroupings', 'lockunspent', 'listlockunspent', 'listreceivedbyaccount', 'listreceivedbyaddress', 'listsinceblock', 'listtransactions', 'listunspent', 'move', 'sendfrom', 'sendmany', 'sendtoaddress', 'setaccount', 'settxfee', 'signmessage', 'walletlock', 'walletpassphrasechange', 'walletpassphrase', 'removeprunedfunds']; /** * Easily interact with an RPC Wallet to send Bulk transactions extremely quickly in series */ class RPCWallet { constructor(options) { // Store options for later this.options = options || {}; // Set default network to livenet if unset if (!this.options.network) { this.options.network = 'livenet'; } // Make sure we have connection to an RPC wallet if (!this.options.rpc) { throw new Error("RPC options ('options.rpc') are required with an RPC wallet!"); } // Make sure we are being passed a public and private key pair if (!this.options.wif) { throw new Error('`wif` (a Private Key) is a required option in order to use RPC Wallet!'); } if (!this.options.publicAddress) { throw new Error('`publicAddress` (the Public Address for the `wif`) is a required option in order to use RPC Wallet!'); } // If the port is not set, default to Livenet (7313), otherwise if they passed the string "testnet" use the testnet port (17313) if (!this.options.rpc.port) { if (this.options.network && this.options.network === 'testnet') { this.options.rpc.port = 17313; } else if (this.options.network && this.options.network === 'regtest') { this.options.rpc.port = 17413; } else { this.options.rpc.port = 7313; } } // If host is not set, use localhost if (!this.options.rpc.host) { this.options.rpc.host = 'localhost'; } // Create the RPC connection using Axios this.rpc = _axios.default.create({ auth: { username: this.options.rpc.username, password: this.options.rpc.password }, validateStatus: function validateStatus(status) { if ([400, 401, 402, 403, 404].includes(status)) { return false; } return true; } }); // Default to not being an fcoin RPC node this.fcoinRPC = false; // Store the Private Key and the Public Key this.wif = this.options.wif; this.publicAddress = this.options.publicAddress; this.importPrivateKey = this.options.importPrivateKey || true; // Store the "coin" network we should use if (this.options.network === 'livenet' || this.options.network === 'mainnet' || this.options.network === 'main') { this.coin = _config.floMainnet; } else if (this.options.network === 'testnet') { this.coin = _config.floTestnet; } else if (this.options.network === 'regtest') { this.coin = _config.floRegtest; } // Store information about our tx chain and the previous tx output this.unconfirmedTransactions = []; this.unconfirmedTxids = []; this.previousTXOutput = undefined; this.onConfirmationSubscriptions = {}; this.onConfirmationInterval = undefined; this.lastTXTime = Date.now(); // Initialize our initial peer array this.peers = []; // Variables to count utxo ancestors (maximum number of unconfirmed transactions you can chain) this.currentAncestorCount = 0; this.currentAncestorSize = 0; // Repair mode tracker this.repairMode = false; this.rebroadcasting = false; this.checkingAncestorCount = false; } /** * Make any RPC request * @param {String} method - The RPC method you wish to call * @param {Array} parameters - The parameters to pass to with RPC method you are calling * @return {Object} Returns the data response */ async rpcRequest(method, parameters) { // Verify we have all the parameters we need if (!method) { throw new Error("rpcRequest parameter 'method' is Required!"); } if (!parameters) { throw new Error("rpcRequest parameter 'parameters' is Required!"); } // Perform the RPC request using Axios let rpcRequest; try { // Make sure that rpcPort is an int so that we don't append 2 as a string to the port let rpcPort = parseInt(this.options.rpc.port); // If we are using an fcoin RPC, the wallet RPC runs on a different port (7315/17315 instead of 7313/17313) if (WALLET_RPC_METHODS.includes(method) && this.fcoinRPC) { rpcPort += 2; } // Attempt the RPC request rpcRequest = await this.rpc.post(`http://${this.options.rpc.host}:${rpcPort}/`, { 'jsonrpc': '2.0', 'id': 1, 'method': method, 'params': parameters }); // If we have an error with the Method not being found, it is likely an issue with the RPC server being fcoin. if (rpcRequest.data && rpcRequest.data.error && rpcRequest.data.error.message && rpcRequest.data.error.message.includes('Method not found')) { this.fcoinRPC = true; return this.rpcRequest(method, parameters); } } catch (e) { // Throw if there was some weird error for some reason. console.log("[RPC Wallet] Unable to perform RPC request, retrying! Method: '" + method + "' - Params: '" + JSON.stringify(parameters) + "' | RPC settings: " + JSON.stringify(this.options.rpc) + ' | Thrown Error: ' + e); // Sleep 1 second await new Promise((resolve, reject) => { setTimeout(() => { resolve(); }, 1000); }); // Retry RPC Request recursively return this.rpcRequest(method, parameters); } // Remove the `id` field from the response, since we do not care about it return { result: rpcRequest.data.result, error: rpcRequest.data.error }; } // This method checks the database async checkAllUnconfirmed() { let initialUnconfirmedCount = this.unconfirmedTxids.length; let confirmedTXIDs = []; let foundUnconfirmed = false; let numberConfirmed = 0; while (this.unconfirmedTxids.length !== 0 && !foundUnconfirmed) { let txidToCheck = this.unconfirmedTxids[0]; // Check to see if the utxo is still in the mempool and if it has ancestors let getMempoolEntry = await this.rpcRequest('getmempoolentry', [txidToCheck]); // Check if we have an error and handle it if (getMempoolEntry.error && getMempoolEntry.error !== null) { // If the error 'Transaction not in mempool' occurs, that means that the most recent transaction // has already recieved a confirmation, so it has no ancestors we need to worry about. // If the error is different, than throw it up for further inspection. if (getMempoolEntry.error.message === 'Transaction not in mempool' || getMempoolEntry.error.message === 'Transaction not in mempool.') { // Grab the transaction and check the number of confirmations let checkConfirmations = await this.rpcRequest('getrawtransaction', [txidToCheck, true]); // Ignore if there was an error, but set the number of confirmations if available let confirmations; if (checkConfirmations.result && checkConfirmations.result.confirmations) { confirmations = checkConfirmations.result.confirmations; } // Check to make sure if it is not in the mempool, that it at least has one confirmation. if (confirmations >= 1) { // Since it has a confirmation, remove it from the list! // console.log(`Transaction was already Confirmed! Removing it from the list! ${txidToCheck}`) numberConfirmed++; this.unconfirmedTransactions.shift(); this.unconfirmedTxids.shift(); } else { console.error(`TXID not in mempool AND not found! ${txidToCheck}`); foundUnconfirmed = true; } } } else { // Break out of the while loop once there is a transaciton that exists in the Mempool foundUnconfirmed = true; console.log(`Oldest TX in mempool ${txidToCheck}`); } } console.log(`[RPC Wallet] ${numberConfirmed} Transactions Confirmed!`); return numberConfirmed; } /** * Grab the latest unconfirmed tx and check how many ancestors it has * @return {Boolean} Returns true if the update was successful */ async updateAncestorStatus() { console.log('[RPC Wallet] Updating Ancestor Status...'); // Check all transactions still inside of our unconfirmed array await this.checkAllUnconfirmed(); // Using the unconfirmed array, sum up the current Ancestor total this.currentAncestorCount = this.unconfirmedTxids.length; this.currentAncestorSize = 0; for (let hex of this.unconfirmedTransactions) { this.currentAncestorSize += hex.length; } // // We next check to see if there are any transactions currently in the mempool that we need to be aware of. // // To check the mempool, we start by grabbing the UTXO's to get the txid of the most recent transaction that was sent // let utxos = await this.getUTXOs() // // Grab the most recent txid // let mostRecentUTXO = utxos[0] // let mostRecentTXID = mostRecentUTXO.txid // // Check to see if the utxo is still in the mempool and if it has ancestors // let getMempoolEntry = await this.rpcRequest('getmempoolentry', [ mostRecentTXID ]) // // Check if we have an error and handle it // if (getMempoolEntry.error && getMempoolEntry.error !== null) { // // If the error 'Transaction not in mempool' occurs, that means that the most recent transaction // // has already recieved a confirmation, so it has no ancestors we need to worry about. // // If the error is different, than throw it up for further inspection. // if (getMempoolEntry.error.message === 'Transaction not in mempool' || getMempoolEntry.error.message === 'Transaction not in mempool.') { // // Grab the transaction and check the number of confirmations // let checkConfirmations = await this.rpcRequest('getrawtransaction', [ mostRecentTXID, true ]) // // Ignore if there was an error, but set the number of confirmations if available // if (checkConfirmations.result && checkConfirmations.result.confirmations) { mostRecentUTXO.confirmations = checkConfirmations.result.confirmations } // // Check to make sure if it is not in the mempool, that it at least has one confirmation. // if (mostRecentUTXO.confirmations >= 1) { // // Reset utxo count if the most recent one was confirmed // this.currentAncestorCount = 0 // this.currentAncestorSize = 0 // } else { // // If we have gotten here, that means the transaction has zero confirmations, and is not included in the mempool, and so we need to repair it's chain... // console.log(`[RPC Wallet] [WARNING] The most recent unspent transaction has zero confirmations and is not in the mempool! Attempting to repair mempool by rebroadcasting transactions, please wait... (txid: ${mostRecentTXID}) ${JSON.stringify(checkConfirmations)}`) // await this.checkAllUnconfirmed() // await this.rebroadcastTransactions() // // Don't update anything, and return for now, so that `updateAncestorStatus` will run again // return // } // } else { // // Throw an error if the transaction error is not that it is missing from the mempool. // throw new Error('Error grabbing the mempool entry! ' + JSON.stringify(getMempoolEntry.error)) // } // } // // If the tx is still in the mempool, it will have results // if (getMempoolEntry.result) { // let txMempoolStatus = getMempoolEntry.result // // Store the current ancestor count & size // this.currentAncestorCount = txMempoolStatus.ancestorcount // this.currentAncestorSize = txMempoolStatus.ancestorsize // // Also increase the count by one in order to account for the txid from the mempool // this.currentAncestorCount++ // } console.log(`[RPC Wallet] Updated Ancestor Count: ${this.currentAncestorCount} - Updated Ancestor Size: ${(this.currentAncestorSize / ONE_MB).toFixed(2)}MB`); return true; } /** * Add a transaction we just sent to the ancestor count/size * @param {String} hex - The transaction hex to count * @return {Boolean} Returns true on success */ async addAncestor(hex) { // Increase the ancestor count this.currentAncestorCount++; // Increase the ancestor size (byte length) this.currentAncestorSize += hex.length; // Every 100 update our count if (this.currentAncestorCount % 100 === 0) { await this.updateAncestorStatus(); } // Log every 25 if (this.currentAncestorCount % 25 === 0) { console.log(`[RPC Wallet] Updated Ancestor Count: ${this.currentAncestorCount} - Updated Ancestor Size: ${(this.currentAncestorSize / ONE_MB).toFixed(2)}MB`); } return true; } /** * Checks the current ancestor count and returns when it is safe to send more transactions. * This method will wait for tx's in the mempool to be confirmed before continuing. * @async * @return {Boolean} Returns `true` once it is safe to continue sending transactions */ async checkAncestorCount(forceUpdateAncestor) { // Store our starting count let startAncestorCount = this.currentAncestorCount; let startAncestorSize = this.currentAncestorSize; // Variables to track loop state let firstLoop = true; let hadMaxAncestors = false; let reachedAncestorLimitTimestamp; // Check if we have too many ancestors, and if we do, wait for the ancestor count to decrease (aka, some transactions to get confirmed in a block) let forceFirstLoopUpdate = forceUpdateAncestor; while (this.currentAncestorCount >= MAX_MEMPOOL_ANCESTORS || this.currentAncestorSize >= MAX_MEMPOOL_ANCESTOR_SIZE || forceFirstLoopUpdate) { this.checkingAncestorCount = true; // Only force an update for the first loop, then disable to prevent looping forever forceFirstLoopUpdate = false; // Wait for UPDATE_ANCESTOR_STATUS seconds (don't run on the first loop through) if (!firstLoop) { await new Promise((resolve, reject) => { setTimeout(() => { resolve(); }, UPDATE_ANCESTOR_STATUS); }); } // Update the ancestor status (this is what will break us out of our while loop) await this.updateAncestorStatus(); // Check if we currently have the maximum number of ancestors let hasMaxAncestors = this.currentAncestorCount >= MAX_MEMPOOL_ANCESTORS || this.currentAncestorSize >= MAX_MEMPOOL_ANCESTOR_SIZE; // Only log ancestor count if it is the first loop, and we still have too many ancestors if (hasMaxAncestors && firstLoop) { console.log(`[RPC Wallet] Maximum Unconfirmed Transaction count reached, pausing sending of transactions until some of the current transactions get confirmed | Ancestor Count: ${this.currentAncestorCount} - Ancestor Size: ${(this.currentAncestorSize / ONE_MB).toFixed(2)}MB`); hadMaxAncestors = true; reachedAncestorLimitTimestamp = Date.now(); } // After it has been REPAIR_ANCESTORS_AFTER amount of time since the max ancestor limit was reached, enable repair mode if (hasMaxAncestors && Date.now() - REPAIR_ANCESTORS_AFTER > reachedAncestorLimitTimestamp) { // Announce that we are going to attempt the repair console.log(`[RPC Wallet] [WARNING] Unconfirmed Transaction count has not decreased in ${REPAIR_ANCESTORS_AFTER / ONE_MINUTE} minutes, rebroadcasting transactions in an attempt to repair the utxo chain!`); // Attempt the actual repair await this.rebroadcastTransactions(); // Reset the timestamp so that we wait for REPAIR_ANCESTORS_AFTER seconds/minutes reachedAncestorLimitTimestamp = Date.now(); } firstLoop = false; } this.checkingAncestorCount = false; // Count the number of confirmed // let numberConfirmed = startAncestorCount - this.currentAncestorCount // Remove the transactions that just got confirmed // for (let i = 0; i < numberConfirmed; i++) { // this.unconfirmedTransactions.shift() // this.unconfirmedTxids.shift() // } // let numberConfirmed = await this.checkAllUnconfirmed() // // Check to see how many transactions got confirmed // if (startAncestorCount >= MAX_MEMPOOL_ANCESTORS || startAncestorSize >= MAX_MEMPOOL_ANCESTOR_SIZE) { // // If there are less confirmed than REPAIR_MIN_TX, then rebroadcast the transactions // if (numberConfirmed < REPAIR_MIN_TX) { // console.log(`[RPC Wallet] Detected low number of transactions confirmed, re-announcing transactions to make sure miners saw them.`) // await this.rebroadcastTransactions() // } // } // If we had started with maximum ancestors, then log the status if (hadMaxAncestors) { console.log(`[RPC Wallet] Unconfirmed count has decreased, resuming sending transactions! | Ancestor Count: ${this.currentAncestorCount} - Ancestor Size: ${(this.currentAncestorSize / ONE_MB).toFixed(2)}MB`); } // If we enabled repair mode, then this.repairMode = false; // There are fewer ancestors than the maximum, so we can send the next transaction! return true; } /** * Rebroadcast out all transactions on our local mempool */ async rebroadcastTransactions() { // Lock rebroadcasting; Only perform a single rebroadcast at a time!! if (this.rebroadcasting) { console.log(`[RPC Wallet] Already rebroadcasting transactions...`); return; } this.rebroadcasting = true; // Announce that we are starting console.log(`[RPC Wallet] Announcing ${this.unconfirmedTransactions.length} transactions to Peers...`); // Destroy any peers we have currently connected to to get a "clean" rebroadcast await this.destroyPeers(); // Connect to Peers to use for the rebroadcast await this.connectToPeers(); // Announce all of our local unconfirmed transactions // Loop through all peers and announce TXs to each (if we have connected to them) for (let peer of this.peers) { await peer.announceTXs(this.unconfirmedTransactions); } // Wait for REBROADCAST_LENGTH in order to give some time for transactions to be requested, and sent out. await new Promise((resolve, reject) => { setTimeout(() => { resolve(); }, REBROADCAST_LENGTH); }); // Unlock rebroadcasting this.rebroadcasting = false; } /** * Initialize the RPC Wallet. This imports the Private Key, and then checks for unconfirmed transactions in the mempool. * @async * @return {Boolean} Returns true on Success */ async initialize() { if (this.importPrivateKey) { console.log(`[RPC Wallet] Importing the Private Key to the RPC Wallet, this may take a long time...`); // First, we import the Private Key to make sure it exists when we attempt to send transactions. let importPrivKey = await this.rpcRequest('importprivkey', [this.wif, 'default', true]); console.log(`[RPC Wallet] Private Key Import to the RPC Wallet Complete!`); // Check for an error importing the private key. If there is no error, the private key import was successful. // No error and no result signify that the Private Key was already imported previously to the wallet. if (importPrivKey.error && importPrivKey.error !== null && importPrivKey.error.message !== 'Key already exists.') { throw new Error('Error Importing Private Key to RPC Wallet: ' + JSON.stringify(importPrivKey.error)); } } // Update our ancestor count & status await this.updateAncestorStatus(); // If we have no ancestors (i.e. if our tx already got confirmed), skip grabbing the ancestor transaction hex and return true early if (this.currentAncestorCount === 0) { return true; } console.log(`[RPC Wallet] Loading ${this.currentAncestorCount} transactions into local mempool, please wait... (this may take a little while)`); /* Update the tx chain */ // First, we need to get a list of unconfirmed transactions, to do this, we need the most recent utxo. let utxos = await this.getUTXOs(); // Then, while we have ancestors, lookup the transaction hex, and add it to the start of the unconfirmed transaction chain let nextTXID = utxos[0].txid; while (nextTXID) { // Grab the raw transaction hex for the txid let txHex = await this.rpcRequest('getrawtransaction', [nextTXID]); if (txHex.error && txHex.error !== null) { throw new Error('Error gathering raw transaction for (' + nextTXID + '): ' + JSON.stringify(txHex.error)); } // Add the raw tx hex to the start of the unconfirmed transactions list this.unconfirmedTransactions.unshift(txHex.result); this.unconfirmedTxids.unshift(nextTXID); // Then lookup to see if it is in the mempool let txMemInfo = await this.rpcRequest('getmempoolentry', [nextTXID]); if (txMemInfo.error && txMemInfo.error !== null) { throw new Error('Error gathering mempool entry for (' + nextTXID + '): ' + JSON.stringify(txMemInfo.error)); } // See if there are any parent txs that our tx "depends" on and need to be confirmed if (txMemInfo.result.depends.length > 0) { // Store the parent tx to search through next nextTXID = txMemInfo.result.depends[0]; } else { // If there are no transactions that ours depend on, then we can exit the loop nextTXID = undefined; } // Log every 50 added if (this.unconfirmedTransactions.length % 50 === 0) { console.log(`[RPC Wallet] Loaded ${this.unconfirmedTransactions.length}/${this.currentAncestorCount} transactions into local mempool so far...`); } } console.log(`[RPC Wallet] Loaded ${this.unconfirmedTransactions.length} transactions into local mempool!`); // Return true, signifying that the initialization was successful return true; } /** * Create fcoin "Peers" for all peers that the rpc full node as access to. * @async */ async connectToPeers() { // Initialize/wipe peers this.peers = []; // Request peers to connect to from the RPC node we have access to let getPeerInfo = await this.rpcRequest('getpeerinfo', []); if (getPeerInfo.error) { throw new Error(getPeerInfo.error); } console.log(`[RPC Wallet] Connecting to ${getPeerInfo.result.length} Peers...`); // For each of those peers, open up a connection for (let peerInfo of getPeerInfo.result) { // Create an fcoin peer let peerHost = peerInfo.addr; // Always rewrite the port. if (peerHost.includes(':')) { peerHost = peerHost.substring(0, peerHost.indexOf(':')); } // Set to test or mainnet ourselves. if (this.options.network && (this.options.network === 'mainnet' || this.options.network === 'livenet')) { peerHost = peerHost + ':7312'; } else if (this.options.network && this.options.network === 'regtest') { peerHost = peerHost + ':17412'; } else { peerHost = peerHost + ':17312'; } console.log(`peer host ${peerHost}`); let peer = new _Peer.default({ ip: peerHost, network: this.options.network }); // Start the connection attempt peer.connect(); // Add it to our peerrs array this.peers.push(peer); } // Wait for PEER_CONNECT_LENGTH in order to allow peers to initialize await new Promise((resolve, reject) => { setTimeout(() => { resolve(); }, PEER_CONNECT_LENGTH); }); // Count how many peers we have successfully connected to let connectedPeers = 0; for (let peer of this.peers) { if (peer.connected) { connectedPeers++; } } // Log the connected count console.log(`[RPC Wallet] Connected to ${connectedPeers} Peers!`); } /** * Destroy peers created in rebroadcastTransactions */ async destroyPeers() { // Destroy each `fcoin` peer for (let peer of this.peers) { peer.peer.destroy(); } if (this.peers.length > 0) { // Wait for PEER_DESTROY_LENGTH in order to allow peers to destroy and be cleared outt await new Promise((resolve, reject) => { setTimeout(() => { resolve(); }, PEER_DESTROY_LENGTH); }); } // Wipe out the array this.peers = []; } /** * Sign a message using the internal private key * @async * @param {String} message - The message you wish to have signed * @return {String} Returns the base64 version of the Signature */ async signMessage(message) { // Create the ECPair to sign with (this is the Private Key basically) let myECPair = _bitcoinjsLib.ECPair.fromWIF(this.wif, this.coin.network); let privateKeyBuffer = myECPair.privateKey; // Check if we are a compress privKey let compressed = myECPair.compressed || true; // Create the actual signature let signatureBuffer; try { signatureBuffer = (0, _bitcoinjsMessage.sign)(message, privateKeyBuffer, compressed, myECPair.network.messagePrefix); } catch (e) { throw new Error(e); } // Convert the signature to a base64 string let signature = signatureBuffer.toString('base64'); // Return the base64 signature return signature; } /** * Get the latest unspent transaction outputs * @async * @return {Array.<Object>} Returns an Array of UTXO's */ async getUTXOs() { // If we have a previous txo, then just return that one! if (this.previousTXOutput && this.previousTXOutput.amount > MIN_UTXO_AMOUNT) { return [this.previousTXOutput]; } // On the first run, log that we are requesting the transaction output. if (!this.initialUTXOLog) { console.log('[RPC Wallet] Grabbing initial transaction output to use, this may take a few seconds...'); this.initialUTXOLog = true; } // Request the list of unspent transactions const MIN_CONFIRMATIONS = 0; const MAX_CONFIRMATIONS = 9999999; let utxoRes = await this.rpcRequest('listunspent', [MIN_CONFIRMATIONS, MAX_CONFIRMATIONS, [this.publicAddress], true, { "minimumAmount": MIN_UTXO_AMOUNT, "maximumCount": 1 }]); // Throw if there was an error if (utxoRes.error && utxoRes.error !== null) { throw new Error('Unable to get unspent transactions for: ' + this.publicAddress + '\n' + JSON.stringify(utxoRes.error)); } // Pull out the utxos array let utxos = utxoRes.result; // Filter out transactions that don't meet our minimum UTXO value let filtered = utxos.filter(utxo => { if (utxo.amount >= MIN_UTXO_AMOUNT) { return true; } return false; }); // Check if we have no transaction outputs available to spend from, and throw an error if so if (filtered.length === 0) { console.log(utxoRes); throw new Error('No previous unspent output available! Please send some FLO to ' + this.publicAddress + ' and then try again!'); } // Sort by confirmations descending (highest conf first) filtered.sort((a, b) => { return b.confirmations - a.confirmations; }); // Return the filtered and sorted utxos return filtered; } /** * Create and broadcast a transaction containing the requested data as floData * @async * @param {String} floData - The data you wish to be placed into the `floData` of the transaction. * @return {String} Returns the TXID of the transaction if sent successfully */ async sendDataToChain(floData) { // Grab the unspent outputs for our address let utxos = await this.getUTXOs(); // Select the first input (since we have already sorted and filtered) let input = utxos[0]; // Calculate the minimum Transaction fee for our transaction by counting the size of the inputs, outputs, and floData let myTxFee = (this.options.txFeePerByte || TX_FEE_PER_BYTE) * (TX_AVG_BYTE_SIZE + (0, _util.varIntBuffer)(floData.length).toString('hex').length + Buffer.from(floData).length); // Create an output to send the funds to let output = {}; output[this.publicAddress] = parseFloat((input.amount - myTxFee).toFixed(8)); // Send the transaction let txid = await this.sendTX([input], output, floData); // Returns the TXID of the transaction if sent successfully return txid; } /** * Create and sign a transaction using bitcoinjs-lib and js-oip libraries * @param {Object} input [description] * @param {Object} outputs [description] * @param {String} floData [description] * @return {String} Returns the built hex string */ createAndSignTransaction(input, outputs, floData) { // Create a Bitcoinjs-lib transaction builder, and pass it the FLO network params let txb = new _FLOTransactionBuilder.default(this.coin.network); // Add our single input txb.addInput(input.txid, input.vout); // Add our output txb.addOutput(this.publicAddress, parseInt(outputs[this.publicAddress] * SAT_PER_FLO)); // Add our floData txb.setFloData(floData); // Sign our transaction using the local `flosigner` at `src/config/networks/flo/flosigner.js` txb.sign(0, _bitcoinjsLib.ECPair.fromWIF(this.wif, this.coin.network)); // Build the hex let builtHex; try { builtHex = txb.build().toHex(); } catch (err) { throw new Error(`Unable to build Transaction Hex!: ${err}`); } // Return the completed hex as a string return builtHex; } /** * Send a Transaction using the requested inputs, outputs, and floData * @async * @param {Array.<Object>} inputs - An array of utxo inputs to use to fund the transaction * @param {Object} outputs - An Object that contains addresses to output to as the key, and the amount to send as the value (i.e. `{ 'mypublicaddresshere': 1 }`) * @param {String} floData - The `floData` you wish to include in the transaction * @return {String} Returns the txid of the transaction if sent successfully */ async sendTX(inputs, outputs, floData) { // Perform validation checks on the floData being added to the chain if (typeof floData !== 'string') { throw new Error(`Data must be of type string. Got: ${typeof floData}`); } if (floData.length > _FLOTransaction.FLODATA_MAX_LEN) { throw new Error(`Error: 'floData' length exceeds ${_FLOTransaction.FLODATA_MAX_LEN} characters. Please send a smaller data package.`); } // Make sure that we don't have too many ancestors. If we do, then waits for some transactions to be confirmed. await this.checkAncestorCount(); // Check if transactions have been confirmed, and if so run the callback provided await this.checkForConfirmations(); // Create and sign the transaction hex let signedTXHex = this.createAndSignTransaction(inputs[0], outputs, floData); // Broadcast the transaction hex we created to the network let broadcastTX = await this.rpcRequest('sendrawtransaction', [signedTXHex]); // Check if there was an error broadcasting the transaction if (broadcastTX.error && broadcastTX.error !== null) { throw new Error('Error broadcasting tx: ' + signedTXHex + '\n' + JSON.stringify(broadcastTX.error)); } // Add our tx hex to the unconfirmed transactions array this.unconfirmedTransactions.push(signedTXHex); this.unconfirmedTxids.push(broadcastTX.result); // Add the tx we just sent to the Ancestor count await this.addAncestor(signedTXHex); // Set the new tx to be used as the next output. this.previousTXOutput = { txid: broadcastTX.result, amount: outputs[this.publicAddress], vout: 0 // Mark a timestamp for the last time we have sent a transaction, // this is to allow us to check if we should be running `checkAncestorCount` // inside of the `onConfirmationInterval` check }; this.lastTXTime = Date.now(); // Return the TXID of the transaction return broadcastTX.result; } /** * Get the current onConfirmation subscription count * @return {Integer} returns the count of pending onConfirmation subscriptions */ getConfirmationSubscriptionCount() { return Object.keys(this.onConfirmationSubscriptions).length; } /** * Subscribe a callback function to be run when all TXIDs are confirmed. * @param {Array<String>} response.txids - An Array of TXIDs * @param {OIPRecord} response.record - The OIPRecord that was published (to be returned to the Callback) * @param {function} options.onConfirmation - A function to run once the transaction recieves a confirmation that it was included in a block. * @param {String} options.onConfirmationRef - A reference string that should be returned in the onConfirmation function (useful for ID's). */ onConfirmation(_ref, options) { let txids = _ref.txids, record = _ref.record; // If we do not have an onConfirmation subscription, exit the function if (!options || !options.onConfirmation || typeof options.onConfirmation !== 'function') { return; } // Add this subscription to the tracked subscriptions this.onConfirmationSubscriptions[record.getTXID()] = { txids, record, options // If we do not already have a loop going to make sure confirmations get fired off, create one }; if (!this.onConfirmationInterval) { this.onConfirmationInterval = setInterval((async () => { // Only run the checkAncestorCount function IF if has been at least CONFIRMATION_CHECK_UPDATE_ANCESTORS_AFTER // since the last transaction was sent if (Date.now() - this.lastTXTime > CONFIRMATION_CHECK_UPDATE_ANCESTORS_AFTER && !this.checkingAncestorCount) { this.lastTXTime = Date.now(); await this.checkAncestorCount(true); } await this.checkForConfirmations(); }).bind(this), CONFIRMATION_CHECK_INTERVAL); } } /** * Check if any of the confirmation subscriptions should be run because TXIDs got confirmed * @return {Promise} Returns a promise that resolves once all of the available confirmation callbacks have been run */ async checkForConfirmations() { if (this.getConfirmationSubscriptionCount() === 0) { // Sanity check to clear out the interval if for whatever reason it currently exists. if (this.onConfirmationInterval) { clearInterval(this.onConfirmationInterval); this.onConfirmationInterval = undefined; } return; } // Note: Occasionally this will log a lower number than the Ancestor Count, this occurs // when a Multipart record is subscribed to using the onConfirmation subscription. console.log(`[RPC Wallet] Checking ${this.getConfirmationSubscriptionCount()} confirmation subscriptions...`); for (let subscription in this.onConfirmationSubscriptions) { let _this$onConfirmationS = this.onConfirmationSubscriptions[subscription], txids = _this$onConfirmationS.txids, record = _this$onConfirmationS.record, options = _this$onConfirmationS.options; // Check if we are still waiting to be confirmed let waitingForConfirmation = false; for (let txid of txids) { if (this.unconfirmedTxids.includes(txid)) { waitingForConfirmation = true; } } if (!waitingForConfirmation) { try { await options.onConfirmation(record, txids, options.onConfirmationRef); } catch (e) { console.error(`[RPC Wallet] Error when running onConfirmation function in subscription: `); console.error(e); } delete this.onConfirmationSubscriptions[subscription]; } } // Now that we have attempted to process confirmation subscriptions, see if we should immediately clear the setInterval if (this.getConfirmationSubscriptionCount() === 0 && this.onConfirmationInterval) { clearInterval(this.onConfirmationInterval); this.onConfirmationInterval = undefined; } } /** * Wait for all Confirmation Subscriptions to complete, then resolves the Promise * @return {Promise} Returns a promise that resolves once all subscribed Confirmation callbacks have completed */ async waitForConfirmations() { let subCount = this.getConfirmationSubscriptionCount(); let lastConfirmationTime = Date.now(); while (subCount > 0) { console.log(`[RPC Wallet] Waiting for ${Object.keys(this.onConfirmationSubscriptions).length} records to be confirmed! (onConfirmation subscription)`); await new Promise((resolve, reject) => { setTimeout(resolve, 10 * ONE_SECOND); }); await this.checkAncestorCount(true); await this.checkForConfirmations(); let tmpSubCount = subCount; subCount = this.getConfirmationSubscriptionCount(); if (tmpSubCount === subCount) { // Check if we have been waiting longer than CONFIRMATION_REBROADCAST_DELAY for a confirmation to happen if (Date.now() - lastConfirmationTime > CONFIRMATION_REBROADCAST_DELAY) { // If so, rebroadcast the pending transactions lastConfirmationTime = Date.now(); console.log(`[RPC Wallet] Rebroadcasting Transactions! There were no record confirmations within the past ${CONFIRMATION_REBROADCAST_DELAY / 1000} seconds.`); await this.rebroadcastTransactions(); } } else { // Update the last confirmation time lastConfirmationTime = Date.now(); } } console.log(`[RPC Wallet] No more confirmation subscriptions exist, all Records have been confirmed`); } } var _default = RPCWallet; exports.default = _default;