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

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.protocol
     src.transaction
     src.transaction
     src.persistence
     src.persistence
+    src.rpc_client
+    src.rpc_server
 
 
 Tests
 Tests
 *****
 *****

+ 8 - 75
miner.py

@@ -1,28 +1,29 @@
 #!/usr/bin/env python3
 #!/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__ = []
 __all__ = []
 
 
 import argparse
 import argparse
-import json
 from urllib.parse import urlparse
 from urllib.parse import urlparse
+from typing import Tuple
 
 
 import logging
 import logging
 logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-8s %(message)s")
 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.crypto import Signing
 from src.protocol import Protocol
 from src.protocol import Protocol
 from src.block import GENESIS_BLOCK
 from src.block import GENESIS_BLOCK
 from src.chainbuilder import ChainBuilder
 from src.chainbuilder import ChainBuilder
 from src.mining import Miner
 from src.mining import Miner
-from src.transaction import TransactionInput
 from src.persistence import Persistence
 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)
     url = urlparse("//" + val)
     assert url.scheme == ''
     assert url.scheme == ''
     assert url.path == ''
     assert url.path == ''
@@ -33,74 +34,6 @@ def parse_addr_port(val):
     assert url.hostname is not None
     assert url.hostname is not None
     return (url.hostname, url.port)
     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():
 def main():
     parser = argparse.ArgumentParser(description="Blockchain Miner.")
     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__ = []
 __all__ = []
 
 
 import argparse
 import argparse
-import requests
 import sys
 import sys
-import json
 from binascii import hexlify
 from binascii import hexlify
+from io import IOBase
+from typing import List, Union, Callable, Tuple, Optional
 
 
 import logging
 import logging
 logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-8s %(message)s")
 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.blockchain import Blockchain
 from src.transaction import Transaction, TransactionTarget, TransactionInput
 from src.transaction import Transaction, TransactionTarget, TransactionInput
 from src.crypto import Signing
 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
     start = True
     def parse(val):
     def parse(val):
         nonlocal start
         nonlocal start
@@ -74,13 +38,20 @@ def parse_targets():
         return val
         return val
     return parse
     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)
     val = Signing.from_file(path)
     if not val.has_private:
     if not val.has_private:
         raise ValueError("The specified key is not a private key.")
         raise ValueError("The specified key is not a private key.")
     return val
     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:
     try:
         with open(path, "rb") as f:
         with open(path, "rb") as f:
             contents = f.read()
             contents = f.read()
@@ -127,10 +98,47 @@ def main():
                           help="The private key(s) whose coins should be used for the transfer.")
                           help="The private key(s) whose coins should be used for the transfer.")
     args = parser.parse_args()
     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]
         all_keys = keys + args.wallet[0]
         if not all_keys:
         if not all_keys:
             print("missing key or wallet", file=sys.stderr)
             print("missing key or wallet", file=sys.stderr)
@@ -138,42 +146,26 @@ def main():
         return all_keys
         return all_keys
 
 
     if args.command == 'show-transactions':
     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":
     elif args.command == "create-address":
         if not args.wallet[1]:
         if not args.wallet[1]:
             print("no wallet specified", file=sys.stderr)
             print("no wallet specified", file=sys.stderr)
             parser.parse_args(["--help"])
             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':
     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':
     elif args.command == 'show-network':
-        for [k, v] in network_info(s, url):
-            print("{}\t{}".format(k, v))
+        network_info()
     elif args.command == 'transfer':
     elif args.command == 'transfer':
         if len(args.target) % 2:
         if len(args.target) % 2:
             print("Missing amount to transfer for last target key.\n", file=sys.stderr)
             print("Missing amount to transfer for last target key.\n", file=sys.stderr)
             parser.parse_args(["--help"])
             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])]
         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:
     else:
         print("You need to specify what to do.\n", file=sys.stderr)
         print("You need to specify what to do.\n", file=sys.stderr)
         parser.parse_args(["--help"])
         parser.parse_args(["--help"])