BUGFIX: correctly handle unspent outputs
[electrum-server.git] / modules / python_bitcoin / composed.py
1 import bitcoin
2 import threading
3 import time
4
5 class ExpiryQueue(threading.Thread):
6
7     def __init__(self):
8         self.lock = threading.Lock()
9         self.items = []
10         threading.Thread.__init__(self)
11         self.daemon = True
12
13     def run(self):
14         # Garbage collection
15         while True:
16             with self.lock:
17                 self.items = [i for i in self.items if not i.stopped()]
18             time.sleep(0.1)
19
20     def add(self, item):
21         with self.lock:
22             self.items.append(item)
23
24 expiry_queue = ExpiryQueue()
25
26 class StatementLine:
27
28     def __init__(self, output_point):
29         self.lock = threading.Lock()
30         self.output_point = output_point
31         self.output_loaded = None
32         self.input_point = None
33         self.input_loaded = None
34
35     def is_loaded(self):
36         with self.lock:
37             if self.output_loaded is None:
38                 return False
39             elif (self.input_point is not False and
40                   self.input_loaded is None):
41                 return False
42         return True
43
44 class PaymentHistory:
45
46     def __init__(self, chain):
47         self.chain = chain
48         self.lock = threading.Lock()
49         self.statement = []
50         self._stopped = False
51
52     def run(self, address, handle_finish):
53         self.address = address
54         self.handle_finish = handle_finish
55
56         pubkey_hash = bitcoin.address_to_short_hash(address)
57         self.chain.fetch_outputs(pubkey_hash, self.start_loading)
58
59     def start_loading(self, ec, output_points):
60         with self.lock:
61             for outpoint in output_points:
62                 statement_line = StatementLine(outpoint)
63                 self.statement.append(statement_line)
64                 self.chain.fetch_spend(outpoint,
65                     bitcoin.bind(self.load_spend,
66                         bitcoin._1, bitcoin._2, statement_line))
67                 self.load_tx_info(outpoint, statement_line, False)
68
69     def load_spend(self, ec, inpoint, statement_line):
70         with statement_line.lock:
71             if ec:
72                 statement_line.input_point = False
73             else:
74                 statement_line.input_point = inpoint
75         self.finish_if_done()
76         if not ec:
77             self.load_tx_info(inpoint, statement_line, True)
78
79     def finish_if_done(self):
80         with self.lock:
81             if any(not line.is_loaded() for line in self.statement):
82                 return
83         result = []
84         for line in self.statement:
85             if line.input_point:
86                 line.input_loaded["value"] = -line.output_loaded["value"]
87                 result.append(line.input_loaded)
88             result.append(line.output_loaded)
89         self.handle_finish(result)
90         self.stop()
91
92     def stop(self):
93         with self.lock:
94             self._stopped = True
95
96     def stopped(self):
97         with self.lock:
98             return self._stopped
99
100     def load_tx_info(self, point, statement_line, is_input):
101         info = {}
102         info["tx_hash"] = str(point.hash)
103         info["pos"] = point.index
104         info["is_in"] = 1 if is_input else 0
105         self.chain.fetch_transaction_index(point.hash,
106             bitcoin.bind(self.tx_index, bitcoin._1, bitcoin._2, bitcoin._3,
107                 statement_line, info))
108
109     def tx_index(self, ec, block_depth, offset, statement_line, info):
110         info["height"] = block_depth
111         self.chain.fetch_block_header_by_depth(block_depth,
112             bitcoin.bind(self.block_header, bitcoin._1, bitcoin._2,
113                 statement_line, info))
114
115     def block_header(self, ec, blk_head, statement_line, info):
116         info["time"] = blk_head.timestamp
117         info["blk_hash"] = str(bitcoin.hash_block_header(blk_head))
118         tx_hash = bitcoin.hash_digest(info["tx_hash"])
119         self.chain.fetch_transaction(tx_hash,
120             bitcoin.bind(self.load_tx, bitcoin._1, bitcoin._2,
121                 statement_line, info))
122
123     def load_tx(self, ec, tx, statement_line, info):
124         outputs = []
125         for tx_out in tx.outputs:
126             script = tx_out.output_script
127             if script.type() == bitcoin.payment_type.pubkey_hash:
128                 pkh = bitcoin.short_hash(str(script.operations()[2].data))
129                 outputs.append(bitcoin.public_key_hash_to_address(pkh))
130             else:
131                 outputs.append("Unknown")
132         info["outputs"] = outputs
133         info["inputs"] = [None for i in range(len(tx.inputs))]
134         if info["is_in"] == 1:
135             info["inputs"][info["pos"]] = self.address
136         else:
137             info["value"] = tx.outputs[info["pos"]].value
138         if not [empty_in for empty_in in info["inputs"] if empty_in is None]:
139             # We have the sole input
140             assert(info["is_in"] == 1)
141             with statement_line.lock:
142                 statement_line.input_loaded = info
143             self.finish_if_done()
144         for tx_idx, tx_in in enumerate(tx.inputs):
145             if info["is_in"] == 1 and info["pos"] == tx_idx:
146                 continue
147             self.chain.fetch_transaction(tx_in.previous_output.hash,
148                 bitcoin.bind(self.load_input, bitcoin._1, bitcoin._2,
149                     tx_in.previous_output.index, statement_line, info, tx_idx))
150
151     def load_input(self, ec, tx, index, statement_line, info, inputs_index):
152         script = tx.outputs[index].output_script
153         if script.type() == bitcoin.payment_type.pubkey_hash:
154             pkh = bitcoin.short_hash(str(script.operations()[2].data))
155             info["inputs"][inputs_index] = \
156                 bitcoin.public_key_hash_to_address(pkh)
157         else:
158             info["inputs"][inputs_index] = "Unknown"
159         if not [empty_in for empty_in in info["inputs"] if empty_in is None]:
160             with statement_line.lock:
161                 if info["is_in"] == 1:
162                     statement_line.input_loaded = info
163                 else:
164                     statement_line.output_loaded = info
165         self.finish_if_done()
166
167 def payment_history(chain, address, handle_finish):
168     ph = PaymentHistory(chain)
169     expiry_queue.add(ph)
170     ph.run(address, handle_finish)
171
172 if __name__ == "__main__":
173     def finish(result):
174         print result
175     def last(ec, depth):
176         print "D:", depth
177
178     service = bitcoin.async_service(1)
179     prefix = "/home/genjix/libbitcoin/database"
180     chain = bitcoin.bdb_blockchain(service, prefix)
181     chain.fetch_last_depth(last)
182     address = "1Pbn3DLXfjqF1fFV9YPdvpvyzejZwkHhZE"
183     print "Looking up", address
184     payment_history(chain, address, finish)
185     raw_input()
186