-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Janosch Gräf
committed
Feb 27, 2018
0 parents
commit 3991702
Showing
8 changed files
with
324 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
__pycache__ | ||
*.pyc | ||
*.egg-info |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from nimiqrpc import NimiqApi | ||
|
||
|
||
nimiq = NimiqApi() | ||
|
||
for account in nimiq.accounts(): | ||
balance = nimiq.get_balance(account["address"]) | ||
print("{!s}: {!s} NIM".format(account["address"], balance)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from pprint import pprint | ||
|
||
from nimiqrpc import NimiqApi | ||
|
||
|
||
nimiq = NimiqApi() | ||
pprint(nimiq.get_block_by_number(1)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .api import NimiqApi |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
__all__ = ["NimiqApi"] | ||
|
||
from binascii import hexlify, unhexlify | ||
|
||
from decimal import Decimal | ||
from typing import Union | ||
|
||
from .jsonrpc import JsonRpcClient | ||
from .util import satoshi_to_coin, ensure_satoshi | ||
|
||
|
||
class NimiqApi: | ||
""" | ||
API client for the Nimiq JSON RPC server. | ||
Examples: | ||
>>> nimiq = NimiqApi() | ||
>>> nimiq.get_block_by_number(0) | ||
{'accountsHash': 'c5ec6638e93eda82c1221c24083f9c6b0d85b227c1d14ead5e595eb3c4f272db', | ||
'bodyHash': '8223886130688ca40f9812eae66b0280e6ce2f72a743d08aac819be475b3ec4f', | ||
'difficulty': 1, | ||
'extraData': '', | ||
'hash': 'ca49936f6db63cad7cf73eb1e9da53dd497ad3b7068f31731024785973be9be6', | ||
'miner': '0000000000000000000000000000000000000000', | ||
'minerString': 'NQ07 0000 0000 0000 0000 0000 0000 0000 0000', | ||
'nonce': 104295, | ||
'number': 1, | ||
'parentHash': '0000000000000000000000000000000000000000000000000000000000000000', | ||
'pow': '00009505db53818e41456a83f79c19fe47a485bd6a486ac4accdef3ca8cde937', | ||
'size': 173, | ||
'timestamp': 0, | ||
'transactions': []} | ||
>>> nimiq.accounts() | ||
[{'id': 'f1ac032bfe0814331d2a57ce74f346c6c75b4eb6', 'address': 'NQ76 X6N0 6AYX 10A3 679A AY77 9US6 QT3M NKMN'}, | ||
{'id': '8629c69a857f715aff9a6e5b084eaa15af7730b2', 'address': 'NQ53 GQLU D6L5 FVQM MYUS DRDG GKMA 2NPP EC5J'}] | ||
>>> nimiq.get_balance("NQ53 GQLU D6L5 FVQM MYUS DRDG GKMA 2NPP EC5J") | ||
Decimal('83503.27457') | ||
>>> nimiq.send_transaction("NQ53 GQLU D6L5 FVQM MYUS DRDG GKMA 2NPP EC5J", \ | ||
"NQ76 X6N0 6AYX 10A3 679A AY77 9US6 QT3M NKMN", \ | ||
100000, 10) | ||
'74cee0799277e9f58bf558329ac64bb75ef432e6ca685e4d8986517204a148cf' | ||
""" | ||
|
||
def __init__(self, url="http://localhost:8648"): | ||
""" | ||
:param url: URL to RPC endpoint (default: ``http://localhost:8648``) | ||
""" | ||
self._rpc = JsonRpcClient(url) | ||
|
||
def send_raw_transaction(self, tx: bytes): | ||
""" | ||
Sends a raw transaction | ||
:param tx: Raw transaction (as bytes) | ||
:return: Transaction hash (as hex string) | ||
""" | ||
return self._rpc.call("sendRawTransaction", hexlify(tx).decode()) | ||
|
||
def send_transaction(self, from_addr: str, to_addr: str, value: Union[int, Decimal], fee: Union[int, Decimal]): | ||
""" | ||
Sends a transaction | ||
:param from_addr: Sender address | ||
:param to_addr: Receiver address | ||
:param value: Value in Satoshis (int) or NIMs (Decimal) | ||
:param fee: Fee in Satoshis (int) or NIMs (Decimal) | ||
:return: Transaction hash | ||
""" | ||
return self._rpc.call("sendTransaction", { | ||
"from": from_addr, | ||
"to": to_addr, | ||
"value": ensure_satoshi(value), | ||
"fee": ensure_satoshi(fee), | ||
}) | ||
|
||
|
||
def get_transaction_by_hash(self, hash: str): | ||
""" | ||
Gets transaction data by transaction hash. | ||
:param hash: Transaction hash | ||
:return: Transaction data as dict | ||
""" | ||
tx = self._rpc.call("getTransactionByHash", hash) | ||
if tx.get("data") is not None: | ||
tx["data"] = unhexlify(tx["data"]) | ||
# NOTE: The official client doesn't return raw transaction data | ||
if tx.get("raw") is not None: | ||
tx["raw"] = unhexlify(tx["raw"]) | ||
return tx | ||
|
||
# TODO: getTransactionByBlockHashAndIndex, | ||
# getTransactionByBlockNumberAndIndex, | ||
# getTransactionReceipt | ||
|
||
def mempool(self): | ||
""" | ||
:return: A list of transactions that are currently in the mempool. | ||
""" | ||
return self._rpc.call("mempool") | ||
|
||
def mining(self) -> bool: | ||
""" | ||
:return: True if mining is enabled, False otherwise. | ||
""" | ||
return self._rpc.call("mempool") | ||
|
||
def hashrate(self): | ||
""" | ||
:return: The current hashrate (in Hash/s), if mining is enabled. | ||
""" | ||
return self._rpc.call("hashrate") | ||
|
||
def accounts(self): | ||
""" | ||
:return: Returns a list of accounts. A account is a dict of account_id and address. | ||
""" | ||
return self._rpc.call("accounts") | ||
|
||
def create_account(self): | ||
""" | ||
Creates a new account | ||
:return: The wallet as dict with account_id, address and public_key | ||
""" | ||
return self._rpc.call("createAccount") | ||
|
||
def get_balance(self, address: str) -> Decimal: | ||
""" | ||
Returns the balance for an address. | ||
:param address: The address | ||
:return: The balance in NIM (as Decimal) | ||
""" | ||
return satoshi_to_coin(self._rpc.call("getBalance", address)) | ||
|
||
def block_number(self): | ||
""" | ||
:return: The current block chain height. | ||
""" | ||
return self._rpc.call("blockNumber") | ||
|
||
def get_block_by_hash(self, hash: str, include_txs: bool = False) -> dict: | ||
""" | ||
Get block by hash | ||
:param hash: Block hash | ||
:param include_txs: Whether to include transactions | ||
:return: The block | ||
""" | ||
return self._rpc.call("getBlockByHash", hash, include_txs) | ||
|
||
def get_block_by_number(self, number: int, include_txs: bool = False) -> dict: | ||
""" | ||
Get block by number | ||
:param hash: Block number | ||
:param include_txs: Whether to include transactions | ||
:return: The block | ||
""" | ||
return self._rpc.call("getBlockByNumber", number, include_txs) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
__all__ = ["JsonRpcError", "JsonRpcRemoteException", "JsonRpcClient"] | ||
|
||
from itertools import count | ||
import logging | ||
|
||
import requests | ||
|
||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class JsonRpcError(Exception): | ||
""" Internal error during a JSON RPC request. """ | ||
pass | ||
|
||
|
||
class JsonRpcRemoteException(Exception): | ||
""" Exception on the remote server """ | ||
pass | ||
|
||
|
||
class JsonRpcClient: | ||
""" A very minimal JSON RPC client """ | ||
|
||
def __init__(self, url): | ||
""" | ||
Creates a JSON RPC client | ||
:param url: The URL to the API endpoint | ||
""" | ||
self.url = url | ||
self.session = requests.Session() | ||
self.call_ids = count(1) | ||
|
||
def call(self, method, *args): | ||
""" | ||
Calls a remote procedure | ||
:param method: Method name | ||
:param args: Argument list | ||
:return: The result of the remote procedure | ||
NOTE: Since the Nimiq JSON RPC server only supports positional arguments, we don't care about keyword arguments. | ||
""" | ||
|
||
call_id = next(self.call_ids) | ||
rpc_req = { | ||
"jsonrpc": "2.0", | ||
"method": method, | ||
"params": args, | ||
"id": call_id | ||
} | ||
logger.info("Request: {!r}".format(rpc_req)) | ||
|
||
resp = requests.post( | ||
self.url, | ||
json=rpc_req | ||
) | ||
resp.raise_for_status() | ||
|
||
rpc_resp = resp.json() | ||
logger.info("Response (call_id={:d}): {!r}".format(call_id, rpc_resp)) | ||
if rpc_resp.get("id") != call_id: | ||
raise JsonRpcError("Response with incorrect call ID. Expected {:d}, but received {:d}".format(call_id, rpc_resp.get("id"))) | ||
|
||
result = rpc_resp.get("result") | ||
error = rpc_resp.get("error") | ||
if error is not None: | ||
raise JsonRpcRemoteException(rpc_req, rpc_resp) | ||
return result |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
__all__ = ["satoshi_to_coin", "coin_to_satoshi", "ensure_coin", "block_listener"] | ||
|
||
import logging | ||
|
||
from decimal import Decimal | ||
from time import sleep | ||
from typing import Union | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
SATOSHI_PER_COIN = 100000 | ||
|
||
|
||
def satoshi_to_coin(satoshi: int) -> Decimal: | ||
""" | ||
Converts a value in Satoshis into Coins (NIMs). | ||
:param satoshi: The value in Satoshis as int | ||
:return: The value in coins as Decimal | ||
""" | ||
return Decimal(satoshi) / SATOSHI_PER_COIN | ||
|
||
|
||
def coin_to_satoshi(coin: Decimal) -> int: | ||
""" | ||
Converts a value in Coins (NIMs) into Satoshis | ||
:param coin: The value in coins as Decimal | ||
:return: The value in Satoshi as int | ||
""" | ||
return int(coin * SATOSHI_PER_COIN) | ||
|
||
|
||
def ensure_satoshi(value: Union[int, Decimal]) -> int: | ||
""" | ||
Ensures the returned value is in Satoshi. If the value was an int, it's already in Satoshi. If the value was an | ||
Decimal, it was in Coins and will be converted. | ||
:param value: The value in Satoshis (int) or Coins (Decimal). | ||
:return: The value in Satoshis (int) | ||
""" | ||
if type(value) == Decimal: | ||
return coin_to_satoshi(value) | ||
elif type(value) == int: | ||
return value | ||
else: | ||
raise ValueError("Expected value to be either int or Decimal") | ||
|
||
|
||
def block_listener(nimiq): | ||
""" | ||
An interable that yields blocks as they are added to the block chain. | ||
:param nimiq: The Nimiq API | ||
:return: The iterable that yields new blocks. | ||
""" | ||
height = nimiq.get_block_height() | ||
while True: | ||
block = nimiq.get_block_by_number(height) | ||
if block is None: | ||
sleep(1) | ||
else: | ||
yield block | ||
height += 1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
from setuptools import setup | ||
|
||
setup( | ||
name='nimiq-api-python', | ||
version='0.0.1', | ||
description='A python client for the Nimiq JSON-RPC API', | ||
url='http://github.com/jgraef/nimiq-api-python', | ||
author='Janosch Gräf', | ||
author_email='janosch.graef@cispa.saarland', | ||
license='MIT', | ||
packages=['nimiqrpc'], | ||
zip_safe=True, | ||
install_requires=[ | ||
'requests' | ||
], | ||
) |