diff --git a/bigchaindb/__init__.py b/bigchaindb/__init__.py index 4bb76c44..4982052f 100644 --- a/bigchaindb/__init__.py +++ b/bigchaindb/__init__.py @@ -39,7 +39,8 @@ config = { 'host': e('BIGCHAIN_STATSD_HOST', default='localhost'), 'port': e('BIGCHAIN_STATSD_PORT', default=8125), 'rate': e('BIGCHAIN_STATSD_SAMPLERATE', default=0.01) - } + }, + 'api_endpoint': 'http://localhost:8008/api/v1' } # We need to maintain a backup copy of the original config dict in case diff --git a/bigchaindb/client.py b/bigchaindb/client.py new file mode 100644 index 00000000..ad2944f2 --- /dev/null +++ b/bigchaindb/client.py @@ -0,0 +1,113 @@ +import requests + +import bigchaindb +from bigchaindb import util +from bigchaindb import config_utils +from bigchaindb import exceptions +from bigchaindb import crypto + + +class Client: + """Client for BigchainDB. + + A Client is initialized with a keypair and is able to create, sign, and submit transactions to a Node + in the Federation. At the moment, a Client instance is bounded to a specific ``host`` in the Federation. + In the future, a Client might connect to >1 hosts. + """ + + def __init__(self, public_key=None, private_key=None, api_endpoint=None): + """Initialize the Client instance + + There are three ways in which the Client instance can get its parameters. + The order by which the parameters are chosen are: + + 1. Setting them by passing them to the `__init__` method itself. + 2. Setting them as environment variables + 3. Reading them from the `config.json` file. + + Args: + public_key (str): the base58 encoded public key for the ECDSA secp256k1 curve. + private_key (str): the base58 encoded private key for the ECDSA secp256k1 curve. + host (str): hostname where the rethinkdb is running. + port (int): port in which rethinkb is running (usually 28015). + """ + + config_utils.autoconfigure() + + self.public_key = public_key or bigchaindb.config['keypair']['public'] + self.private_key = private_key or bigchaindb.config['keypair']['private'] + self.api_endpoint = api_endpoint or bigchaindb.config['api_endpoint'] + + if not self.public_key or not self.private_key: + raise exceptions.KeypairNotFoundException() + + def make_tx(self, new_owner, tx_input, operation='TRANSFER', payload=None): + """Make a new transaction + + Refer to the documentation of ``bigchaindb.util.create_tx`` + """ + + return util.create_tx(self.public_key, new_owner, tx_input, operation, payload) + + def sign_tx(self, tx): + """Sign a transaction + + Refer to the documentation of ``bigchaindb.util.sign_tx`` + """ + + return util.sign_tx(tx, self.private_key) + + def push_tx(self, tx): + """Submit a transaction to the Federation. + + Args: + tx (dict): the transaction to be pushed to the Federation. + + Return: + The transaction pushed to the Federation. + """ + + res = requests.post(self.api_endpoint + '/tx/', json=tx) + return res.json() + + def create(self, payload=None): + """Create a transaction. + + Args: + payload (dict): the payload for the transaction. + + Return: + The transaction pushed to the Federation. + """ + + tx = self.make_tx(self.public_key, None, operation='CREATE', payload=payload) + signed_tx = self.sign_tx(tx) + return self.push_tx(signed_tx) + + def transfer(self, new_owner, tx_input, payload=None): + """Transfer a transaction. + + Args: + new_owner (str): the public key of the new owner + tx_input (str): the id of the transaction to use as input + payload (dict, optional): the payload for the transaction. + + Return: + The transaction pushed to the Federation. + """ + + tx = self.make_tx(new_owner, tx_input, payload=payload) + signed_tx = self.sign_tx(tx) + return self.push_tx(signed_tx) + + +def temp_client(): + """Create a new temporary client. + + Return: + A client initialized with a keypair generated on the fly. + """ + + private_key, public_key = crypto.generate_key_pair() + return Client(private_key=private_key, public_key=public_key, api_endpoint='http://localhost:5000') + diff --git a/bigchaindb/core.py b/bigchaindb/core.py index d69b009c..9122c13f 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -43,8 +43,8 @@ class Bigchain(object): public_key (str): the base58 encoded public key for the ECDSA secp256k1 curve. private_key (str): the base58 encoded private key for the ECDSA secp256k1 curve. keyring (list[str]): list of base58 encoded public keys of the federation nodes. - """ + config_utils.autoconfigure() self.host = host or bigchaindb.config['database']['host'] self.port = port or bigchaindb.config['database']['port'] diff --git a/bigchaindb/processes.py b/bigchaindb/processes.py index 003f23ba..29289f5f 100644 --- a/bigchaindb/processes.py +++ b/bigchaindb/processes.py @@ -6,6 +6,7 @@ import rethinkdb as r from bigchaindb import Bigchain from bigchaindb.voter import Voter from bigchaindb.block import Block +from bigchaindb.web import server logger = logging.getLogger(__name__) @@ -80,3 +81,9 @@ class Processes(object): logger.info('starting voter') p_voter.start() + + # start the web api + webapi = server.create_app() + p_webapi = mp.Process(name='webapi', target=webapi.run, kwargs={'host': '0.0.0.0'}) + p_webapi.start() + diff --git a/setup.py b/setup.py index 27ef2e7b..30a7793c 100644 --- a/setup.py +++ b/setup.py @@ -74,6 +74,7 @@ setup( 'base58==0.2.2', 'bitcoin==1.1.42', 'flask==0.10.1', + 'requests==2.9', ], setup_requires=['pytest-runner'], tests_require=tests_require, diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 00000000..5943bb36 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,94 @@ +import pytest + + +@pytest.fixture +def client(): + from bigchaindb.client import temp_client + return temp_client() + + +@pytest.fixture +def mock_requests_post(monkeypatch): + class MockResponse: + def __init__(self, json): + self._json = json + + def json(self): + return self._json + + def mockreturn(*args, **kwargs): + return MockResponse(kwargs.get('json')) + + monkeypatch.setattr('requests.post', mockreturn) + + +def test_temp_client_returns_a_temp_client(): + from bigchaindb.client import temp_client + client = temp_client() + assert client.public_key + assert client.private_key + + +def test_client_can_make_transactions(client): + tx = client.make_tx('a', 123) + + assert tx['transaction']['current_owner'] == client.public_key + assert tx['transaction']['new_owner'] == 'a' + assert tx['transaction']['input'] == 123 + + +def test_client_can_sign_transactions(client): + from bigchaindb import util + + tx = client.make_tx('a', 123) + signed_tx = client.sign_tx(tx) + + assert signed_tx['transaction']['current_owner'] == client.public_key + assert signed_tx['transaction']['new_owner'] == 'a' + assert signed_tx['transaction']['input'] == 123 + + assert util.verify_signature(signed_tx) + + +def test_client_can_push_transactions(mock_requests_post, client): + from bigchaindb import util + + tx = client.make_tx('a', 123) + signed_tx = client.sign_tx(tx) + ret_tx = client.push_tx(signed_tx) + + assert ret_tx['transaction']['current_owner'] == client.public_key + assert ret_tx['transaction']['new_owner'] == 'a' + assert ret_tx['transaction']['input'] == 123 + + assert util.verify_signature(ret_tx) + + +def test_client_can_create_transactions_using_shortcut_method(mock_requests_post, client): + from bigchaindb import util + + tx = client.create() + + # XXX: `CREATE` operations require the node that receives the transaction to modify the data in + # the transaction itself. + # `current_owner` will be overwritten with the public key of the node in the federation + # that will create the real transaction. `signature` will be overwritten with the new signature. + # Note that this scenario is ignored by this test. + assert tx['transaction']['current_owner'] == client.public_key + assert tx['transaction']['new_owner'] == client.public_key + assert tx['transaction']['input'] == None + + assert util.verify_signature(tx) + + +def test_client_can_transfer_transactions_using_shortcut_method(mock_requests_post, client): + from bigchaindb import util + + tx = client.transfer('a', 123) + + assert tx['transaction']['current_owner'] == client.public_key + assert tx['transaction']['new_owner'] == 'a' + assert tx['transaction']['input'] == 123 + + assert util.verify_signature(tx) +