wallet.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. #!/usr/bin/env python3
  2. """
  3. The wallet allows a user to query account balance, send money, and get status information about a
  4. miner.
  5. """
  6. __all__ = []
  7. import argparse
  8. import sys
  9. from binascii import hexlify
  10. from io import IOBase
  11. from typing import List, Union, Callable, Tuple, Optional
  12. import logging
  13. logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-8s %(message)s")
  14. from src.block import Block
  15. from src.blockchain import Blockchain
  16. from src.transaction import Transaction, TransactionTarget, TransactionInput
  17. from src.crypto import Signing
  18. from src.rpc_client import RPCClient
  19. def parse_targets() -> Callable[[str], Union[Signing, int]]:
  20. """
  21. Parses transaction targets from the command line: the first value is a path to a key, the
  22. second an amount and so on.
  23. """
  24. start = True
  25. def parse(val):
  26. nonlocal start
  27. if start:
  28. val = Signing.from_file(val)
  29. else:
  30. val = int(val)
  31. start = not start
  32. return val
  33. return parse
  34. def private_signing(path: str) -> Signing:
  35. """ Parses a path to a private key from the command line. """
  36. val = Signing.from_file(path)
  37. if not val.has_private:
  38. raise ValueError("The specified key is not a private key.")
  39. return val
  40. def wallet_file(path: str) -> Tuple[List[Signing], str]:
  41. """
  42. Parses the wallet from the command line.
  43. Returns a tuple with a list of keys from the wallet and the path to the wallet (for write
  44. operations).
  45. """
  46. try:
  47. with open(path, "rb") as f:
  48. contents = f.read()
  49. except FileNotFoundError:
  50. return [], path
  51. return list(Signing.read_many_private(contents)), path
  52. def main():
  53. parser = argparse.ArgumentParser(description="Wallet.")
  54. parser.add_argument("--miner-port", default=40203, type=int,
  55. help="The RPC port of the miner to connect to.")
  56. parser.add_argument("--wallet", type=wallet_file, default=([],None),
  57. help="The wallet file containing the private keys to use.")
  58. subparsers = parser.add_subparsers(dest="command")
  59. balance = subparsers.add_parser("create-address",
  60. help="Creates new addresses and stores their secret keys in the wallet.")
  61. balance.add_argument("file", nargs="+", type=argparse.FileType("wb"),
  62. help="Path to a file where the address should be stored.")
  63. balance = subparsers.add_parser("show-balance",
  64. help="Shows the current balance of the public key "
  65. "stored in the specified file.")
  66. balance.add_argument("key", nargs="*", type=Signing.from_file)
  67. trans = subparsers.add_parser("show-transactions",
  68. help="Shows all transactions involving the public key "
  69. "stored in the specified file.")
  70. trans.add_argument("key", nargs="*", type=Signing.from_file)
  71. subparsers.add_parser("show-network",
  72. help="Prints networking information about the miner.")
  73. transfer = subparsers.add_parser("transfer", help="Transfer money.")
  74. transfer.add_argument("--private-key", type=private_signing,
  75. default=[], action="append", required=False,
  76. help="The private key(s) whose coins should be used for the transfer.")
  77. transfer.add_argument("--change-key", type=Signing.from_file, required=False,
  78. help="The private key where any remaining coins are sent to.")
  79. transfer.add_argument("--transaction-fee", type=int, default=0,
  80. help="The transaction fee you want to pay to the miner.")
  81. transfer.add_argument("target", nargs='*', metavar=("TARGET_KEY AMOUNT"),
  82. type=parse_targets(),
  83. help="The private key(s) whose coins should be used for the transfer.")
  84. args = parser.parse_args()
  85. rpc = RPCClient(args.miner_port)
  86. def show_transactions(keys: List[Signing]):
  87. for key in keys:
  88. for trans in rpc.get_transactions(key):
  89. print(trans.to_json_compatible())
  90. print()
  91. def create_address(wallet_keys: List[Signing], wallet_path: str, output_files: List[IOBase]):
  92. keys = [Signing.generate_private_key() for _ in output_files]
  93. Signing.write_many_private(wallet_path, wallet_keys + keys)
  94. for fp, key in zip(output_files, keys):
  95. fp.write(key.as_bytes())
  96. fp.close()
  97. def show_balance(keys: List[Signing]):
  98. total = 0
  99. for pubkey, balance in rpc.show_balance(keys):
  100. print("{}: {}".format(hexlify(pubkey.as_bytes()), balance))
  101. total += balance
  102. print()
  103. print("total: {}".format(total))
  104. def network_info():
  105. for k, v in rpc.network_info():
  106. print("{}\t{}".format(k, v))
  107. def transfer(targets: List[TransactionTarget], change_key: Optional[Signing],
  108. wallet_keys: List[Signing], wallet_path: str, priv_keys: List[Signing]):
  109. if not change_key:
  110. change_key = Signing.generate_private_key()
  111. Signing.write_many_private(wallet_path, wallet_keys + [change_key])
  112. trans = rpc.build_transaction(priv_keys, targets, change_key, args.transaction_fee)
  113. rpc.send_transaction(trans)
  114. def get_keys(keys: List[Signing]) -> List[Signing]:
  115. """
  116. Returns a combined list of keys from the `keys` and the wallet. Shows an error if empty.
  117. """
  118. all_keys = keys + args.wallet[0]
  119. if not all_keys:
  120. print("missing key or wallet", file=sys.stderr)
  121. parser.parse_args(["--help"])
  122. return all_keys
  123. if args.command == 'show-transactions':
  124. show_transactions(get_keys(args.key))
  125. elif args.command == "create-address":
  126. if not args.wallet[1]:
  127. print("no wallet specified", file=sys.stderr)
  128. parser.parse_args(["--help"])
  129. create_address(*args.wallet, args.file)
  130. elif args.command == 'show-balance':
  131. show_balance(get_keys(args.key))
  132. elif args.command == 'show-network':
  133. network_info()
  134. elif args.command == 'transfer':
  135. if len(args.target) % 2:
  136. print("Missing amount to transfer for last target key.\n", file=sys.stderr)
  137. parser.parse_args(["--help"])
  138. if not args.change_key and not args.wallet[0]:
  139. print("You need to specify either --wallet or --change-key.\n", file=sys.stderr)
  140. parser.parse_args(["--help"])
  141. targets = [TransactionTarget(k, a) for k, a in zip(args.target[::2], args.target[1::2])]
  142. transfer(targets, args.change_key, *args.wallet, get_keys(args.private_key))
  143. else:
  144. print("You need to specify what to do.\n", file=sys.stderr)
  145. parser.parse_args(["--help"])
  146. if __name__ == '__main__':
  147. main()