wallet.py 6.8 KB

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