wallet.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  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.blockchain import Blockchain
  15. from src.block import Block
  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 main():
  70. parser = argparse.ArgumentParser(description="Wallet.")
  71. parser.add_argument("--miner-port", default=40203, type=int,
  72. help="The RPC port of the miner to connect to.")
  73. subparsers = parser.add_subparsers(dest="command")
  74. balance = subparsers.add_parser("show-balance",
  75. help="Shows the current balance of the public key "
  76. "stored in the specified file.")
  77. balance.add_argument("key", nargs="+", type=Signing.from_file)
  78. trans = subparsers.add_parser("show-transactions",
  79. help="Shows all transactions involving the public key "
  80. "stored in the specified file.")
  81. trans.add_argument("key", nargs="+", type=Signing.from_file)
  82. subparsers.add_parser("show-network",
  83. help="Prints networking information about the miner.")
  84. transfer = subparsers.add_parser("transfer", help="Transfer money.")
  85. transfer.add_argument("--private-key", type=private_signing,
  86. default=[], action="append", required=True,
  87. help="The private key(s) whose coins should be used for the transfer.")
  88. transfer.add_argument("--change-key", type=Signing.from_file, required=True,
  89. help="The private key where any remaining coins are sent to.")
  90. transfer.add_argument("--transaction-fee", type=int, default=0,
  91. help="The transaction fee you want to pay to the miner.")
  92. transfer.add_argument("target", nargs='*', metavar=("TARGET_KEY AMOUNT"),
  93. type=parse_targets(),
  94. help="The private key(s) whose coins should be used for the transfer.")
  95. args = parser.parse_args()
  96. url = "http://localhost:{}/".format(args.miner_port)
  97. s = requests.session()
  98. if args.command == 'show-transactions':
  99. for key in args.key:
  100. for trans in get_transactions(s, url, key):
  101. print(trans.to_json_compatible())
  102. elif args.command == 'show-balance':
  103. total = 0
  104. for pubkey, balance in show_balance(s, url, args.key):
  105. print("{}: {}".format(hexlify(pubkey.as_bytes()), balance))
  106. total += balance
  107. print()
  108. print("total: {}".format(total))
  109. elif args.command == 'show-network':
  110. for [k, v] in network_info(s, url):
  111. print("{}\t{}".format(k, v))
  112. elif args.command == 'transfer':
  113. if len(args.target) % 2:
  114. print("Missing amount to transfer for last target key.\n", file=sys.stderr)
  115. parser.parse_args(["--help"])
  116. targets = [TransactionTarget(k, a) for k, a in zip(args.target[::2], args.target[1::2])]
  117. build_transaction(s, url, args.private_key, targets, args.change_key, args.transaction_fee)
  118. else:
  119. print("You need to specify what to do.\n", file=sys.stderr)
  120. parser.parse_args(["--help"])
  121. if __name__ == '__main__':
  122. main()