improve get_history (clearer + better error checking)
[electrum-server.git] / backends / libbitcoin / history.py
1 import bitcoin
2 from bitcoin import bind, _1, _2, _3
3 import threading
4
5 class PaymentEntry:
6
7     def __init__(self, output_point):
8         self.lock = threading.Lock()
9         self.output_point = output_point
10         self.output_loaded = None
11         self.input_point = None
12         self.input_loaded = None
13         self.raw_output_script = None
14
15     def is_loaded(self):
16         with self.lock:
17             if self.output_loaded is None:
18                 return False
19             elif self.has_input() and self.input_loaded is None:
20                 return False
21         return True
22
23     def has_input(self):
24         return self.input_point is not False
25
26 class History:
27
28     def __init__(self, chain):
29         self.chain = chain
30         self.lock = threading.Lock()
31         self.statement = []
32         self._stopped = False
33
34     def start(self, address, handle_finish):
35         self.address = address
36         self.handle_finish = handle_finish
37
38         address = bitcoin.payment_address(address)
39         # To begin we fetch all the outputs (payments in)
40         # associated with this address
41         self.chain.fetch_outputs(address, self.start_loading)
42
43     def stop(self):
44         with self.lock:
45             assert self._stopped == False
46             self._stopped = True
47
48     def stopped(self):
49         with self.lock:
50             return self._stopped
51
52     def stop_on_error(self, ec):
53         if ec:
54             self.handle_finish(None)
55             self.stop()
56         return self.stopped()
57
58     def start_loading(self, ec, output_points):
59         if self.stop_on_error(ec):
60             return
61         # Create a bunch of entry lines which are outputs and
62         # then their corresponding input (if it exists)
63         for outpoint in output_points:
64             entry = PaymentEntry(outpoint)
65             with self.lock:
66                 self.statement.append(entry)
67             # Attempt to fetch the spend of this output
68             self.chain.fetch_spend(outpoint,
69                 bind(self.load_spend, _1, _2, entry))
70             self.load_tx_info(outpoint, entry, False)
71
72     def load_spend(self, ec, inpoint, entry):
73         # Need a custom self.stop_on_error(...) as a missing spend
74         # is not an error in this case.
75         if ec and ec != bitcoin.error.missing_object:
76             self.stop()
77         if self.stopped():
78             return
79         with entry.lock:
80             if ec == bitcoin.error.missing_object:
81                 # This particular entry.output_point
82                 # has not been spent yet
83                 entry.input_point = False
84             else:
85                 entry.input_point = inpoint
86         if ec == bitcoin.error.missing_object:
87             # Attempt to stop if all the info for the inputs and outputs
88             # has been loaded.
89             self.finish_if_done()
90         else:
91             # We still have to load at least one more payment outwards
92             self.load_tx_info(inpoint, entry, True)
93
94     def finish_if_done(self):
95         with self.lock:
96             # Still have more entries to finish loading, so return
97             if any(not entry.is_loaded() for entry in self.statement):
98                 return
99         # Whole operation completed successfully! Finish up.
100         result = []
101         for entry in self.statement:
102             if entry.input_point:
103                 # value of the input is simply the inverse of
104                 # the corresponding output
105                 entry.input_loaded["value"] = -entry.output_loaded["value"]
106                 # output should come before the input as it's chronological
107                 result.append(entry.output_loaded)
108                 result.append(entry.input_loaded)
109             else:
110                 # Unspent outputs have a raw_output_script field
111                 assert entry.raw_output_script is not None
112                 entry.output_loaded["raw_output_script"] = \
113                     entry.raw_output_script
114                 result.append(entry.output_loaded)
115         self.handle_finish(result)
116         self.stop()
117
118     def load_tx_info(self, point, entry, is_input):
119         info = {}
120         info["tx_hash"] = str(point.hash)
121         info["index"] = point.index
122         info["is_input"] = 1 if is_input else 0
123         # Before loading the transaction, Stratum requires the hash
124         # of the parent block, so we load the block depth and then
125         # fetch the block header and hash it.
126         self.chain.fetch_transaction_index(point.hash,
127             bind(self.tx_index, _1, _2, _3, entry, info))
128
129     def tx_index(self, ec, block_depth, offset, entry, info):
130         if self.stop_on_error(ec):
131             return
132         info["height"] = block_depth
133         # And now for the block hash
134         self.chain.fetch_block_header_by_depth(block_depth,
135             bind(self.block_header, _1, _2, entry, info))
136
137     def block_header(self, ec, blk_head, entry, info):
138         if self.stop_on_error(ec):
139             return
140         info["timestamp"] = blk_head.timestamp
141         info["block_hash"] = str(bitcoin.hash_block_header(blk_head))
142         tx_hash = bitcoin.hash_digest(info["tx_hash"])
143         # Now load the actual main transaction for this input or output
144         self.chain.fetch_transaction(tx_hash,
145             bind(self.load_tx, _1, _2, entry, info))
146
147     def load_tx(self, ec, tx, entry, info):
148         if self.stop_on_error(ec):
149             return
150         # List of output addresses
151         outputs = []
152         for tx_out in tx.outputs:
153             address = bitcoin.payment_address()
154             # Attempt to extract address from output script
155             if address.extract(tx_out.output_script):
156                 outputs.append(address.encoded())
157             else:
158                 # ... otherwise append "Unknown"
159                 outputs.append("Unknown")
160         info["outputs"] = outputs
161         # For the inputs, we need the originator address which has to
162         # be looked up in the blockchain.
163         # Create list of Nones and then populate it.
164         # Loading has finished when list is no longer all None.
165         info["inputs"] = [None for i in tx.inputs]
166         # If this transaction was loaded for an input, then we already
167         # have a source address for at least one input.
168         if info["is_input"] == 1:
169             info["inputs"][info["index"]] = self.address
170         else:
171             our_output = tx.outputs[info["index"]]
172             info["value"] = our_output.value
173             # Save serialised output script in case this output is unspent
174             with entry.lock:
175                 entry.raw_output_script = \
176                     str(bitcoin.save_script(our_output.output_script))
177         # If all the inputs are loaded
178         if self.inputs_all_loaded(info["inputs"]):
179             # We are the sole input
180             assert(info["is_input"] == 1)
181             with entry.lock:
182                 entry.input_loaded = info
183             self.finish_if_done()
184         # Load the previous_output for every input so we can get
185         # the output address
186         for input_index, tx_input in enumerate(tx.inputs):
187             if info["is_input"] == 1 and info["index"] == input_index:
188                 continue
189             prevout = tx_input.previous_output
190             self.chain.fetch_transaction(prevout.hash,
191                 bind(self.load_input_tx, _1, _2,
192                      prevout.index, entry, info, input_index))
193
194     def inputs_all_loaded(self, info_inputs):
195         return not [empty_in for empty_in in info_inputs if empty_in is None]
196
197     def load_input_tx(self, ec, tx, output_index, entry, info, input_index):
198         if self.stop_on_error(ec):
199             return
200         # For our input, we load the previous tx so we can get the
201         # corresponding output.
202         # We need the output to extract the address.
203         script = tx.outputs[output_index].output_script
204         address = bitcoin.payment_address()
205         if address.extract(script):
206             info["inputs"][input_index] = address.encoded()
207         else:
208             info["inputs"][input_index] = "Unknown"
209         # If all the inputs are loaded, then we have finished loading
210         # the info for this input-output entry pair
211         if self.inputs_all_loaded(info["inputs"]):
212             with entry.lock:
213                 if info["is_input"] == 1:
214                     entry.input_loaded = info
215                 else:
216                     entry.output_loaded = info
217         self.finish_if_done()
218
219 if __name__ == "__main__":
220     def blockchain_started(ec, chain):
221         print "Blockchain initialisation:", ec
222     def finish(result):
223         print result
224
225     service = bitcoin.async_service(1)
226     prefix = "/home/genjix/libbitcoin/database"
227     chain = bitcoin.bdb_blockchain(service, prefix, blockchain_started)
228     address = "1Pbn3DLXfjqF1fFV9YPdvpvyzejZwkHhZE"
229     print "Looking up", address
230     h = History(chain)
231     h.start(address, finish)
232     raw_input()
233     print "Stopping..."
234