Projects : gbw-node : gbw-node_genesis
1 | #!/usr/bin/python2 |
2 | # J. Welsh, December 2019 |
3 | |
4 | from os import getenv, open as os_open, O_RDONLY, O_WRONLY, mkdir, mkfifo, read, write, close, stat |
5 | from stat import S_ISDIR, S_ISFIFO |
6 | from sys import argv, stdin, stdout, stderr, exit |
7 | from socket import socket |
8 | from threading import Thread, Event |
9 | from binascii import a2b_hex, b2a_hex |
10 | from base64 import b64encode |
11 | from struct import Struct |
12 | from hashlib import sha256 as _sha256 |
13 | from decimal import Decimal |
14 | from inspect import getdoc |
15 | from getpass import getpass |
16 | import errno |
17 | import signal |
18 | import string |
19 | import json |
20 | import sqlite3 |
21 | from sqlite3 import IntegrityError |
22 | |
23 | # Safety level: scanning stops this many blocks behind tip |
24 | CONFIRMS = 6 |
25 | |
26 | # There's no provision for handling forks/reorgs. In the event of one deeper than CONFIRMS, a heavy workaround would be: |
27 | # $ sqlite3 ~/.gbw/db |
28 | # sqlite> DELETE FROM output; |
29 | # sqlite> DELETE FROM input; |
30 | # sqlite> DELETE FROM tx; |
31 | # sqlite> .exit |
32 | # $ gbw-node reset |
33 | # $ gbw-node scan |
34 | |
35 | gbw_home = getenv('HOME') + '/.gbw' |
36 | bitcoin_conf_path = getenv('HOME') + '/.bitcoin/bitcoin.conf' |
37 | |
38 | # Further knobs in main() for database tuning. |
39 | db = None |
40 | |
41 | b2lx = lambda b: b2a_hex(b[::-1]) |
42 | lx2b = lambda x: a2b_hex(x)[::-1] |
43 | |
44 | COIN = 10**8 |
45 | |
46 | def format_coin(v): |
47 | neg = False |
48 | if v < 0: |
49 | v = -v |
50 | neg = True |
51 | s = '%d.%08d' % divmod(v, COIN) |
52 | if neg: |
53 | return '-' + s |
54 | return s |
55 | |
56 | u16 = Struct('<H') |
57 | u32 = Struct('<I') |
58 | u64 = Struct('<Q') |
59 | s64 = Struct('<q') |
60 | unpack_u16 = u16.unpack |
61 | unpack_u32 = u32.unpack |
62 | unpack_u64 = u64.unpack |
63 | unpack_s64 = s64.unpack |
64 | unpack_header = Struct('<i32s32sIII').unpack |
65 | unpack_outpoint = Struct('<32sI').unpack |
66 | |
67 | def sha256(v): |
68 | return _sha256(v).digest() |
69 | |
70 | def sha256d(v): |
71 | return _sha256(_sha256(v).digest()).digest() |
72 | |
73 | class Conflict(ValueError): |
74 | pass |
75 | |
76 | def read_all(fd): |
77 | parts = [] |
78 | while True: |
79 | part = read(fd, 65536) |
80 | if len(part) == 0: |
81 | break |
82 | parts.append(part) |
83 | return ''.join(parts) |
84 | |
85 | def require_dir(path): |
86 | try: |
87 | mkdir(path) |
88 | except OSError, e: |
89 | if e.errno != errno.EEXIST: |
90 | raise |
91 | if not S_ISDIR(stat(path).st_mode): |
92 | die('not a directory: %r' % path) |
93 | |
94 | def require_fifo(path): |
95 | try: |
96 | mkfifo(path) |
97 | except OSError, e: |
98 | if e.errno != errno.EEXIST: |
99 | raise |
100 | if not S_ISFIFO(stat(path).st_mode): |
101 | die('not a fifo: %r' % path) |
102 | |
103 | ################################################## |
104 | # RPC client |
105 | |
106 | class JSONRPCError(Exception): |
107 | "Error returned in JSON-RPC response" |
108 | |
109 | def __init__(self, error): |
110 | super(JSONRPCError, self).__init__(error['code'], error['message']) |
111 | |
112 | def __str__(self): |
113 | return 'code: {}, message: {}'.format(*self.args) |
114 | |
115 | bitcoin_conf = None |
116 | def require_conf(): |
117 | global bitcoin_conf |
118 | if bitcoin_conf is None: |
119 | bitcoin_conf = {} |
120 | with open(bitcoin_conf_path) as f: |
121 | for line in f: |
122 | line = line.split('#', 1)[0].rstrip() |
123 | if not line: |
124 | continue |
125 | k, v = line.split('=', 1) |
126 | bitcoin_conf[k.strip()] = v.lstrip() |
127 | |
128 | def rpc(method, *args): |
129 | require_conf() |
130 | host = bitcoin_conf.get('rpcconnect', '127.0.0.1') |
131 | port = int(bitcoin_conf.get('rpcport', 8332)) |
132 | auth = 'Basic ' + b64encode('%s:%s' % ( |
133 | bitcoin_conf.get('rpcuser', ''), |
134 | bitcoin_conf.get('rpcpassword', ''))) |
135 | payload = json.dumps({'method': method, 'params': args}) |
136 | headers = [ |
137 | ('Host', host), |
138 | ('Content-Type', 'application/json'), |
139 | ('Content-Length', len(payload)), |
140 | ('Connection', 'close'), |
141 | ('Authorization', auth), |
142 | ] |
143 | msg = 'POST / HTTP/1.1\r\n%s\r\n\r\n%s' % ('\r\n'.join('%s: %s' % kv for kv in headers), payload) |
144 | sock = socket() |
145 | sock.connect((host, port)) |
146 | sock.sendall(msg) |
147 | response = read_all(sock.fileno()) |
148 | sock.close() |
149 | headers, payload = response.split('\r\n\r\n', 1) |
150 | r = json.loads(payload, parse_float=Decimal) |
151 | if r['error'] is not None: |
152 | raise JSONRPCError(r['error']) |
153 | return r['result'] |
154 | |
155 | ################################################# |
156 | # Bitcoin data parsing |
157 | |
158 | # "load" functions take a memoryview and return the object and number of bytes consumed. |
159 | |
160 | def load_compactsize(v): |
161 | # serialize.h WriteCompactSize |
162 | size = ord(v[0]) |
163 | if size < 253: |
164 | return size, 1 |
165 | elif size == 253: |
166 | return unpack_u16(v[1:3])[0], 3 |
167 | elif size == 254: |
168 | return unpack_u32(v[1:5])[0], 5 |
169 | else: |
170 | return unpack_u64(v[1:9])[0], 9 |
171 | |
172 | def load_string(v): |
173 | # serialize.h Serialize, std::basic_string and CScript overloads |
174 | n, i = load_compactsize(v) |
175 | return v[i:i+n].tobytes(), i+n |
176 | |
177 | def vector_loader(load_element): |
178 | # serialize.h Serialize_impl |
179 | def load_vector(v): |
180 | n, i = load_compactsize(v) |
181 | r = [None]*n |
182 | for elem in xrange(n): |
183 | r[elem], delta = load_element(v[i:]) |
184 | i += delta |
185 | return r, i |
186 | return load_vector |
187 | |
188 | def load_txin(v): |
189 | # main.h CTxIn |
190 | i = 36 |
191 | txid, pos = unpack_outpoint(v[:i]) |
192 | scriptsig, delta = load_string(v[i:]) |
193 | i += delta |
194 | i += 4 # skipping sequence |
195 | return (txid, pos, scriptsig), i |
196 | |
197 | load_txins = vector_loader(load_txin) |
198 | |
199 | def load_txout(v): |
200 | # main.h CTxOut |
201 | i = 8 |
202 | value, = unpack_s64(v[:i]) |
203 | scriptpubkey, delta = load_string(v[i:]) |
204 | return (value, scriptpubkey), i+delta |
205 | |
206 | load_txouts = vector_loader(load_txout) |
207 | |
208 | def load_transaction(v): |
209 | # main.h CTransaction |
210 | i = 4 # skipping version |
211 | txins, delta = load_txins(v[i:]) |
212 | i += delta |
213 | txouts, delta = load_txouts(v[i:]) |
214 | i += delta |
215 | i += 4 # skipping locktime |
216 | hash = sha256d(v[:i]) |
217 | return (hash, i, txins, txouts), i |
218 | |
219 | load_transactions = vector_loader(load_transaction) |
220 | |
221 | def load_block(v): |
222 | # main.h CBlock |
223 | i = 80 |
224 | head = v[:i] |
225 | version, prev, root, time, target, nonce = unpack_header(head) |
226 | hash = sha256d(head) |
227 | txs, delta = load_transactions(v[i:]) |
228 | return (hash, prev, time, target, txs), i+delta |
229 | |
230 | def out_script_address(s): |
231 | # Standard P2PKH script: OP_DUP OP_HASH160 20 ... OP_EQUALVERIFY OP_CHECKSIG |
232 | if len(s) == 25 and s[:3] == '\x76\xA9\x14' and s[23:] == '\x88\xAC': |
233 | return s[3:23] |
234 | return None |
235 | |
236 | getblock_thread = None |
237 | getblock_done = Event() |
238 | getblock_result = None |
239 | def getblock_reader(pipe): |
240 | global getblock_result |
241 | while True: |
242 | fd = os_open(pipe, O_RDONLY) |
243 | getblock_result = read_all(fd) |
244 | getblock_done.set() |
245 | close(fd) |
246 | |
247 | def getblock(height): |
248 | global getblock_thread |
249 | pipe = gbw_home + '/blockpipe' |
250 | if getblock_thread is None: |
251 | require_fifo(pipe) |
252 | getblock_thread = Thread(target=getblock_reader, args=(pipe,)) |
253 | getblock_thread.daemon = True |
254 | getblock_thread.start() |
255 | if not rpc('dumpblock', height, pipe): |
256 | raise ValueError('dumpblock returned false') |
257 | getblock_done.wait() |
258 | getblock_done.clear() |
259 | return getblock_result |
260 | |
261 | ################################################## |
262 | # Base58 |
263 | |
264 | base58_alphabet = (string.digits + string.uppercase + string.lowercase).translate(None, '0OIl') |
265 | base58_inverse = [None]*256 |
266 | def init_base58_inverse(): |
267 | for index, character in enumerate(base58_alphabet): |
268 | base58_inverse[ord(character)] = index |
269 | init_base58_inverse() |
270 | |
271 | def bytes_to_int(b): |
272 | "Convert big-endian byte sequence to unsigned integer" |
273 | i = 0 |
274 | for byte in b: |
275 | i = (i << 8) + ord(byte) |
276 | return i |
277 | |
278 | def b2a_base58check(data): |
279 | data += sha256d(data)[:4] |
280 | |
281 | leading_zeros = 0 |
282 | for b in data: |
283 | if b != '\x00': |
284 | break |
285 | leading_zeros += 1 |
286 | |
287 | data_num = bytes_to_int(data) |
288 | |
289 | digits = [] |
290 | while data_num: |
291 | data_num, digit = divmod(data_num, 58) |
292 | digits.append(digit) |
293 | digits.extend([0] * leading_zeros) |
294 | |
295 | return ''.join(base58_alphabet[digit] for digit in reversed(digits)) |
296 | |
297 | class Base58Error(ValueError): |
298 | pass |
299 | |
300 | class BadDigit(Base58Error): |
301 | pass |
302 | |
303 | class BadChecksum(Base58Error): |
304 | pass |
305 | |
306 | def a2b_base58(data): |
307 | digits = [base58_inverse[ord(b)] for b in data] |
308 | if None in digits: |
309 | raise BadDigit |
310 | |
311 | leading_zeros = 0 |
312 | for digit in digits: |
313 | if digit != 0: |
314 | break |
315 | leading_zeros += 1 |
316 | |
317 | data_num = 0 |
318 | for digit in digits: |
319 | data_num = 58*data_num + digit |
320 | |
321 | data_bytes = [] |
322 | while data_num: |
323 | data_bytes.append(data_num & 0xFF) |
324 | data_num = data_num >> 8 |
325 | data_bytes.extend([0] * leading_zeros) |
326 | |
327 | return ''.join(chr(b) for b in reversed(data_bytes)) |
328 | |
329 | def a2b_base58check(data): |
330 | data = a2b_base58(data) |
331 | payload = data[:-4] |
332 | check = data[-4:] |
333 | if check != sha256d(payload)[:4]: |
334 | raise BadChecksum |
335 | return payload |
336 | |
337 | class BadAddressLength(ValueError): |
338 | pass |
339 | |
340 | class BadAddressVersion(ValueError): |
341 | pass |
342 | |
343 | def parse_address(a): |
344 | b = a2b_base58check(a) |
345 | if len(b) != 21: |
346 | raise BadAddressLength |
347 | if b[0] != '\x00': |
348 | raise BadAddressVersion(ord(b[0])) |
349 | return b[1:] |
350 | |
351 | def format_address(b): |
352 | return b2a_base58check('\x00' + b) |
353 | |
354 | ################################################## |
355 | # Common database operations |
356 | |
357 | def get_address_id(a): |
358 | r = db.execute('SELECT address_id FROM address WHERE address=?', (buffer(a),)).fetchone() |
359 | return None if r is None else r[0] |
360 | |
361 | def insert_or_get_address_id(a): |
362 | try: |
363 | return db.execute('INSERT INTO address (address) VALUES (?)', (buffer(a),)).lastrowid |
364 | except IntegrityError: |
365 | return get_address_id(a) |
366 | |
367 | def get_tx_id(hash): |
368 | r = db.execute('SELECT tx_id FROM tx WHERE hash=?', (buffer(hash),)).fetchone() |
369 | return None if r is None else r[0] |
370 | |
371 | def insert_or_get_tx_id(hash, blkhash, height, n, size): |
372 | try: |
373 | return db.execute('INSERT INTO tx (hash, block_hash, block_height, n, size) VALUES (?,?,?,?,?)', |
374 | (buffer(hash), buffer(blkhash), height, n, size)).lastrowid |
375 | except IntegrityError: |
376 | r = db.execute('SELECT tx_id, block_hash, block_height, n, size FROM tx WHERE hash=?', |
377 | (buffer(hash),)).fetchone() |
378 | if r is None: |
379 | raise Conflict('differing transactions with same (height, index)', (height, n), hash) |
380 | if r[1:] != (buffer(blkhash), height, n, size): |
381 | raise Conflict('differing transactions with same hash', |
382 | hash, (blkhash, height, n, size), r[1:]) |
383 | return r[0] |
384 | |
385 | def insert_output(tx_id, n, addr_id, val): |
386 | try: |
387 | db.execute('INSERT INTO output (tx_id, n, address_id, value) VALUES (?,?,?,?)', |
388 | (tx_id, n, addr_id, val)) |
389 | except IntegrityError: |
390 | r = db.execute('SELECT address_id, value FROM output WHERE tx_id=? AND n=?', |
391 | (tx_id, n)).fetchone() |
392 | if r != (addr_id, val): |
393 | raise Conflict('output differs from previous content', tx_id, n, (addr_id, val), r) |
394 | |
395 | def insert_input(tx_id, n, prevout_id): |
396 | try: |
397 | input_id = db.execute('INSERT INTO input (tx_id, n) VALUES (?,?)', (tx_id, n)).lastrowid |
398 | except IntegrityError: |
399 | input_id = db.execute('SELECT input_id FROM input WHERE tx_id=? AND n=?', |
400 | (tx_id, n)).fetchone()[0] |
401 | db.execute('UPDATE output SET spent=? WHERE output_id=?', (input_id, prevout_id)) |
402 | |
403 | def get_output_id(tx_id, n): |
404 | r = db.execute('SELECT output_id FROM output WHERE tx_id=? AND n=?', (tx_id, n)).fetchone() |
405 | return None if r is None else r[0] |
406 | |
407 | def get_tag_id(name): |
408 | r = db.execute('SELECT tag_id FROM tag WHERE name=?', (name,)).fetchone() |
409 | return None if r is None else r[0] |
410 | |
411 | def insert_or_get_tag_id(name): |
412 | try: |
413 | return db.execute('INSERT INTO tag (name) VALUES (?)', (name,)).lastrowid |
414 | except IntegrityError: |
415 | return get_tag_id(name) |
416 | |
417 | ################################################## |
418 | # Command implementations |
419 | |
420 | def scan_block(height, v): |
421 | stdout.write('block %s' % height) |
422 | (blkhash, prev, time, target, txs), size = load_block(v) |
423 | |
424 | count_out = 0 |
425 | n_tx = 0 |
426 | for (hash, size, txins, txouts) in txs: |
427 | matched_outs = [] |
428 | for n, txout in enumerate(txouts): |
429 | val, script = txout |
430 | a = out_script_address(script) |
431 | if a is not None: |
432 | #print format_address(a) |
433 | addr_id = get_address_id(a) |
434 | if addr_id is not None: |
435 | matched_outs.append((n, addr_id, val)) |
436 | if len(matched_outs) > 0: |
437 | tx_id = insert_or_get_tx_id(hash, blkhash, height, n_tx, size) |
438 | for n, addr_id, val in matched_outs: |
439 | insert_output(tx_id, n, addr_id, val) |
440 | count_out += len(matched_outs) |
441 | n_tx += 1 |
442 | stdout.write(' new-outs %s' % count_out) |
443 | |
444 | # Inputs scanned second in case an output from the same block is spent. |
445 | # Coinbase (input of first tx in block) doesn't reference anything. |
446 | count_in = 0 |
447 | n_tx = 1 |
448 | for (hash, size, txins, txouts) in txs[1:]: |
449 | matched_ins = [] |
450 | for n, txin in enumerate(txins): |
451 | prevout_hash, prevout_n, scriptsig = txin |
452 | prevout_tx_id = get_tx_id(prevout_hash) |
453 | if prevout_tx_id is not None: |
454 | prevout_id = get_output_id(prevout_tx_id, prevout_n) |
455 | if prevout_id is not None: |
456 | matched_ins.append((n, prevout_id)) |
457 | if len(matched_ins) > 0: |
458 | tx_id = insert_or_get_tx_id(hash, blkhash, height, n_tx, size) |
459 | for n, prevout_id in matched_ins: |
460 | insert_input(tx_id, n, prevout_id) |
461 | count_in += len(matched_ins) |
462 | n_tx += 1 |
463 | stdout.write(' spent-outs %s\n' % count_in) |
464 | |
465 | def die(msg, help=False): |
466 | stderr.write('gbw-node: %s\n' % msg) |
467 | if help: |
468 | cmd_help([]) |
469 | exit(-1) |
470 | |
471 | def require_tag(name): |
472 | i = get_tag_id(name) |
473 | if i is None: |
474 | die('tag not found: %r' % name) |
475 | return i |
476 | |
477 | def cmd_scan(argv): |
478 | ''' |
479 | scan |
480 | |
481 | Iterate blocks from bitcoind, indexing transaction inputs and outputs affecting watched addresses. May be safely interrupted and resumed. |
482 | |
483 | NOT PRESENTLY SAFE TO RUN CONCURRENT INSTANCES due to the dumpblock to named pipe kludge. |
484 | ''' |
485 | db.execute('PRAGMA synchronous=NORMAL') |
486 | height = db.execute('SELECT scan_height FROM state').fetchone()[0] |
487 | # RPC calls can have high latency, so refresh the goal height upon reaching the previous rather than for each block scanned. |
488 | last_blockcount = -1 |
489 | while True: |
490 | blockcount = max(-1, rpc('getblockcount') - CONFIRMS) |
491 | if blockcount == last_blockcount: |
492 | break |
493 | last_blockcount = blockcount |
494 | while height < blockcount: |
495 | height += 1 |
496 | scan_block(height, memoryview(getblock(height))) |
497 | db.execute('UPDATE state SET scan_height = ?', (height,)) |
498 | db.commit() |
499 | |
500 | def cmd_reset(argv): |
501 | ''' |
502 | reset |
503 | |
504 | Reset the scan pointer so the next scan will proceed from the genesis block, to find transactions associated with newly watched addresses. |
505 | ''' |
506 | db.execute('UPDATE state SET scan_height = -1') |
507 | db.commit() |
508 | |
509 | def cmd_tags(argv): |
510 | ''' |
511 | tags [ADDRESS] |
512 | |
513 | List tags for the given ADDRESS (or all tags). |
514 | ''' |
515 | if len(argv) > 0: |
516 | addr_id = get_address_id(parse_address(argv.pop(0))) |
517 | if addr_id is None: |
518 | die('address not found: %r' % name) |
519 | r = db.execute('SELECT name FROM tag \ |
520 | JOIN address_tag ON tag.tag_id=address_tag.tag_id \ |
521 | WHERE address_id=? ORDER BY name', (addr_id,)) |
522 | else: |
523 | r = db.execute('SELECT name FROM tag ORDER BY name') |
524 | for name, in r: |
525 | stdout.write(name + '\n') |
526 | |
527 | def cmd_addresses(argv): |
528 | ''' |
529 | addresses [TAG] |
530 | |
531 | List addresses with the given TAG (or all watched addresses). |
532 | ''' |
533 | if len(argv) > 0: |
534 | tag_id = require_tag(argv.pop(0)) |
535 | r = db.execute('SELECT address FROM address \ |
536 | JOIN address_tag ON address.address_id=address_tag.address_id \ |
537 | WHERE tag_id=? ORDER BY address', (tag_id,)) |
538 | else: |
539 | r = db.execute('SELECT address FROM address ORDER BY address') |
540 | for a, in r: |
541 | stdout.write(format_address(str(a)) + '\n') |
542 | |
543 | def cmd_unspent_outs(argv): |
544 | ''' |
545 | unspent-outs [TAG] |
546 | |
547 | Display the unspent outputs table for addresses with the given TAG (or all watched addresses), as required by the offline wallet, ordered by age. |
548 | ''' |
549 | if len(argv) > 0: |
550 | tag_id = require_tag(argv.pop(0)) |
551 | r = db.execute('SELECT address, value, hash, output.n, block_height, tx.n FROM output \ |
552 | JOIN address ON output.address_id = address.address_id \ |
553 | JOIN tx ON output.tx_id = tx.tx_id \ |
554 | JOIN address_tag ON output.address_id = address_tag.address_id \ |
555 | WHERE spent IS NULL AND tag_id=? \ |
556 | ORDER BY block_height DESC', (tag_id,)) |
557 | else: |
558 | r = db.execute('SELECT address, value, hash, output.n, block_height, tx.n FROM output \ |
559 | JOIN address ON output.address_id = address.address_id \ |
560 | JOIN tx ON output.tx_id = tx.tx_id \ |
561 | WHERE spent IS NULL \ |
562 | ORDER BY block_height DESC') |
563 | for a, v, hash, n_out, height, n_tx in r: |
564 | stdout.write('%s %s %s %s #blk %s tx %s\n' % (format_address(str(a)), format_coin(v), b2lx(hash), n_out, height, n_tx)) |
565 | |
566 | def cmd_balance(argv): |
567 | ''' |
568 | balance [TAG] |
569 | |
570 | Display confirmed balance of addresses with the given TAG (or all watched addresses). |
571 | ''' |
572 | if len(argv) > 0: |
573 | tag_id = require_tag(argv.pop(0)) |
574 | r = db.execute('SELECT COALESCE(SUM(value),0) FROM output \ |
575 | JOIN address_tag ON output.address_id = address_tag.address_id \ |
576 | WHERE spent IS NULL AND tag_id=?', (tag_id,)) |
577 | else: |
578 | r = db.execute('SELECT COALESCE(SUM(value),0) FROM output WHERE spent IS NULL') |
579 | bal, = r.fetchone() |
580 | stdout.write('%s\n' % format_coin(bal)) |
581 | |
582 | def cmd_register(argv): |
583 | ''' |
584 | register [TAG] |
585 | |
586 | Display a tab-delimited transaction register report for addresses with the given TAG (or all watched addresses). Columns are: |
587 | |
588 | - confirmation block height |
589 | - number of transaction within block |
590 | - total deposits (new outputs) |
591 | - total withdrawals (spent outputs) |
592 | - running balance |
593 | ''' |
594 | if len(argv) > 0: |
595 | tag_id = require_tag(argv.pop(0)) |
596 | outs = db.execute('SELECT block_height, tx.n, COALESCE(SUM(value),0) FROM tx \ |
597 | JOIN output ON output.tx_id = tx.tx_id \ |
598 | JOIN address_tag ON output.address_id = address_tag.address_id \ |
599 | WHERE tag_id=? \ |
600 | GROUP BY tx.tx_id \ |
601 | ORDER BY block_height, tx.n', (tag_id,)) |
602 | ins = db.execute('SELECT block_height, tx.n, COALESCE(SUM(value),0) FROM tx \ |
603 | JOIN input ON input.tx_id = tx.tx_id \ |
604 | JOIN output ON input.input_id = output.spent \ |
605 | JOIN address_tag ON output.address_id = address_tag.address_id \ |
606 | WHERE tag_id=? \ |
607 | GROUP BY tx.tx_id \ |
608 | ORDER BY block_height, tx.n', (tag_id,)) |
609 | else: |
610 | outs = db.execute('SELECT block_height, tx.n, COALESCE(SUM(value),0) FROM tx \ |
611 | JOIN output ON output.tx_id = tx.tx_id \ |
612 | GROUP BY tx.tx_id \ |
613 | ORDER BY block_height, tx.n') |
614 | ins = db.execute('SELECT block_height, tx.n, COALESCE(SUM(value),0) FROM tx \ |
615 | JOIN input ON input.tx_id = tx.tx_id \ |
616 | JOIN output ON input.input_id = output.spent \ |
617 | GROUP BY tx.tx_id \ |
618 | ORDER BY block_height, tx.n') |
619 | bal = 0 |
620 | for height, n, o_val, i_val in merge_moves(outs.fetchall(), ins.fetchall()): |
621 | bal = bal + o_val - i_val |
622 | stdout.write('%s\t%s\t%s\t%s\t%s\n' % (height, n, format_coin(o_val), format_coin(-i_val), format_coin(bal))) |
623 | |
624 | # Merge ordered lists of total input and output values per transaction into single table with columns for both. |
625 | def merge_moves(outs, ins): |
626 | i = o = 0 |
627 | |
628 | while True: |
629 | if o == len(outs): |
630 | for height, n, val in ins[i:]: |
631 | yield (height, n, 0, val) |
632 | return |
633 | o_height, o_n, o_val = outs[o] |
634 | o_key = (o_height, o_n) |
635 | |
636 | if i == len(ins): |
637 | for height, n, val in outs[o:]: |
638 | yield (height, n, val, 0) |
639 | return |
640 | i_height, i_n, i_val = ins[i] |
641 | i_key = (i_height, i_n) |
642 | |
643 | if o_key < i_key: |
644 | yield (o_height, o_n, o_val, 0) |
645 | o += 1 |
646 | elif i_key < o_key: |
647 | yield (i_height, i_n, 0, i_val) |
648 | i += 1 |
649 | else: |
650 | yield (o_height, o_n, o_val, i_val) |
651 | i += 1 |
652 | o += 1 |
653 | |
654 | def cmd_watch(argv): |
655 | ''' |
656 | watch [TAG] |
657 | |
658 | Import a set of addresses to watch linewise from stdin, optionally named by the given TAG. Addresses can be associated with multiple tags using multiple watch commands. |
659 | ''' |
660 | tag_id = None |
661 | if len(argv) > 0: |
662 | name = argv.pop(0) |
663 | if '\n' in name: |
664 | die('newline not allowed in tag name') |
665 | tag_id = insert_or_get_tag_id(name) |
666 | while True: |
667 | l = stdin.readline() |
668 | if len(l) == 0: |
669 | break |
670 | addr_id = insert_or_get_address_id(parse_address(l.rstrip('\n'))) |
671 | if tag_id is not None: |
672 | try: |
673 | db.execute('INSERT INTO address_tag (address_id, tag_id) VALUES (?,?)', |
674 | (addr_id, tag_id)) |
675 | except IntegrityError: |
676 | pass |
677 | db.commit() |
678 | |
679 | def cmd_push(argv): |
680 | ''' |
681 | push |
682 | |
683 | Import raw hex transactions linewise from stdin and send to bitcoind. |
684 | ''' |
685 | while True: |
686 | line = stdin.readline() |
687 | if len(line) == 0: |
688 | break |
689 | tx_hex = line.rstrip('\n') |
690 | stdout.write('txid %s\n' % rpc('sendrawtransaction', tx_hex)) |
691 | |
692 | def cmd_unlock_wallet(argv): |
693 | ''' |
694 | unlock-wallet [TIMEOUT] |
695 | |
696 | Read encryption passphrase from the terminal and unlock the bitcoind internal wallet for TIMEOUT seconds (default 60). |
697 | ''' |
698 | timeout = int(argv.pop(0)) if len(argv) > 0 else 60 |
699 | r = rpc('walletpassphrase', getpass('Passphrase: '), timeout) |
700 | if r is not None: |
701 | stdout.write('%r\n' % r) |
702 | else: |
703 | stdout.write('Cached for %s seconds\n' % timeout) |
704 | |
705 | def cmd_help(argv): |
706 | ''' |
707 | help [COMMAND] |
708 | |
709 | Display help for a given command or list all commands. |
710 | ''' |
711 | if len(argv) > 0: |
712 | name = argv.pop(0) |
713 | name, func = get_command(name) |
714 | doc = getdoc(func) |
715 | if doc is None: |
716 | stdout.write('No help for %r\n' % name) |
717 | else: |
718 | stdout.write('gbw-node %s\n' % doc) |
719 | else: |
720 | stdout.write('''Usage: gbw-node COMMAND [ARGS] |
721 | |
722 | Available commands (can be abbreviated when unambiguous): |
723 | |
724 | %s |
725 | ''' % '\n'.join([name for name, f in cmds])) |
726 | |
727 | cmds = ( |
728 | ('help', cmd_help), |
729 | ('scan', cmd_scan), |
730 | ('reset', cmd_reset), |
731 | ('tags', cmd_tags), |
732 | ('addresses', cmd_addresses), |
733 | ('unspent-outs', cmd_unspent_outs), |
734 | ('watch', cmd_watch), |
735 | ('push', cmd_push), |
736 | ('balance', cmd_balance), |
737 | ('register', cmd_register), |
738 | ('unlock-wallet', cmd_unlock_wallet), |
739 | ) |
740 | |
741 | def get_command(name): |
742 | rows = [r for r in cmds if r[0].startswith(name)] |
743 | if len(rows) == 0: |
744 | die('command not found: %s' % name) |
745 | if len(rows) > 1: |
746 | die('ambiguous command %s. Completions: %s' % (name, ' '.join([r[0] for r in rows]))) |
747 | return rows[0] |
748 | |
749 | def main(): |
750 | global db |
751 | signal.signal(signal.SIGINT, signal.SIG_DFL) |
752 | require_dir(gbw_home) |
753 | db = sqlite3.connect(gbw_home + '/db', timeout=600) # in seconds |
754 | db.execute('PRAGMA foreign_keys=ON') |
755 | db.execute('PRAGMA cache_size=-8000') # negative means in KiB |
756 | db.execute('PRAGMA wal_autocheckpoint=10000') # in pages (4k) |
757 | if len(argv) < 2: |
758 | die('missing command', help=True) |
759 | get_command(argv[1])[1](argv[2:]) |
760 | |
761 | if __name__ == '__main__': |
762 | main() |