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