wallet.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  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 requests
  9. import sys
  10. import json
  11. from binascii import hexlify
  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. def send_transaction(sess, url, transaction):
  19. resp = sess.put(url + 'new-transaction', data=json.dumps(transaction.to_json_compatible()),
  20. headers={"Content-Type": "application/json"})
  21. resp.raise_for_status()
  22. def network_info(sess, url):
  23. resp = sess.get(url + 'network-info')
  24. resp.raise_for_status()
  25. return resp.json()
  26. def get_transactions(sess, url, pubkey):
  27. resp = sess.post(url + 'transactions', data=pubkey.as_bytes(),
  28. headers={"Content-Type": "application/json"})
  29. resp.raise_for_status()
  30. return [Transaction.from_json_compatible(t) for t in resp.json()]
  31. def show_balance(sess, url, pubkeys):
  32. resp = sess.post(url + "show-balance", data=json.dumps([pk.to_json_compatible() for pk in pubkeys]),
  33. headers={"Content-Type": "application/json"})
  34. resp.raise_for_status()
  35. return zip(pubkeys, resp.json())
  36. def build_transaction(sess, url, source_keys, targets, change_key, transaction_fee):
  37. resp = sess.post(url + "build-transaction", data=json.dumps({
  38. "sender-pubkeys": [k.to_json_compatible() for k in source_keys],
  39. "amount": sum(t.amount for t in targets) + transaction_fee,
  40. }), headers={"Content-Type": "application/json"})
  41. resp.raise_for_status()
  42. resp = resp.json()
  43. remaining = resp['remaining_amount']
  44. if remaining < 0:
  45. print("You do not have sufficient funds for this transaction. ({} missing)".format(-remaining), file=sys.stderr)
  46. sys.exit(2)
  47. elif remaining > 0:
  48. targets = targets + [TransactionTarget(change_key, remaining)]
  49. inputs = [TransactionInput.from_json_compatible(i) for i in resp['inputs']]
  50. trans = Transaction(inputs, targets)
  51. trans.sign([source_keys[idx] for idx in resp['key_indices']])
  52. send_transaction(sess, url, trans)
  53. def parse_targets():
  54. start = True
  55. def parse(val):
  56. nonlocal start
  57. if start:
  58. val = Signing.from_file(val)
  59. else:
  60. val = int(val)
  61. start = not start
  62. return val
  63. return parse
  64. def private_signing(path):
  65. val = Signing.from_file(path)
  66. if not val.has_private:
  67. raise ValueError("The specified key is not a private key.")
  68. return val
  69. def wallet_file(path):
  70. try:
  71. with open(path, "rb") as f:
  72. contents = f.read()
  73. except FileNotFoundError:
  74. return [], path
  75. return list(Signing.read_many_private(contents)), path
  76. def main():
  77. parser = argparse.ArgumentParser(description="Wallet.")
  78. parser.add_argument("--miner-port", default=40203, type=int,
  79. help="The RPC port of the miner to connect to.")
  80. parser.add_argument("--wallet", type=wallet_file, default=([],None),
  81. help="The wallet file containing the private keys to use.")
  82. subparsers = parser.add_subparsers(dest="command")
  83. balance = subparsers.add_parser("create-address",
  84. help="Creates new addresses and stores their secret keys in the wallet.")
  85. balance.add_argument("file", nargs="+", type=argparse.FileType("wb"),
  86. help="Path to a file where the address should be stored.")
  87. balance = subparsers.add_parser("show-balance",
  88. help="Shows the current balance of the public key "
  89. "stored in the specified file.")
  90. balance.add_argument("key", nargs="*", type=Signing.from_file)
  91. trans = subparsers.add_parser("show-transactions",
  92. help="Shows all transactions involving the public key "
  93. "stored in the specified file.")
  94. trans.add_argument("key", nargs="*", type=Signing.from_file)
  95. subparsers.add_parser("show-network",
  96. help="Prints networking information about the miner.")
  97. transfer = subparsers.add_parser("transfer", help="Transfer money.")
  98. transfer.add_argument("--private-key", type=private_signing,
  99. default=[], action="append", required=False,
  100. help="The private key(s) whose coins should be used for the transfer.")
  101. transfer.add_argument("--change-key", type=Signing.from_file, required=False,
  102. help="The private key where any remaining coins are sent to.")
  103. transfer.add_argument("--transaction-fee", type=int, default=0,
  104. help="The transaction fee you want to pay to the miner.")
  105. transfer.add_argument("target", nargs='*', metavar=("TARGET_KEY AMOUNT"),
  106. type=parse_targets(),
  107. help="The private key(s) whose coins should be used for the transfer.")
  108. args = parser.parse_args()
  109. url = "http://localhost:{}/".format(args.miner_port)
  110. s = requests.session()
  111. def get_keys(keys):
  112. all_keys = keys + args.wallet[0]
  113. if not all_keys:
  114. print("missing key or wallet", file=sys.stderr)
  115. parser.parse_args(["--help"])
  116. return all_keys
  117. if args.command == 'show-transactions':
  118. for key in get_keys(args.key):
  119. for trans in get_transactions(s, url, key):
  120. print(trans.to_json_compatible())
  121. print()
  122. elif args.command == "create-address":
  123. if not args.wallet[1]:
  124. print("no wallet specified", file=sys.stderr)
  125. parser.parse_args(["--help"])
  126. keys = [Signing.generate_private_key() for _ in args.file]
  127. Signing.write_many_private(args.wallet[1], args.wallet[0] + keys)
  128. for fp, key in zip(args.file, keys):
  129. fp.write(key.as_bytes())
  130. fp.close()
  131. elif args.command == 'show-balance':
  132. total = 0
  133. for pubkey, balance in show_balance(s, url, get_keys(args.key)):
  134. print("{}: {}".format(hexlify(pubkey.as_bytes()), balance))
  135. total += balance
  136. print()
  137. print("total: {}".format(total))
  138. elif args.command == 'show-network':
  139. for [k, v] in network_info(s, url):
  140. print("{}\t{}".format(k, v))
  141. elif args.command == 'transfer':
  142. if len(args.target) % 2:
  143. print("Missing amount to transfer for last target key.\n", file=sys.stderr)
  144. parser.parse_args(["--help"])
  145. targets = [TransactionTarget(k, a) for k, a in zip(args.target[::2], args.target[1::2])]
  146. change_key = args.change_key
  147. if not change_key:
  148. get_keys([]) # shows error if no wallet
  149. change_key = Signing.generate_private_key()
  150. Signing.write_many_private(args.wallet[1], args.wallet[0] + [change_key])
  151. build_transaction(s, url, get_keys(args.private_key), targets, change_key, args.transaction_fee)
  152. else:
  153. print("You need to specify what to do.\n", file=sys.stderr)
  154. parser.parse_args(["--help"])
  155. if __name__ == '__main__':
  156. main()