wallet.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  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. from src.blockchain import Blockchain
  13. from src.block import Block
  14. from src.transaction import Transaction, TransactionTarget, TransactionInput
  15. from src.crypto import Signing
  16. def send_transaction(sess, url, transaction):
  17. resp = sess.put(url + 'new-transaction', data=json.dumps(transaction.to_json_compatible()),
  18. headers={"Content-Type": "application/json"})
  19. resp.raise_for_status()
  20. def network_info(sess, url):
  21. resp = sess.get(url + 'network-info')
  22. resp.raise_for_status()
  23. return resp.json()
  24. def get_transactions(sess, url, pubkey):
  25. resp = sess.post(url + 'transactions', data=pubkey.as_bytes(),
  26. headers={"Content-Type": "application/json"})
  27. resp.raise_for_status()
  28. return [Transaction.from_json_compatible(t) for t in resp.json()]
  29. def show_balance(sess, url, pubkeys):
  30. resp = sess.post(url + "show-balance", data=json.dumps([pk.to_json_compatible() for pk in pubkeys]),
  31. headers={"Content-Type": "application/json"})
  32. resp.raise_for_status()
  33. return zip(pubkeys, resp.json())
  34. def build_transaction(sess, url, source_keys, targets, change_key):
  35. resp = sess.post(url + "build-transaction", data=json.dumps({
  36. "sender-pubkeys": [k.to_json_compatible() for k in source_keys],
  37. "amount": sum(t.amount for t in targets),
  38. }), headers={"Content-Type": "application/json"})
  39. resp.raise_for_status()
  40. resp = resp.json()
  41. remaining = resp['remaining_amount']
  42. if remaining < 0:
  43. print("You do not have sufficient funds for this transaction. ({} missing)".format(-remaining), file=sys.stderr)
  44. sys.exit(2)
  45. elif remaining > 0:
  46. targets = targets + [TransactionTarget(change_key, remaining)]
  47. inputs = [TransactionInput.from_json_compatible(i) for i in resp['inputs']]
  48. trans = Transaction(inputs, targets)
  49. trans.sign([source_keys[idx] for idx in resp['key_indices']])
  50. send_transaction(sess, url, trans)
  51. def parse_targets():
  52. start = True
  53. def parse(val):
  54. nonlocal start
  55. if start:
  56. val = Signing.from_file(val)
  57. else:
  58. val = int(val)
  59. start = not start
  60. return val
  61. return parse
  62. def private_signing(path):
  63. val = Signing.from_file(path)
  64. if not val.has_private:
  65. raise ValueError("The specified key is not a private key.")
  66. return val
  67. def main():
  68. parser = argparse.ArgumentParser(description="Wallet.")
  69. parser.add_argument("--miner-port", default=40203, type=int,
  70. help="The RPC port of the miner to connect to.")
  71. subparsers = parser.add_subparsers(dest="command")
  72. balance = subparsers.add_parser("show-balance",
  73. help="Shows the current balance of the public key "
  74. "stored in the specified file.")
  75. balance.add_argument("key", nargs="+", type=Signing.from_file)
  76. trans = subparsers.add_parser("show-transactions",
  77. help="Shows all transactions involving the public key "
  78. "stored in the specified file.")
  79. trans.add_argument("key", nargs="+", type=Signing.from_file)
  80. subparsers.add_parser("show-network",
  81. help="Prints networking information about the miner.")
  82. transfer = subparsers.add_parser("transfer", help="Transfer money.")
  83. transfer.add_argument("--private-key", type=private_signing,
  84. default=[], action="append", required=True,
  85. help="The private key(s) whose coins should be used for the transfer.")
  86. transfer.add_argument("--change-key", type=Signing.from_file, required=True,
  87. help="The private key where any remaining coins are sent to.")
  88. transfer.add_argument("target", nargs='*', metavar=("TARGET_KEY AMOUNT"),
  89. type=parse_targets(),
  90. help="The private key(s) whose coins should be used for the transfer.")
  91. args = parser.parse_args()
  92. url = "http://localhost:{}/".format(args.miner_port)
  93. s = requests.session()
  94. if args.command == 'show-transactions':
  95. for key in args.key:
  96. for trans in get_transactions(s, url, key):
  97. print(trans.to_json_compatible())
  98. elif args.command == 'show-balance':
  99. total = 0
  100. for pubkey, balance in show_balance(s, url, args.key):
  101. print("{}: {}".format(hexlify(pubkey.as_bytes()), balance))
  102. total += balance
  103. print()
  104. print("total: {}".format(total))
  105. elif args.command == 'show-network':
  106. for [k, v] in network_info(s, url):
  107. print("{}\t{}".format(k, v))
  108. elif args.command == 'transfer':
  109. if len(args.target) % 2:
  110. print("Missing amount to transfer for last target key.\n", file=sys.stderr)
  111. parser.parse_args(["--help"])
  112. targets = [TransactionTarget(k, a) for k, a in zip(args.target[::2], args.target[1::2])]
  113. build_transaction(s, url, args.private_key, targets, args.change_key)
  114. else:
  115. print("You need to specify what to do.\n", file=sys.stderr)
  116. parser.parse_args(["--help"])
  117. if __name__ == '__main__':
  118. main()