rpc_server.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. """ The RPC functionality the miner provides for the wallet and the blockchain explorer.
  2. All REST-API calls are defined here. """
  3. import binascii
  4. import json
  5. import time
  6. from binascii import hexlify
  7. from datetime import datetime
  8. from sys import maxsize
  9. import flask
  10. from flask_api import status
  11. from .chainbuilder import ChainBuilder
  12. from .crypto import Key
  13. from .persistence import Persistence
  14. from .config import DIFFICULTY_BLOCK_INTERVAL
  15. from .transaction import TransactionInput
  16. time_format = "%d.%m.%Y %H:%M:%S" # Defines the format string of timestamps in local time.
  17. app = flask.Flask(__name__)
  18. cb = None
  19. pers = None
  20. QUERY_PARAMETER_LIMIT = maxsize
  21. def datetime_from_utc_to_local(utc_datetime):
  22. """ Converts UTC timestamp to local timezone. """
  23. now_timestamp = time.time()
  24. offset = datetime.fromtimestamp(now_timestamp) - datetime.utcfromtimestamp(now_timestamp)
  25. return utc_datetime + offset
  26. def rpc_server(port: int, chainbuilder: ChainBuilder, persist: Persistence):
  27. """ Runs the RPC server (forever). """
  28. global cb
  29. cb = chainbuilder
  30. global pers
  31. pers = persist
  32. app.run(port=port)
  33. @app.route("/network-info", methods=['GET'])
  34. def get_network_info():
  35. """ Returns the connected peers.
  36. Route: `\"/network-info\"`.
  37. HTTP Method: `'GET'`
  38. """
  39. return json.dumps([list(peer.peer_addr)[:2] for peer in cb.protocol.peers if peer.is_connected])
  40. @app.route("/new-transaction", methods=['PUT'])
  41. def send_transaction():
  42. """
  43. Sends a transaction to the network, and uses it for mining.
  44. Route: `\"/new-transaction\"`.
  45. HTTP Method: `'PUT'`
  46. """
  47. cb.protocol.received("transaction", flask.request.json, None, 0)
  48. return b""
  49. @app.route("/show-balance", methods=['POST'])
  50. def show_balance():
  51. """
  52. Returns the balance of a number of public keys.
  53. Route: `\"/show-balance\"`.
  54. HTTP Method: `'POST'`
  55. """
  56. pubkeys = {Key.from_json_compatible(pk): i for (i, pk) in enumerate(flask.request.json)}
  57. amounts = [0 for _ in pubkeys.values()]
  58. for output in cb.primary_block_chain.unspent_coins.values():
  59. if output.get_pubkey in pubkeys:
  60. amounts[pubkeys[output.get_pubkey]] += output.amount
  61. return json.dumps(amounts)
  62. @app.route("/build-transaction", methods=['POST'])
  63. def build_transaction():
  64. """
  65. Returns the transaction inputs that can be used to build a transaction with a certain
  66. amount from some public keys.
  67. Route: `\"/build-transaction\"`.
  68. HTTP Method: `'POST'`
  69. """
  70. sender_pks = {
  71. Key.from_json_compatible(o): i
  72. for i, o in enumerate(flask.request.json['sender-pubkeys'])
  73. }
  74. amount = flask.request.json['amount']
  75. # TODO maybe give preference to the coins that are already unlocked when creating a transaction!
  76. inputs = []
  77. used_keys = []
  78. for (inp, output) in cb.primary_block_chain.unspent_coins.items():
  79. if (output.get_pubkey in sender_pks) and (
  80. not output.is_locked): # here we check is the amount is not locked before creating a Tx
  81. amount -= output.amount
  82. temp_input = TransactionInput(inp[0], inp[1], "empty sig_script")
  83. inputs.append(temp_input.to_json_compatible())
  84. used_keys.append(sender_pks[output.get_pubkey])
  85. if amount <= 0:
  86. break
  87. if amount > 0:
  88. inputs = []
  89. used_keys = []
  90. return json.dumps({
  91. "inputs": inputs,
  92. "remaining_amount": -amount,
  93. "key_indices": used_keys,
  94. })
  95. @app.route("/transaction", methods=['POST'])
  96. def get_transaction_for_hash():
  97. """
  98. Returns the transaction for provided hash.
  99. Route: `\"/transaction\"`.
  100. HTTP Method: `'POST'`
  101. """
  102. tx_hash = flask.request.data
  103. chain = cb.primary_block_chain
  104. for b in chain.blocks:
  105. for t in b.transactions:
  106. if t.get_hash() == tx_hash:
  107. return json.dumps(t.to_json_compatible())
  108. return json.dumps("")
  109. @app.route("/transactions", methods=['POST'])
  110. def get_transactions_for_key():
  111. """
  112. Returns all transactions involving a certain public key.
  113. Route: `\"/transactions\"`.
  114. HTTP Method: `'POST'`
  115. """
  116. key = Key(flask.request.data)
  117. transactions = set()
  118. outputs = set()
  119. chain = cb.primary_block_chain
  120. for b in chain.blocks:
  121. for t in b.transactions:
  122. for i, target in enumerate(t.targets):
  123. if target.get_pubkey == key:
  124. transactions.add(t)
  125. outputs.add((t.get_hash(), i))
  126. for b in chain.blocks:
  127. for t in b.transactions:
  128. for inp in t.inputs:
  129. if (inp.transaction_hash, inp.output_idx) in outputs:
  130. transactions.add(t)
  131. return json.dumps([t.to_json_compatible() for t in transactions])
  132. @app.route("/explorer/sortedtransactions/<string:key>", methods=['GET'])
  133. def get_sorted_transactions_for_key(key):
  134. """
  135. Returns all transactions involving a certain public key.
  136. Route: `\"/explorer/sortedtransactions/<string:key>\"`.
  137. HTTP Method: `'GET'`
  138. """
  139. key = Key(binascii.unhexlify(key))
  140. all_transactions = {}
  141. received_transactions = []
  142. sent_transactions = []
  143. outputs = set()
  144. chain = cb.primary_block_chain
  145. for b in chain.blocks:
  146. for t in b.transactions:
  147. for i, target in enumerate(t.targets):
  148. if target.get_pubkey == key:
  149. received_transactions.append(t.to_json_compatible())
  150. outputs.add((t.get_hash(), i))
  151. for b in chain.blocks:
  152. for t in b.transactions:
  153. for inp in t.inputs:
  154. if (inp.transaction_hash, inp.output_idx) in outputs:
  155. sent_transactions.append(t.to_json_compatible())
  156. for t in sent_transactions:
  157. t['timestamp'] = datetime_from_utc_to_local(datetime.strptime(t['timestamp'],
  158. "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
  159. time_format)
  160. for t in received_transactions:
  161. t['timestamp'] = datetime_from_utc_to_local(datetime.strptime(t['timestamp'],
  162. "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
  163. time_format)
  164. all_transactions["sent"] = sent_transactions
  165. all_transactions["received"] = received_transactions
  166. return json.dumps(all_transactions)
  167. @app.route("/explorer/addresses", methods=['GET'])
  168. def get_addresses():
  169. """
  170. Returns all addresses in the blockchain.
  171. Route: `\"/explorer/addresses\"`.
  172. HTTP Method: `'GET'`
  173. """
  174. addresses = set()
  175. chain = cb.primary_block_chain
  176. for b in chain.blocks:
  177. for t in b.transactions:
  178. for i, target in enumerate(t.targets):
  179. addresses.add(hexlify(target.get_pubkey.as_bytes()).decode())
  180. if len(addresses) != 0:
  181. return json.dumps([a for a in addresses])
  182. return json.dumps("Resource not found."), status.HTTP_404_NOT_FOUND
  183. @app.route("/explorer/show-balance", methods=['POST'])
  184. def show_single_balance():
  185. """
  186. Returns the balance of a public key.
  187. Route: `\"/explorer/show-balance\"`
  188. HTTP Method: `'POST'`
  189. """
  190. key = Key(flask.request.data)
  191. amount = 0
  192. for output in cb.primary_block_chain.unspent_coins.values():
  193. if output.get_pubkey == key:
  194. amount += output.amount
  195. result = {"credit": amount}
  196. return json.dumps(result)
  197. @app.route("/explorer/lasttransactions/<int:amount>", methods=['GET'])
  198. def get_last_transactions(amount):
  199. """
  200. Returns last transactions. Number is specified in `amount`.
  201. Route: `\"/explorer/lasttransactions/<int:amount>\"`
  202. HTTP Method: `'GET'`
  203. """
  204. last_transactions = []
  205. counter = 0
  206. unconfirmed_tx = cb.unconfirmed_transactions
  207. for (key, value) in unconfirmed_tx.items():
  208. if counter < amount:
  209. val = value.to_json_compatible()
  210. val['block_id'] = "Pending.."
  211. val['block_hash'] = ""
  212. val['number_confirmations'] = 0
  213. val['timestamp'] = datetime_from_utc_to_local(datetime.strptime(val['timestamp'],
  214. "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
  215. time_format)
  216. last_transactions.append(val)
  217. counter += 1
  218. else:
  219. break
  220. last_confirmed_transactions = []
  221. chain = cb.primary_block_chain
  222. for b in reversed(chain.blocks):
  223. if not counter < amount:
  224. break
  225. for t in reversed(b.transactions):
  226. if counter < amount:
  227. trans = t.to_json_compatible()
  228. block = b.to_json_compatible()
  229. trans['block_id'] = block['id']
  230. trans['block_hash'] = block['hash']
  231. trans['number_confirmations'] = chain.head.id - int(block['id'])
  232. trans['timestamp'] = datetime_from_utc_to_local(datetime.strptime(trans['timestamp'],
  233. "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
  234. time_format)
  235. last_confirmed_transactions.append(trans)
  236. counter += 1
  237. else:
  238. break
  239. last_transactions.extend(last_confirmed_transactions)
  240. return json.dumps(last_transactions)
  241. @app.route("/explorer/transactions", methods=['GET'])
  242. def get_transactions():
  243. """
  244. Returns all transactions.
  245. Route: `\"/explorer/transactions\"`
  246. HTTP Method: `'GET'`
  247. """
  248. transactions = []
  249. chain = cb.primary_block_chain
  250. for b in reversed(chain.blocks):
  251. for t in reversed(b.transactions):
  252. trans = t.to_json_compatible()
  253. block = b.to_json_compatible()
  254. trans['block_id'] = block['id']
  255. trans['block_hash'] = block['hash']
  256. trans['number_confirmations'] = chain.head.id - int(block['id'])
  257. transactions.append(trans)
  258. for t in transactions:
  259. t['timestamp'] = datetime_from_utc_to_local(datetime.strptime(t['timestamp'],
  260. "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
  261. time_format)
  262. return json.dumps(transactions)
  263. @app.route("/explorer/transaction/<string:hash>", methods=['GET'])
  264. def get_transaction_from_hash(hash):
  265. """
  266. Returns a transaction with specified hash.
  267. Route: `\"/explorer/transaction/<string:hash>\"`
  268. HTTP Method: `'GET'`
  269. """
  270. chain = cb.primary_block_chain
  271. for b in chain.blocks:
  272. for t in b.transactions:
  273. if hexlify(t.get_hash()).decode() == hash:
  274. trans = t.to_json_compatible()
  275. block = b.to_json_compatible()
  276. trans['block_id'] = block['id']
  277. trans['block_hash'] = block['hash']
  278. trans['number_confirmations'] = chain.head.id - int(block['id'])
  279. trans['timestamp'] = datetime_from_utc_to_local(datetime.strptime(trans['timestamp'],
  280. "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
  281. time_format)
  282. trans['fee'] = t.get_past_transaction_fee(chain)
  283. return json.dumps(trans)
  284. unconfirmed_tx = cb.unconfirmed_transactions
  285. for (key, value) in unconfirmed_tx.items():
  286. if hexlify(key).decode() == hash:
  287. trans = value.to_json_compatible()
  288. trans['block_id'] = ""
  289. trans['block_hash'] = "Pending..."
  290. trans['timestamp'] = datetime_from_utc_to_local(datetime.strptime(trans['timestamp'],
  291. "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
  292. time_format)
  293. trans['fee'] = t.get_past_transaction_fee(chain)
  294. return json.dumps(trans)
  295. return json.dumps("Resource not found."), status.HTTP_404_NOT_FOUND
  296. @app.route("/explorer/blocks", methods=['GET'])
  297. def get_blocks():
  298. """
  299. Returns all blocks in the blockchain.
  300. Route: `\"/explorer/blocks\"`
  301. HTTP Method: `'GET'`
  302. """
  303. chain = cb.primary_block_chain
  304. result = []
  305. for o in reversed(chain.blocks):
  306. block = o.to_json_compatible()
  307. block['time'] = datetime_from_utc_to_local(datetime.strptime(block['time'],
  308. "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
  309. time_format)
  310. result.append(block)
  311. return json.dumps(result)
  312. @app.route("/explorer/lastblocks/<int:amount>", methods=['GET'])
  313. def get_blocks_amount(amount):
  314. """
  315. Returns the latest number of blocks in the blockchain.
  316. Route: `\"/explorer/lastblocks/<int:amount>\"`
  317. HTTP Method: `'GET'`
  318. """
  319. result = []
  320. chain = cb.primary_block_chain
  321. counter = 0
  322. for b in reversed(chain.blocks):
  323. block = b.to_json_compatible()
  324. block['time'] = datetime_from_utc_to_local(datetime.strptime(block['time'],
  325. "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
  326. time_format)
  327. result.append(block)
  328. counter += 1
  329. if counter >= amount:
  330. break
  331. return json.dumps(result)
  332. @app.route("/explorer/blockat/<int:at>", methods=['GET'])
  333. def get_block_at(at):
  334. """
  335. Returns block at postion from zero in the blockchain.
  336. Route: `\"/explorer/blockat/<int:at>\"`
  337. HTTP Method: `'GET'`
  338. """
  339. chain = cb.primary_block_chain
  340. result = chain.blocks[at].to_json_compatible()
  341. result['time'] = datetime_from_utc_to_local(datetime.strptime(result['time'],
  342. "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
  343. time_format)
  344. return json.dumps(result)
  345. @app.route("/explorer/block/<string:hash>", methods=['GET'])
  346. def get_blocks_hash(hash):
  347. """
  348. Returns block with given hash from the blockchain.
  349. Route: `\"/explorer/block/<string:hash>\"`
  350. HTTP Method: `'GET'`
  351. """
  352. chain = cb.primary_block_chain
  353. for b in chain.blocks:
  354. if hexlify(b.hash).decode() == hash:
  355. block = b.to_json_compatible()
  356. block['time'] = datetime_from_utc_to_local(datetime.strptime(block['time'],
  357. "%Y-%m-%dT%H:%M:%S.%f UTC")).strftime(
  358. time_format)
  359. return json.dumps(block)
  360. return json.dumps("Resource not found."), status.HTTP_404_NOT_FOUND
  361. @app.route("/explorer/statistics/hashrate", methods=['GET'])
  362. def get_hashrate():
  363. """
  364. Returns the total amount of blocks.
  365. Route: `\"/explorer/statistics/hashrate\"`
  366. HTTP Method: `'GET'`
  367. """
  368. parameter = flask.request.args.get('length')
  369. if (parameter != None and isinstance(parameter, int)) and parameter > 0 and parameter < QUERY_PARAMETER_LIMIT:
  370. user_input_length = parameter
  371. else:
  372. user_input_length = DIFFICULTY_BLOCK_INTERVAL
  373. chain = cb.primary_block_chain
  374. if chain.head.id <= user_input_length:
  375. if (len(chain.blocks)) <= 2:
  376. return json.dumps(0)
  377. user_input_length = len(chain.blocks) - 1
  378. block_hashrate = []
  379. for i in range(user_input_length):
  380. first_block = chain.blocks[-i - 1]
  381. second_block = chain.blocks[-i - 2]
  382. first_time = first_block.time
  383. second_time = second_block.time
  384. difficulty = first_block.difficulty
  385. time_difference = abs((first_time - second_time).seconds)
  386. if time_difference == 0:
  387. time_difference = 1
  388. hashrate = (difficulty / time_difference)
  389. block_hashrate.append(hashrate)
  390. block_hashrate_sum = 0
  391. for i in block_hashrate:
  392. block_hashrate_sum += i
  393. block_hashrate_avg = block_hashrate_sum / len(block_hashrate)
  394. if block_hashrate_avg >= 1000000000:
  395. return json.dumps("%.1f" % (block_hashrate_avg / 1000000000) + " Gh/s")
  396. if block_hashrate_avg >= 1000000:
  397. return json.dumps("%.1f" % (block_hashrate_avg / 1000000) + " Mh/s")
  398. if block_hashrate_avg >= 1000:
  399. return json.dumps("%.1f" % (block_hashrate_avg / 1000) + " Kh/s")
  400. return json.dumps("%.2f" % block_hashrate_avg) # Returns float formatted with only 2 decimals
  401. @app.route("/explorer/statistics/tps", methods=['GET'])
  402. def get_tps():
  403. """
  404. Returns the average transaction rate over the last <length>- query parameter blocks.
  405. Route: `\"/explorer/statistics/tps\"`
  406. HTTP Method: `'GET'`
  407. """
  408. parameter = flask.request.args.get('length')
  409. if (parameter != None and isinstance(parameter, int)) and parameter > 0 and parameter < QUERY_PARAMETER_LIMIT:
  410. user_input_length = parameter
  411. else:
  412. user_input_length = DIFFICULTY_BLOCK_INTERVAL
  413. chain = cb.primary_block_chain
  414. if chain.head.id <= user_input_length:
  415. # if only genesis block exists, no transacions have been made
  416. if (len(chain.blocks)) <= 1:
  417. return json.dumps(0)
  418. first_block = chain.head
  419. # use block after genesis block, because genesis block has hard-coded timestamp
  420. second_block = chain.blocks[1]
  421. first_time = first_block.time
  422. second_time = second_block.time
  423. else:
  424. first_block = chain.head
  425. second_block = chain.blocks[- 1 - user_input_length]
  426. first_time = first_block.time
  427. second_time = second_block.time
  428. transactions = 0
  429. for i in range(user_input_length):
  430. transactions += len(chain.blocks[-1 - i].transactions)
  431. time_difference = abs((first_time - second_time).seconds)
  432. if time_difference == 0:
  433. time_difference = 1
  434. tps = transactions / time_difference
  435. return json.dumps("%.2f" % tps) # Returns float formatted with only 2 decimals
  436. @app.route("/explorer/statistics/totalblocks", methods=['GET'])
  437. def get_total_blocks():
  438. """
  439. Returns the total amount of blocks.
  440. Route: `\"/explorer/statistics/totalblocks\"`
  441. HTTP Method: `'GET'`
  442. """
  443. chain = cb.primary_block_chain
  444. total_blocks = len(chain.blocks)
  445. return json.dumps(total_blocks)
  446. @app.route("/explorer/statistics/target", methods=['GET'])
  447. def get_difficulty():
  448. """
  449. Returns the current target.
  450. Route: `\"/explorer/statistics/target\"`
  451. HTTP Method: `'GET'`
  452. """
  453. chain = cb.primary_block_chain
  454. current_difficulty = chain.head.difficulty
  455. return json.dumps(current_difficulty)
  456. @app.route("/explorer/statistics/blocktime", methods=['GET'])
  457. def get_blocktime():
  458. """
  459. Returns the average time between two blocks.
  460. Route: `\"/explorer/statistics/blocktime\"`
  461. HTTP Method: `'GET'`
  462. """
  463. chain = cb.primary_block_chain
  464. current_block = chain.head
  465. if len(chain.blocks) < 2:
  466. return json.dumps(0)
  467. second_block = chain.blocks[1]
  468. current_timestamp = current_block.time
  469. second_timestamp = second_block.time
  470. time_difference = abs((current_timestamp - second_timestamp).seconds)
  471. total_blocks = int(get_total_blocks()) - 1
  472. blocktime = time_difference / total_blocks
  473. return json.dumps("%.2f" % blocktime) # Returns float formatted with only 2 decimals
  474. @app.route('/shutdown', methods=['POST', 'GET'])
  475. def shutdown():
  476. """
  477. Shuts down the RPC-Server.
  478. Route: `\"/shutdown\"`
  479. HTTP Method: `'GET'/'POST'`
  480. """
  481. shutdown_server()
  482. return 'Server shutting down...'
  483. def shutdown_server():
  484. """
  485. Shuts down flask. Needed for pytests.
  486. """
  487. func = flask.request.environ.get('werkzeug.server.shutdown')
  488. if func is None:
  489. raise RuntimeError('Not running with the Werkzeug Server')
  490. func()