Просмотр исходного кода

clean up the code for wallet and miner

Malte Kraus 8 лет назад
Родитель
Сommit
9abdbb69e1
5 измененных файлов с 235 добавлено и 148 удалено
  1. 2 0
      doc/index.rst
  2. 8 75
      miner.py
  3. 67 0
      src/rpc_client.py
  4. 93 0
      src/rpc_server.py
  5. 65 73
      wallet.py

+ 2 - 0
doc/index.rst

@@ -37,6 +37,8 @@ Source Code Documentation
     src.protocol
     src.transaction
     src.persistence
+    src.rpc_client
+    src.rpc_server
 
 Tests
 *****

+ 8 - 75
miner.py

@@ -1,28 +1,29 @@
 #!/usr/bin/env python3
 
-""" The executable that participates in the P2P network and optionally mines new blocks. """
+"""
+The executable that participates in the P2P network and optionally mines new blocks. Can be reached
+through a REST API by the wallet.
+"""
 
 __all__ = []
 
 import argparse
-import json
 from urllib.parse import urlparse
+from typing import Tuple
 
 import logging
 logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-8s %(message)s")
 
-import flask
-app = flask.Flask(__name__)
-
 from src.crypto import Signing
 from src.protocol import Protocol
 from src.block import GENESIS_BLOCK
 from src.chainbuilder import ChainBuilder
 from src.mining import Miner
-from src.transaction import TransactionInput
 from src.persistence import Persistence
+from src.rpc_server import rpc_server
 
-def parse_addr_port(val):
+def parse_addr_port(val: str) -> Tuple[str, int]:
+    """ Parse a user-specified "host:port" value to a tuple. """
     url = urlparse("//" + val)
     assert url.scheme == ''
     assert url.path == ''
@@ -33,74 +34,6 @@ def parse_addr_port(val):
     assert url.hostname is not None
     return (url.hostname, url.port)
 
-def rpc_server(port, chainbuilder, persist):
-    @app.route("/network-info", methods=['GET'])
-    def get_network_info():
-        return json.dumps([list(peer.peer_addr)[:2] for peer in chainbuilder.protocol.peers if peer.is_connected])
-
-    @app.route("/new-transaction", methods=['PUT'])
-    def send_transaction():
-        chainbuilder.protocol.received("transaction", flask.request.json, None, 0)
-        # TODO: from the right thread, we want to call persist.store() here
-        return b""
-
-    @app.route("/show-balance", methods=['POST'])
-    def show_balance():
-        pubkeys = {Signing.from_json_compatible(pk): i for (i, pk) in enumerate(flask.request.json)}
-        amounts = [0 for _ in pubkeys.values()]
-        for output in chainbuilder.primary_block_chain.unspent_coins.values():
-            if output.recipient_pk in pubkeys:
-                amounts[pubkeys[output.recipient_pk]] += output.amount
-
-        return json.dumps(amounts)
-
-    @app.route("/build-transaction", methods=['POST'])
-    def build_transaction():
-        sender_pks = {Signing.from_json_compatible(o): i for i, o in enumerate(flask.request.json['sender-pubkeys'])}
-        amount = flask.request.json['amount']
-
-        inputs = []
-        used_keys = []
-        for (inp, output) in chainbuilder.primary_block_chain.unspent_coins.items():
-            if output.recipient_pk in sender_pks:
-                amount -= output.amount
-                inputs.append(inp.to_json_compatible())
-                used_keys.append(sender_pks[output.recipient_pk])
-                if amount <= 0:
-                    break
-
-        if amount > 0:
-            inputs = []
-            used_keys = []
-
-        return json.dumps({
-            "inputs": inputs,
-            "remaining_amount": -amount,
-            "key_indices": used_keys,
-        })
-
-
-    @app.route("/transactions", methods=['POST'])
-    def get_transactions_for_key():
-        key = Signing(flask.request.data)
-        transactions = set()
-        outputs = set()
-        chain = chainbuilder.primary_block_chain
-        for b in chain.blocks:
-            for t in b.transactions:
-                for i, target in enumerate(t.targets):
-                    if target.recipient_pk == key:
-                        transactions.add(t)
-                        outputs.add(TransactionInput(t.get_hash(), i))
-        for b in chain.blocks:
-            for t in b.transactions:
-                for inp in t.inputs:
-                    if inp in outputs:
-                        transactions.add(t)
-
-        return json.dumps([t.to_json_compatible() for t in transactions])
-
-    app.run(port=port)
 
 def main():
     parser = argparse.ArgumentParser(description="Blockchain Miner.")

+ 67 - 0
src/rpc_client.py

@@ -0,0 +1,67 @@
+""" The RPC functionality used by the wallet to talk to the miner application. """
+
+import json
+from typing import List, Tuple, Iterator
+
+import requests
+
+from .transaction import Transaction, TransactionTarget, TransactionInput
+from .crypto import Signing
+
+
+class RPCClient:
+    """ The RPC methods used by the wallet to talk to the miner application. """
+
+    def __init__(self, miner_port: int):
+        self.sess = requests.Session()
+        self.url = "http://localhost:{}/".format(miner_port)
+
+    def send_transaction(self, transaction: Transaction):
+        """ Sends a transaction to the miner. """
+        resp = self.sess.put(self.url + 'new-transaction', data=json.dumps(transaction.to_json_compatible()),
+                        headers={"Content-Type": "application/json"})
+        resp.raise_for_status()
+
+    def network_info(self) -> List[Tuple[str, int]]:
+        """ Returns the peers connected to the miner. """
+        resp = self.sess.get(self.url + 'network-info')
+        resp.raise_for_status()
+        return [tuple(peer) for peer in resp.json()]
+
+    def get_transactions(self, pubkey: Signing) -> List[Transaction]:
+        """ Returns all transactions involving a certain public key. """
+        resp = self.sess.post(self.url + 'transactions', data=pubkey.as_bytes(),
+                         headers={"Content-Type": "application/json"})
+        resp.raise_for_status()
+        return [Transaction.from_json_compatible(t) for t in resp.json()]
+
+    def show_balance(self, pubkeys: List[Signing]) -> Iterator[Tuple[Signing, int]]:
+        """ Returns the balance of a number of public keys. """
+        resp = self.sess.post(self.url + "show-balance", data=json.dumps([pk.to_json_compatible() for pk in pubkeys]),
+                         headers={"Content-Type": "application/json"})
+        resp.raise_for_status()
+        return zip(pubkeys, resp.json())
+
+    def build_transaction(self, source_keys: List[Signing], targets: List[TransactionTarget],
+                          change_key: Signing, transaction_fee: int) -> Transaction:
+        """
+        Builds a transaction sending money from `source_keys` to `targets`, sending any change to the
+        key `change_key` and a transaction fee `transaction_fee` to the miner.
+        """
+        resp = self.sess.post(self.url + "build-transaction", data=json.dumps({
+                "sender-pubkeys": [k.to_json_compatible() for k in source_keys],
+                "amount": sum(t.amount for t in targets) + transaction_fee,
+            }), headers={"Content-Type": "application/json"})
+        resp.raise_for_status()
+        resp = resp.json()
+        remaining = resp['remaining_amount']
+        if remaining < 0:
+            print("You do not have sufficient funds for this transaction. ({} missing)".format(-remaining), file=sys.stderr)
+            sys.exit(2)
+        elif remaining > 0:
+            targets = targets + [TransactionTarget(change_key, remaining)]
+
+        inputs = [TransactionInput.from_json_compatible(i) for i in resp['inputs']]
+        trans = Transaction(inputs, targets)
+        trans.sign([source_keys[idx] for idx in resp['key_indices']])
+        return trans

+ 93 - 0
src/rpc_server.py

@@ -0,0 +1,93 @@
+""" The RPC functionality the miner provides for the wallet. """
+
+import json
+
+import flask
+
+from .chainbuilder import ChainBuilder
+from .persistence import Persistence
+from .crypto import Signing
+from .transaction import TransactionInput
+
+def rpc_server(port: int, chainbuilder: ChainBuilder, persist: Persistence):
+    """ Runs the RPC server. """
+
+    app = flask.Flask(__name__)
+
+    @app.route("/network-info", methods=['GET'])
+    def get_network_info():
+        """ Returns the connected peers. """
+        return json.dumps([list(peer.peer_addr)[:2] for peer in chainbuilder.protocol.peers if peer.is_connected])
+
+    @app.route("/new-transaction", methods=['PUT'])
+    def send_transaction():
+        """ Sends a transaction to the network, and uses it for mining. """
+        chainbuilder.protocol.received("transaction", flask.request.json, None, 0)
+        return b""
+
+    @app.route("/show-balance", methods=['POST'])
+    def show_balance():
+        """ Returns the balance of a number of public keys. """
+        pubkeys = {Signing.from_json_compatible(pk): i for (i, pk) in enumerate(flask.request.json)}
+        amounts = [0 for _ in pubkeys.values()]
+        for output in chainbuilder.primary_block_chain.unspent_coins.values():
+            if output.recipient_pk in pubkeys:
+                amounts[pubkeys[output.recipient_pk]] += output.amount
+
+        return json.dumps(amounts)
+
+    @app.route("/build-transaction", methods=['POST'])
+    def build_transaction():
+        """
+        Returns the transaction inputs that can be used to build a transaction with a certain
+        amount from some public keys.
+        """
+        sender_pks = {
+                Signing.from_json_compatible(o): i
+                    for i, o in enumerate(flask.request.json['sender-pubkeys'])
+        }
+        amount = flask.request.json['amount']
+
+        inputs = []
+        used_keys = []
+        for (inp, output) in chainbuilder.primary_block_chain.unspent_coins.items():
+            if output.recipient_pk in sender_pks:
+                amount -= output.amount
+                inputs.append(inp.to_json_compatible())
+                used_keys.append(sender_pks[output.recipient_pk])
+                if amount <= 0:
+                    break
+
+        if amount > 0:
+            inputs = []
+            used_keys = []
+
+        return json.dumps({
+            "inputs": inputs,
+            "remaining_amount": -amount,
+            "key_indices": used_keys,
+        })
+
+
+    @app.route("/transactions", methods=['POST'])
+    def get_transactions_for_key():
+        """ Returns all transactions involving a certain public key. """
+        key = Signing(flask.request.data)
+        transactions = set()
+        outputs = set()
+        chain = chainbuilder.primary_block_chain
+        for b in chain.blocks:
+            for t in b.transactions:
+                for i, target in enumerate(t.targets):
+                    if target.recipient_pk == key:
+                        transactions.add(t)
+                        outputs.add(TransactionInput(t.get_hash(), i))
+        for b in chain.blocks:
+            for t in b.transactions:
+                for inp in t.inputs:
+                    if inp in outputs:
+                        transactions.add(t)
+
+        return json.dumps([t.to_json_compatible() for t in transactions])
+
+    app.run(port=port)

+ 65 - 73
wallet.py

@@ -8,10 +8,10 @@ miner.
 __all__ = []
 
 import argparse
-import requests
 import sys
-import json
 from binascii import hexlify
+from io import IOBase
+from typing import List, Union, Callable, Tuple, Optional
 
 import logging
 logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-8s %(message)s")
@@ -20,49 +20,13 @@ from src.block import Block
 from src.blockchain import Blockchain
 from src.transaction import Transaction, TransactionTarget, TransactionInput
 from src.crypto import Signing
+from src.rpc_client import RPCClient
 
-def send_transaction(sess, url, transaction):
-    resp = sess.put(url + 'new-transaction', data=json.dumps(transaction.to_json_compatible()),
-                    headers={"Content-Type": "application/json"})
-    resp.raise_for_status()
-
-def network_info(sess, url):
-    resp = sess.get(url + 'network-info')
-    resp.raise_for_status()
-    return resp.json()
-
-def get_transactions(sess, url, pubkey):
-    resp = sess.post(url + 'transactions', data=pubkey.as_bytes(),
-                     headers={"Content-Type": "application/json"})
-    resp.raise_for_status()
-    return [Transaction.from_json_compatible(t) for t in resp.json()]
-
-def show_balance(sess, url, pubkeys):
-    resp = sess.post(url + "show-balance", data=json.dumps([pk.to_json_compatible() for pk in pubkeys]),
-                     headers={"Content-Type": "application/json"})
-    resp.raise_for_status()
-    return zip(pubkeys, resp.json())
-
-def build_transaction(sess, url, source_keys, targets, change_key, transaction_fee):
-    resp = sess.post(url + "build-transaction", data=json.dumps({
-            "sender-pubkeys": [k.to_json_compatible() for k in source_keys],
-            "amount": sum(t.amount for t in targets) + transaction_fee,
-        }), headers={"Content-Type": "application/json"})
-    resp.raise_for_status()
-    resp = resp.json()
-    remaining = resp['remaining_amount']
-    if remaining < 0:
-        print("You do not have sufficient funds for this transaction. ({} missing)".format(-remaining), file=sys.stderr)
-        sys.exit(2)
-    elif remaining > 0:
-        targets = targets + [TransactionTarget(change_key, remaining)]
-
-    inputs = [TransactionInput.from_json_compatible(i) for i in resp['inputs']]
-    trans = Transaction(inputs, targets)
-    trans.sign([source_keys[idx] for idx in resp['key_indices']])
-    send_transaction(sess, url, trans)
-
-def parse_targets():
+def parse_targets() -> Callable[[str], Union[Signing, int]]:
+    """
+    Parses transaction targets from the command line: the first value is a path to a key, the
+    second an amount and so on.
+    """
     start = True
     def parse(val):
         nonlocal start
@@ -74,13 +38,20 @@ def parse_targets():
         return val
     return parse
 
-def private_signing(path):
+def private_signing(path: str) -> Signing:
+    """ Parses a path to a private key from the command line. """
     val = Signing.from_file(path)
     if not val.has_private:
         raise ValueError("The specified key is not a private key.")
     return val
 
-def wallet_file(path):
+def wallet_file(path: str) -> Tuple[List[Signing], str]:
+    """
+    Parses the wallet from the command line.
+
+    Returns a tuple with a list of keys from the wallet and the path to the wallet (for write
+    operations).
+    """
     try:
         with open(path, "rb") as f:
             contents = f.read()
@@ -127,10 +98,47 @@ def main():
                           help="The private key(s) whose coins should be used for the transfer.")
     args = parser.parse_args()
 
-    url = "http://localhost:{}/".format(args.miner_port)
-    s = requests.session()
+    rpc = RPCClient(args.miner_port)
+
+    def show_transactions(keys: List[Signing]):
+        for key in keys:
+            for trans in rpc.get_transactions(key):
+                print(trans.to_json_compatible())
+            print()
+
+    def create_address(wallet_keys: List[Signing], wallet_path: str, output_files: List[IOBase]):
+        keys = [Signing.generate_private_key() for _ in output_files]
+        Signing.write_many_private(wallet_path, wallet_keys + keys)
+        for fp, key in zip(output_files, keys):
+            fp.write(key.as_bytes())
+            fp.close()
+
+    def show_balance(keys: List[Signing]):
+        total = 0
+        for pubkey, balance in rpc.show_balance(keys):
+            print("{}: {}".format(hexlify(pubkey.as_bytes()), balance))
+            total += balance
+        print()
+        print("total: {}".format(total))
+
+    def network_info():
+        for k, v in rpc.network_info():
+            print("{}\t{}".format(k, v))
+
+    def transfer(targets: List[TransactionTarget], change_key: Optional[Signing],
+                 wallet_keys: List[Signing], wallet_path: str, priv_keys: List[Signing]):
+        if not change_key:
+            change_key = Signing.generate_private_key()
+            Signing.write_many_private(wallet_path, wallet_keys + [change_key])
+
+        trans = rpc.build_transaction(priv_keys, targets, change_key, args.transaction_fee)
+        rpc.send_transaction(trans)
+
 
-    def get_keys(keys):
+    def get_keys(keys: List[Signing]) -> List[Signing]:
+        """
+        Returns a combined list of keys from the `keys` and the wallet. Shows an error if empty.
+        """
         all_keys = keys + args.wallet[0]
         if not all_keys:
             print("missing key or wallet", file=sys.stderr)
@@ -138,42 +146,26 @@ def main():
         return all_keys
 
     if args.command == 'show-transactions':
-        for key in get_keys(args.key):
-            for trans in get_transactions(s, url, key):
-                print(trans.to_json_compatible())
-            print()
+        show_transactions(get_keys(args.key))
     elif args.command == "create-address":
         if not args.wallet[1]:
             print("no wallet specified", file=sys.stderr)
             parser.parse_args(["--help"])
 
-        keys = [Signing.generate_private_key() for _ in args.file]
-        Signing.write_many_private(args.wallet[1], args.wallet[0] + keys)
-        for fp, key in zip(args.file, keys):
-            fp.write(key.as_bytes())
-            fp.close()
+        create_address(*args.wallet, args.file)
     elif args.command == 'show-balance':
-        total = 0
-        for pubkey, balance in show_balance(s, url, get_keys(args.key)):
-            print("{}: {}".format(hexlify(pubkey.as_bytes()), balance))
-            total += balance
-        print()
-        print("total: {}".format(total))
+        show_balance(get_keys(args.key))
     elif args.command == 'show-network':
-        for [k, v] in network_info(s, url):
-            print("{}\t{}".format(k, v))
+        network_info()
     elif args.command == 'transfer':
         if len(args.target) % 2:
             print("Missing amount to transfer for last target key.\n", file=sys.stderr)
             parser.parse_args(["--help"])
+        if not args.change_key and not args.wallet[0]:
+            print("You need to specify either --wallet or --change-key.\n", file=sys.stderr)
+            parser.parse_args(["--help"])
         targets = [TransactionTarget(k, a) for k, a in zip(args.target[::2], args.target[1::2])]
-        change_key = args.change_key
-        if not change_key:
-            get_keys([]) # shows error if no wallet
-            change_key = Signing.generate_private_key()
-            Signing.write_many_private(args.wallet[1], args.wallet[0] + [change_key])
-
-        build_transaction(s, url, get_keys(args.private_key), targets, change_key, args.transaction_fee)
+        transfer(targets, args.change_key, *args.wallet, get_keys(args.private_key))
     else:
         print("You need to specify what to do.\n", file=sys.stderr)
         parser.parse_args(["--help"])