be stricter with response typing.
[electrum-server.git] / backends / libbitcoin / history.cpp
index 9cc5cd5..1379be4 100644 (file)
@@ -1,4 +1,5 @@
 #include <system_error>
+#include <boost/logic/tribool.hpp>
 
 #include <bitcoin/bitcoin.hpp>
 using namespace libbitcoin;
@@ -6,29 +7,90 @@ using namespace libbitcoin;
 #include <boost/python.hpp>
 namespace python = boost::python;
 
-#include "/home/genjix/python-bitcoin/src/primitive.h"
-
-typedef std::function<void (const std::error_code&)> finish_handler;
+#include "memory_buffer.hpp"
 
 namespace ph = std::placeholders;
 
+struct info_unit
+{
+    typedef std::vector<std::string> string_list;
+
+    // block_hash == null_hash if mempool tx
+    hash_digest tx_hash, block_hash;
+    string_list inputs, outputs;
+    size_t index;
+    uint64_t value;
+    size_t height;
+    uint64_t timestamp;
+    bool is_input;
+    // set to empty if unused
+    data_chunk raw_output_script;
+
+    // Convenient storage used by pool txs
+    message::output_point previous_output;
+};
+
+typedef std::shared_ptr<info_unit> info_unit_ptr;
+typedef std::vector<info_unit_ptr> info_unit_list;
+
+bool inputs_all_loaded(info_unit::string_list inputs)
+{
+    for (const auto& input: inputs)
+        if (input.empty())
+            return false;
+    return true;
+}
+
+struct payment_entry
+{
+    bool is_loaded()
+    {
+        if (!loaded_output)
+            return false;
+        if (input_exists && !loaded_input)
+            return false;
+        return true;
+    }
+
+    message::output_point outpoint;
+    info_unit_ptr loaded_output;
+    // indeterminate until we know whether this output has a spend
+    boost::tribool input_exists;
+    message::input_point inpoint;
+    // Ignore if input does not exist
+    info_unit_ptr loaded_input;
+};
+
+typedef std::shared_ptr<payment_entry> payment_entry_ptr;
+
+typedef std::vector<payment_entry_ptr> statement_entry;
+
+typedef std::function<
+    void (const std::error_code&, const statement_entry&,
+        const info_unit_list&)> finish_handler;
+
 class query_history
   : public std::enable_shared_from_this<query_history>
 {
 public:
-    query_history(async_service& service,
-        blockchain_ptr chain, transaction_pool_ptr txpool)
+    query_history(async_service& service, blockchain_ptr chain,
+        transaction_pool_ptr txpool, memory_buffer_ptr membuf)
       : strand_(service.get_service()), chain_(chain),
-        txpool_(txpool), stopped_(false)
+        txpool_(txpool), membuf_(membuf), stopped_(false)
     {
     }
 
     void start(const std::string& address, finish_handler handle_finish)
     {
-        address_.set_encoded(address);
+        if (!address_.set_encoded(address))
+        {
+            handle_finish(make_error_code(std::errc::address_not_available),
+                statement_entry(), info_unit_list());
+            return;
+        }
         handle_finish_ = handle_finish;
         chain_->fetch_outputs(address_,
-            strand_.wrap(std::bind(&query_history::start_loading,
+            strand_.wrap(std::bind(&query_history::check_membuf,
                 shared_from_this(), ph::_1, ph::_2)));
     }
 
@@ -40,39 +102,468 @@ private:
         stopped_ = true;
     }
 
+    bool stop_on_error(const std::error_code& ec)
+    {
+        if (ec)
+        {
+            handle_finish_(ec, statement_entry(), info_unit_list());
+            stop();
+        }
+        return stopped_;
+    }
+
+    void check_membuf(const std::error_code& ec,
+        const message::output_point_list& outpoints)
+    {
+        if (stop_on_error(ec))
+            return;
+        membuf_->check(outpoints, address_,
+            strand_.wrap(std::bind(&query_history::start_loading,
+                shared_from_this(), ph::_1, ph::_2, outpoints)));
+    }
+
     void start_loading(const std::error_code& ec,
+        const memory_buffer::check_result& membuf_result,
         const message::output_point_list& outpoints)
     {
-        handle_finish_(ec);
+        if (stop_on_error(ec))
+            return;
+        else if (outpoints.empty() && membuf_result.empty())
+        {
+            handle_finish_(std::error_code(),
+                statement_entry(), info_unit_list());
+            stop();
+            return;
+        }
+        for (auto outpoint: outpoints)
+        {
+            auto entry = std::make_shared<payment_entry>();
+            entry->outpoint = outpoint;
+            statement_.push_back(entry);
+            chain_->fetch_spend(outpoint,
+                strand_.wrap(std::bind(&query_history::load_spend,
+                    shared_from_this(), ph::_1, ph::_2, entry)));
+            load_tx_info(outpoint, entry, false);
+        }
+        for (const auto& item: membuf_result)
+        {
+            auto info = std::make_shared<info_unit>();
+            info->tx_hash = std::move(item.tx_hash);
+            info->index = item.index;
+            info->is_input = item.is_input;
+            info->timestamp = item.timestamp;
+            membuf_result_.push_back(info);
+            txpool_->fetch(item.tx_hash,
+                strand_.wrap(std::bind(&query_history::load_pool_tx,
+                    shared_from_this(), ph::_1, ph::_2, info)));
+        }
+    }
+
+    void load_spend(const std::error_code& ec,
+        const message::input_point inpoint, payment_entry_ptr entry)
+    {
+        // Need a custom self.stop_on_error(...) as a missing spend
+        // is not an error in this case.
+        if (ec && ec != error::unspent_output)
+            stop_on_error(ec);
+        if (stopped_)
+            return;
+
+        if (ec == error::unspent_output)
+        {
+            // This particular entry.output_point
+            // has not been spent yet
+            entry->input_exists = false;
+            finish_if_done();
+        }
+        else
+        {
+            // Has been spent
+            entry->input_exists = true;
+            entry->inpoint = inpoint;
+            load_tx_info(inpoint, entry, true);
+        }
+    }
+
+    bool find_set_value_from_prevout(info_unit_ptr info)
+    {
+        const hash_digest& prev_hash = info->previous_output.hash;
+        for (auto entry: statement_)
+        {
+            BITCOIN_ASSERT(entry->loaded_output);
+            if (entry->loaded_output->tx_hash == prev_hash)
+            {
+                info->value = entry->loaded_output->value;
+                return true;
+            }
+            else if (entry->input_exists &&
+                entry->loaded_input->tx_hash == prev_hash)
+            {
+                info->value = entry->loaded_output->value;
+                return true;
+            }
+        }
+        for (auto other_info: membuf_result_)
+            if (other_info->tx_hash == prev_hash)
+            {
+                info->value = other_info->value;
+                return true;
+            }
+        return false;
+    }
+
+    void finish_if_done()
+    {
+        for (auto entry: statement_)
+            if (!entry->is_loaded())
+                return;
+        // Finish up
+        for (auto entry: statement_)
+        {
+            if (entry->input_exists)
+            {
+                BITCOIN_ASSERT(entry->loaded_input && entry->loaded_output);
+                // value of the input is simply the inverse of
+                // the corresponding output
+                entry->loaded_input->value = entry->loaded_output->value;
+                // Unspent outputs have a raw_output_script field
+                // Blank this field as it isn't used
+                entry->loaded_output->raw_output_script.clear();
+            }
+            else
+            {
+                // Unspent outputs have a raw_output_script field
+            }
+        }
+        for (auto info: membuf_result_)
+        {
+            // Lookup prevout to set the value field
+            if (info->is_input)
+            {
+                bool set_prevout_value = find_set_value_from_prevout(info);
+                BITCOIN_ASSERT(set_prevout_value);
+            }
+        }
+        handle_finish_(std::error_code(), statement_, membuf_result_);
+        stop();
+    }
+
+    template <typename Point>
+    void load_tx_info(const Point& point, payment_entry_ptr entry,
+        bool is_input)
+    {
+        auto info = std::make_shared<info_unit>();
+        info->tx_hash = point.hash;
+        info->index = point.index;
+        info->is_input = is_input;
+        // Before loading the transaction, Stratum requires the hash
+        // of the parent block, so we load the block depth and then
+        // fetch the block header and hash it.
+        chain_->fetch_transaction_index(point.hash,
+            strand_.wrap(std::bind(&query_history::tx_index,
+                shared_from_this(), ph::_1, ph::_2, ph::_3, entry, info)));
+    }
+
+    void tx_index(const std::error_code& ec, size_t block_depth,
+        size_t offset, payment_entry_ptr entry, info_unit_ptr info)
+    {
+        if (stop_on_error(ec))
+            return;
+        info->height = block_depth;
+        // And now for the block hash
+        chain_->fetch_block_header(block_depth,
+            strand_.wrap(std::bind(&query_history::block_header,
+                shared_from_this(), ph::_1, ph::_2, entry, info)));
+    }
+
+    void block_header(const std::error_code& ec,
+        const message::block blk_head,
+        payment_entry_ptr entry, info_unit_ptr info)
+    {
+        if (stop_on_error(ec))
+            return;
+        info->timestamp = blk_head.timestamp;
+        info->block_hash = hash_block_header(blk_head);
+        // Now load the actual main transaction for this input or output
+        chain_->fetch_transaction(info->tx_hash,
+            strand_.wrap(std::bind(&query_history::load_chain_tx,
+                shared_from_this(), ph::_1, ph::_2, entry, info)));
+    }
+
+    void load_tx(const message::transaction& tx, info_unit_ptr info)
+    {
+        // List of output addresses
+        for (const auto& tx_out: tx.outputs)
+        {
+            payment_address addr;
+            // Attempt to extract address from output script
+            if (extract(addr, tx_out.output_script))
+                info->outputs.push_back(addr.encoded());
+            else
+                info->outputs.push_back("Unknown");
+        }
+        // For the inputs, we need the originator address which has to
+        // be looked up in the blockchain.
+        // Create list of empty strings and then populate it.
+        // Loading has finished when list is no longer all empty strings.
+        for (const auto& tx_in: tx.inputs)
+            info->inputs.push_back("");
+        // If this transaction was loaded for an input, then we already
+        // have a source address for at least one input.
+        if (info->is_input)
+        {
+            BITCOIN_ASSERT(info->index < info->inputs.size());
+            info->inputs[info->index] = address_.encoded();
+        }
+    }
+
+    void load_chain_tx(const std::error_code& ec,
+        const message::transaction& tx,
+        payment_entry_ptr entry, info_unit_ptr info)
+    {
+        if (stop_on_error(ec))
+            return;
+        load_tx(tx, info);
+        if (!info->is_input)
+        {
+            BITCOIN_ASSERT(info->index < tx.outputs.size());
+            const auto& our_output = tx.outputs[info->index];
+            info->value = our_output.value;
+            // Save serialised output script in case this output is unspent
+            info->raw_output_script = save_script(our_output.output_script);
+        }
+        // If all the inputs are loaded
+        if (inputs_all_loaded(info->inputs))
+        {
+            // We are the sole input
+            BITCOIN_ASSERT(info->is_input);
+            entry->loaded_input = info;
+            finish_if_done();
+        }
+
+        // *********
+        // fetch_input_txs
+
+        // Load the previous_output for every input so we can get
+        // the output address
+        for (size_t input_index = 0;
+            input_index < tx.inputs.size(); ++input_index)
+        {
+            if (info->is_input && info->index == input_index)
+                continue;
+            const auto& prevout = tx.inputs[input_index].previous_output;
+            chain_->fetch_transaction(prevout.hash,
+                strand_.wrap(std::bind(&query_history::load_input_chain_tx,
+                    shared_from_this(), ph::_1, ph::_2, prevout.index,
+                        entry, info, input_index)));
+        }
+    }
+
+    void load_pool_tx(const std::error_code& ec,
+        const message::transaction& tx, info_unit_ptr info)
+    {
+        if (stop_on_error(ec))
+            return;
+        // block_hash = mempool:5
+        // inputs (load from prevtx)
+        // outputs (load from tx)
+        // raw_output_script (load from tx)
+        // height is always None
+        // value (get from finish_if_done)
+        load_tx(tx, info);
+        if (info->is_input)
+        {
+            BITCOIN_ASSERT(info->index < tx.inputs.size());
+            info->previous_output = tx.inputs[info->index].previous_output;
+        }
+        else
+        {
+            const auto& our_output = tx.outputs[info->index];
+            info->value = our_output.value;
+            info->raw_output_script = save_script(our_output.output_script);
+        }
+        // If all the inputs are loaded
+        if (inputs_all_loaded(info->inputs))
+        {
+            // We are the sole input
+            BITCOIN_ASSERT(info->is_input);
+            // No more inputs left to load
+            // This info has finished loading
+            info->height = 0;
+            info->block_hash = null_hash;
+            finish_if_done();
+        }
+
+        // *********
+        // fetch_input_txs
+
+        // Load the previous_output for every input so we can get
+        // the output address
+        for (size_t input_index = 0;
+            input_index < tx.inputs.size(); ++input_index)
+        {
+            if (info->is_input && info->index == input_index)
+                continue;
+            const auto& prevout = tx.inputs[input_index].previous_output;
+            chain_->fetch_transaction(prevout.hash,
+                strand_.wrap(std::bind(&query_history::load_input_pool_tx,
+                    shared_from_this(), ph::_1, ph::_2, prevout.index,
+                        info, input_index)));
+        }
+    }
+
+    void load_input_tx(const message::transaction& tx, size_t output_index,
+        info_unit_ptr info, size_t input_index)
+    {
+        // For our input, we load the previous tx so we can get the
+        // corresponding output.
+        // We need the output to extract the address.
+        BITCOIN_ASSERT(output_index < tx.outputs.size());
+        const auto& out_script = tx.outputs[output_index].output_script;
+        payment_address addr;
+        BITCOIN_ASSERT(input_index < info->inputs.size());
+        if (extract(addr, out_script))
+            info->inputs[input_index] = addr.encoded();
+        else
+            info->inputs[input_index] = "Unknown";
+    }
+
+    void load_input_chain_tx(const std::error_code& ec,
+        const message::transaction& tx, size_t output_index,
+        payment_entry_ptr entry, info_unit_ptr info, size_t input_index)
+    {
+        if (stop_on_error(ec))
+            return;
+        load_input_tx(tx, output_index, info, input_index);
+        // If all the inputs are loaded, then we have finished loading
+        // the info for this input-output entry pair
+        if (inputs_all_loaded(info->inputs))
+        {
+            if (info->is_input)
+                entry->loaded_input = info;
+            else
+                entry->loaded_output = info;
+        }
+        finish_if_done();
+    }
+
+    void load_input_pool_tx(const std::error_code& ec,
+        const message::transaction& tx, size_t output_index,
+        info_unit_ptr info, size_t input_index)
+    {
+        if (stop_on_error(ec))
+            return;
+        load_input_tx(tx, output_index, info, input_index);
+        if (inputs_all_loaded(info->inputs))
+        {
+            // No more inputs left to load
+            // This info has finished loading
+            info->height = 0;
+            info->block_hash = null_hash;
+        }
+        finish_if_done();
     }
 
     io_service::strand strand_;
 
     blockchain_ptr chain_;
     transaction_pool_ptr txpool_;
+    memory_buffer_ptr membuf_;
     bool stopped_;
 
+    statement_entry statement_;
+    info_unit_list membuf_result_;
     payment_address address_;
     finish_handler handle_finish_;
 };
 
 typedef std::shared_ptr<query_history> query_history_ptr;
 
+void write_xputs_strings(std::stringstream& ss, const char* xput_name,
+    const info_unit::string_list& xputs)
+{
+    ss << '"' << xput_name << "\": [";
+    for (auto it = xputs.begin(); it != xputs.end(); ++it)
+    {
+        if (it != xputs.begin())
+            ss << ",";
+        ss << '"' << *it << '"';
+    }
+    ss << "]";
+}
+
+void write_info(std::string& json, info_unit_ptr info)
+{
+    std::stringstream ss;
+    ss << "{"
+        << "\"tx_hash\": \"" << pretty_hex(info->tx_hash) << "\","
+        << "\"index\": " << info->index << ","
+        // x for received, and -x for sent amounts
+        << "\"value\": " << (info->is_input ? "-" : "") << info->value << ","
+        << "\"timestamp\": " << info->timestamp << ","
+        << "\"is_input\": " << info->is_input << ",";
+    if (info->height == 0 && info->block_hash == null_hash)
+    {
+        ss << "\"block_hash\": \"mempool\","
+            << "\"height\": null,";
+    }
+    else
+    {
+        ss << "\"block_hash\": \"" << pretty_hex(info->block_hash) << "\","
+            << "\"height\": " << info->height << ",";
+    }
+    write_xputs_strings(ss, "inputs", info->inputs);
+    ss << ",";
+    write_xputs_strings(ss, "outputs", info->outputs);
+    if (!info->raw_output_script.empty())
+        ss << ","
+            << "\"raw_output_script\": \""
+            << pretty_hex(info->raw_output_script) << "\"";
+    ss << "}";
+    json += ss.str();
+}
+
 void keep_query_alive_proxy(const std::error_code& ec,
+    const statement_entry& statement, const info_unit_list& membuf_result,
     python::object handle_finish, query_history_ptr history)
 {
-    pyfunction<const std::error_code&> f(handle_finish);
-    f(ec);
+    std::string json = "[";
+    for (auto it = statement.begin(); it != statement.end(); ++it)
+    {
+        if (it != statement.begin())
+            json += ",";
+        auto entry = *it;
+        BITCOIN_ASSERT(entry->loaded_output);
+        write_info(json, entry->loaded_output);
+        if (entry->input_exists)
+        {
+            BITCOIN_ASSERT(entry->loaded_input);
+            json += ",";
+            write_info(json, entry->loaded_input);
+        }
+    }
+    // A bit of super glue
+    if (!statement.empty() && !membuf_result.empty())
+        json += ",";
+    for (auto it = membuf_result.begin(); it != membuf_result.end(); ++it)
+    {
+        if (it != membuf_result.begin())
+            json += ",";
+        write_info(json, *it);
+    }
+    json += "]";
+    pyfunction<const std::error_code&, const std::string&> f(handle_finish);
+    f(ec, json);
 }
 
 void payment_history(async_service_ptr service, blockchain_ptr chain,
-    transaction_pool_ptr txpool, const std::string& address,
-    python::object handle_finish)
+    transaction_pool_ptr txpool, memory_buffer_ptr membuf,
+    const std::string& address, python::object handle_finish)
 {
     query_history_ptr history =
-        std::make_shared<query_history>(*service, chain, txpool);
+        std::make_shared<query_history>(*service, chain, txpool, membuf);
     history->start(address,
-        std::bind(keep_query_alive_proxy, ph::_1,
+        std::bind(keep_query_alive_proxy, ph::_1, ph::_2, ph::_3,
             handle_finish, history));
 }