import axios from 'axios'
import { sign } from 'bitcoinjs-message'
import { ECPair } from 'bitcoinjs-lib'
import { floMainnet, floTestnet, floRegtest } from '../../config'
import FLOTransactionBuilder from '../flo/FLOTransactionBuilder'
import { FLODATA_MAX_LEN } from '../flo/FLOTransaction'
import Peer from '../flo/Peer'
import { varIntBuffer } from '../../util'
// 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 = 0.0001
// Prevent chaining over ancestor limit
const MAX_MEMPOOL_ANCESTORS = 1250
const MAX_MEMPOOL_ANCESTOR_SIZE = 1.75 * ONE_MB
// Timer lengths used to track and fix the Ancestor chain
const UPDATE_ANCESTOR_STATUS = 1 * ONE_SECOND
const REPAIR_ANCESTORS_AFTER = 1 * ONE_MINUTE
const PEER_CONNECT_LENGTH = 2 * ONE_SECOND
const REBROADCAST_LENGTH = 10 * ONE_SECOND
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.create({
auth: {
username: this.options.rpc.username,
password: this.options.rpc.password
},
validateStatus: function (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 = floMainnet
} else if (this.options.network === 'testnet') {
this.coin = floTestnet
} else if (this.options.network === 'regtest') {
this.coin = floRegtest
}
// Store information about our tx chain and the previous tx output
this.unconfirmedTransactions = []
this.previousTXOutput = undefined
// 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
}
/**
* 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
}
}
/**
* 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...')
// 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 + ')')
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++
}
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
*/
addAncestor (hex) {
// Increase the ancestor count
this.currentAncestorCount++
// Increase the ancestor size (byte length)
this.currentAncestorSize += Buffer.from(hex, 'hex').length
// Log every 50
if (this.currentAncestorCount % 50 === 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 () {
// 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)
while (this.currentAncestorCount >= MAX_MEMPOOL_ANCESTORS || this.currentAncestorSize >= MAX_MEMPOOL_ANCESTOR_SIZE) {
// 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
}
// 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() }
// Check to see how many transactions got confirmed
if (startAncestorCount >= MAX_MEMPOOL_ANCESTORS || startAncestorSize >= MAX_MEMPOOL_ANCESTOR_SIZE) {
console.log(`[RPC Wallet] ${numberConfirmed} Transactions Confirmed! (${((startAncestorSize - this.currentAncestorSize) / ONE_MB).toFixed(2)} MB)`)
// 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 () {
// Announce that we are starting
console.log(`[RPC Wallet] Announcing ${this.unconfirmedTransactions.length} transactions to Peers...`)
// Connect to Peers to use for the rebroadcast
await this.connectToPeers()
// Announce all of our local unconfirmed transactions
let numAnnounced = 0
for (let txHex of this.unconfirmedTransactions) {
// Loop through all peers and announce TX individually to each (if we have connected to them\
for (let peer of this.peers) {
if (peer.connected) { await peer.announceTX(txHex) }
}
// Log every 50 transactions
numAnnounced++
if (numAnnounced % 50 === 0) { console.log(`[RPC Wallet] Announced ${numAnnounced}/${this.unconfirmedTransactions.length} transactions so far...`) }
}
console.log(`[RPC Wallet] Announced ${numAnnounced} transactions!`)
// 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) })
// Destroy the peers we connected too
this.destroyPeers()
}
/**
* 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)
// 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 {
peerHost = peerHost + ':17312'
}
console.log(`peer host ${peerHost}`)
let peer = new Peer({ 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
*/
destroyPeers () {
// Destroy each `fcoin` peer
for (let peer of this.peers) {
peer.peer.destroy()
}
// 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 = 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 = 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] ])
// 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) { 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 + 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(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, 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 > FLODATA_MAX_LEN) { throw new Error(`Error: 'floData' length exceeds ${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()
// 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 the tx we just sent to the Ancestor count
this.addAncestor(signedTXHex)
// Add our tx hex to the unconfirmed transactions array
this.unconfirmedTransactions.push(signedTXHex)
// Set the new tx to be used as the next output.
this.previousTXOutput = {
txid: broadcastTX.result,
amount: outputs[this.publicAddress],
vout: 0
}
// Return the TXID of the transaction
return broadcastTX.result
}
}
export default RPCWallet