From 2e741c5a0be63969900e8eca3305ee5770c9ead0 Mon Sep 17 00:00:00 2001 From: Stanley Ding Date: Fri, 7 Oct 2016 13:04:26 +0800 Subject: [PATCH 1/2] Implement exchange API. --- oss_server/base/urls.py | 11 +--- oss_server/base/v1/views.py | 75 +++++++++++++++++++++++++- oss_server/oss_server/settings/base.py | 10 ++-- requirements.txt | 2 +- 4 files changed, 82 insertions(+), 16 deletions(-) diff --git a/oss_server/base/urls.py b/oss_server/base/urls.py index e33168a..ed4b21d 100644 --- a/oss_server/base/urls.py +++ b/oss_server/base/urls.py @@ -1,14 +1,6 @@ from django.conf.urls import url -from .v1.views import (CreateLicenseRawTxView, - CreateLicenseTransferRawTxView, - CreateMintRawTxView, - CreateSmartContractRawTxView, - CreateRawTxView, - GetBalanceView, - GetLicenseInfoView, - GetRawTxView, - SendRawTxView) +from .v1.views import * urlpatterns = [ url('^v1/balance/(?P
[a-zA-Z0-9]+)$', GetBalanceView.as_view()), @@ -22,5 +14,6 @@ url('^v1/smartcontract/send$', SendRawTxView.as_view()), url('^v1/transaction/prepare$', CreateRawTxView.as_view()), url('^v1/transaction/send$', SendRawTxView.as_view()), + url('^v1/exchange/prepare$', CreateExchangeRawTxView.as_view()), url('^v1/transaction/(?P[a-z0-9]+)$', GetRawTxView.as_view()), ] diff --git a/oss_server/base/v1/views.py b/oss_server/base/v1/views.py index 4eab34c..6984268 100644 --- a/oss_server/base/v1/views.py +++ b/oss_server/base/v1/views.py @@ -1,5 +1,7 @@ import httplib +import json import logging +from decimal import Decimal from django.conf import settings from django.http import JsonResponse @@ -11,9 +13,9 @@ from gcoinrpc import connect_to_remote from gcoinrpc.exceptions import InvalidAddressOrKey, InvalidParameter -from ..utils import balance_from_utxos, select_utxo, utxo_to_txin from .forms import (CreateLicenseRawTxForm, CreateLicenseTransferRawTxForm, CreateSmartContractRawTxForm, MintRawTxForm, RawTxForm) +from ..utils import balance_from_utxos, select_utxo, utxo_to_txin logger = logging.getLogger(__name__) @@ -315,3 +317,74 @@ def _get_license_utxo(self, utxos, color): if utxo['color'] == color: return utxo return None + + +class CreateExchangeRawTxView(CsrfExemptMixin, View): + + def post(self, request, *args, **kwargs): + if request.META['CONTENT_TYPE'] != 'application/json': + return JsonResponse({'error': "only accept 'application/json' as Content-Type"}) + + json_obj = json.loads(request.body, parse_float=Decimal) + print(json_obj) + try: + self._validate_json_obj(json_obj) + except KeyError as e: + return JsonResponse({'error': e.message}) + + addr_in_map = {} + addr_out_map = {} + + ins = [] + outs = [] + + # check if fee_address has enough fee. + utxos = get_rpc_connection().gettxoutaddress(json_obj['fee_address']) + inputs = select_utxo(utxos=utxos, color=1, sum=1) + if not inputs: + return JsonResponse({'error': 'insufficient fee in address {}'.format(json_obj['fee_address'])}) + ins += [utxo_to_txin(utxo) for utxo in inputs] + # the exchange part + for tx in json_obj['txs']: + addr1_in = addr_in_map.setdefault(tx['address1'], {}) + addr1_in[tx['color1']] = addr1_in.get(tx['color1'], 0) + tx['amount1'] + addr2_in = addr_in_map.setdefault(tx['address2'], {}) + addr2_in[tx['color2']] = addr1_in.get(tx['color2'], 0) + tx['amount2'] + + addr1_out = addr_out_map.setdefault(tx['address1'], {}) + addr1_out[tx['color2']] = addr1_in.get(tx['color2'], 0) + tx['amount2'] + addr2_out = addr_out_map.setdefault(tx['address2'], {}) + addr2_out[tx['color1']] = addr1_in.get(tx['color1'], 0) + tx['amount1'] + + for addr, color_amount_map in addr_in_map.items(): + utxos = get_rpc_connection().gettxoutaddress(addr) + for color, amount in color_amount_map.items(): + inputs = select_utxo(utxos=utxos, color=color, sum=amount) + if not inputs: + return JsonResponse({ + 'error': 'address {} does not have enough color {} to exchange'.format(addr, color) + }) + ins += [utxo_to_txin(utxo) for utxo in inputs] + + for addr, color_amount_map in addr_out_map.items(): + for color, amount in color_amount_map.items(): + outs = [{'address': addr, 'value': int(amount * 10**8), 'color': color}] + + raw_tx = make_raw_tx(ins, outs) + return JsonResponse({'raw_tx': raw_tx}) + + def _validate_json_obj(self, json_obj): + for key in ('fee_address', 'txs'): + if key not in json_obj: + raise KeyError('missing key: {}'.format(key)) + for tx in json_obj['txs']: + for key in ('address1', 'color1', 'amount1', 'address2', 'color2', 'amount2'): + if key not in tx: + raise KeyError('missing key in tx: {}'.format(key)) + + def _extract_addr_in_json(self, json_obj): + addr_set = set(json_obj['fee_address']) + for tx in json_obj['txs']: + addr_set.add(tx['address1']) + addr_set.add(tx['address2']) + return addr_set diff --git a/oss_server/oss_server/settings/base.py b/oss_server/oss_server/settings/base.py index 25ac048..58068ca 100644 --- a/oss_server/oss_server/settings/base.py +++ b/oss_server/oss_server/settings/base.py @@ -71,10 +71,10 @@ # Gcoin RPC GCOIN_RPC = { - 'user': '', - 'password': '', - 'host': '', - 'port': '', + 'user': 'gcoin', + 'password': 'abc123', + 'host': 'store1.diqi.us', + 'port': '9876', } @@ -135,7 +135,7 @@ STATIC_URL = '/static/' -LOG_DIR = os.path.dirname(BASE_DIR) + '/log/' +LOG_DIR = os.path.dirname(BASE_DIR) + '/log/' LOGGING = { 'version': 1, 'disable_existing_loggers': False, diff --git a/requirements.txt b/requirements.txt index f14e024..8c118c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,5 @@ mock==1.0.1 MySQL-python==1.2.5 tornado==4.4.1 -git+ssh://git@github.com/OpenNetworking/gcoin-rpc.git@develop#egg=gcoin-python +git+ssh://git@github.com/OpenNetworking/gcoin-rpc.git@develop git+ssh://git@github.com/OpenNetworking/pygcointools@master From 5007e92def41871f17839519318e35fec9dab6fe Mon Sep 17 00:00:00 2001 From: Stanley Ding Date: Wed, 12 Oct 2016 15:04:00 +0800 Subject: [PATCH 2/2] Redesign exchange API. Exchange API is not limited to exchange between 2 address. --- oss_server/base/v1/forms.py | 49 +++++++++++ oss_server/base/v1/views.py | 113 ++++++++++++------------- oss_server/oss_server/settings/base.py | 8 +- 3 files changed, 105 insertions(+), 65 deletions(-) diff --git a/oss_server/base/v1/forms.py b/oss_server/base/v1/forms.py index 39ceea6..0acf1d3 100644 --- a/oss_server/base/v1/forms.py +++ b/oss_server/base/v1/forms.py @@ -129,3 +129,52 @@ class CreateLicenseTransferRawTxForm(forms.Form): 'min_value': '`color_id` should be greater than or equal to %(limit_value)s', 'max_value': '`color_id` should be less than or equal to %(limit_value)s' }) + + +class ExchangeRawTxForm(forms.Form): + fee_address = AddressField(error_messages={ + 'required': '`fee_address` is required', + 'invalid': '`fee_address` is not an address' + }) + address1 = AddressField(error_messages={ + 'required': '`address1` is required', + 'invalid': '`address1` is not an address' + }) + address2 = AddressField(error_messages={ + 'required': '`address2` is required', + 'invalid': '`address2` is not an address' + }) + color_id1 = ColorField(error_messages={ + 'required': '`color_id1` is required', + 'invalid': '`color_id1` is invalid', + 'min_value': '`color_id1` should be greater than or equal to %(limit_value)s', + 'max_value': '`color_id1` should be less than or equal to %(limit_value)s' + }) + color_id2 = ColorField(error_messages={ + 'required': '`color_id2` is required', + 'invalid': '`color_id2` is invalid', + 'min_value': '`color_id2` should be greater than or equal to %(limit_value)s', + 'max_value': '`color_id2` should be less than or equal to %(limit_value)s' + }) + amount1 = TxAmountField(error_messages={ + 'required': '`amount1` is required', + 'invalid': '`amount1` is invalid', + 'min_value': '`amount1` should be greater than or equal to %(limit_value)s', + 'max_value': '`amount1` should be less than or equal to %(limit_value)s', + 'max_decimal_places': '`amount1` only allow up to %(max)s decimal digits' + }) + amount2 = TxAmountField(error_messages={ + 'required': '`amount2` is required', + 'invalid': '`amount2` is invalid', + 'min_value': '`amount2` should be greater than or equal to %(limit_value)s', + 'max_value': '`amount2` should be less than or equal to %(limit_value)s', + 'max_decimal_places': '`amount2` only allow up to %(max)s decimal digits' + }) + + def clean(self): + address1 = self.cleaned_data.get('address1') + address2 = self.cleaned_data.get('address2') + + if address1 and address2: + if address1 == address2: + raise forms.ValidationError("`address1` and `address2` can't be the same") diff --git a/oss_server/base/v1/views.py b/oss_server/base/v1/views.py index 6984268..daf0469 100644 --- a/oss_server/base/v1/views.py +++ b/oss_server/base/v1/views.py @@ -1,7 +1,5 @@ import httplib -import json import logging -from decimal import Decimal from django.conf import settings from django.http import JsonResponse @@ -13,8 +11,7 @@ from gcoinrpc import connect_to_remote from gcoinrpc.exceptions import InvalidAddressOrKey, InvalidParameter -from .forms import (CreateLicenseRawTxForm, CreateLicenseTransferRawTxForm, - CreateSmartContractRawTxForm, MintRawTxForm, RawTxForm) +from .forms import * from ..utils import balance_from_utxos, select_utxo, utxo_to_txin logger = logging.getLogger(__name__) @@ -319,72 +316,66 @@ def _get_license_utxo(self, utxos, color): return None -class CreateExchangeRawTxView(CsrfExemptMixin, View): +class CreateExchangeRawTxView(View): - def post(self, request, *args, **kwargs): - if request.META['CONTENT_TYPE'] != 'application/json': - return JsonResponse({'error': "only accept 'application/json' as Content-Type"}) + def get(self, request, *args, **kwargs): + form = ExchangeRawTxForm(request.GET) - json_obj = json.loads(request.body, parse_float=Decimal) - print(json_obj) - try: - self._validate_json_obj(json_obj) - except KeyError as e: - return JsonResponse({'error': e.message}) + if not form.is_valid(): + errors = ', '.join(reduce(lambda x, y: x + y, form.errors.values())) + response = {'error': errors} + return JsonResponse(response, status=httplib.BAD_REQUEST) - addr_in_map = {} - addr_out_map = {} + fee_address = form.cleaned_data['fee_address'] + address1 = form.cleaned_data['address1'] + address2 = form.cleaned_data['address2'] + color_id1 = form.cleaned_data['color_id1'] + color_id2 = form.cleaned_data['color_id2'] + amount1 = form.cleaned_data['amount1'] + amount2 = form.cleaned_data['amount2'] ins = [] outs = [] + fee_included = False + + for address, color_id, amount in [(address1, color_id1, amount1), (address2, color_id2, amount2)]: + utxos = get_rpc_connection().gettxoutaddress(address) + + inputs = select_utxo(utxos, color_id, amount) + if not inputs: + return JsonResponse({'error': 'insufficient funds'}, status=httplib.BAD_REQUEST) - # check if fee_address has enough fee. - utxos = get_rpc_connection().gettxoutaddress(json_obj['fee_address']) - inputs = select_utxo(utxos=utxos, color=1, sum=1) - if not inputs: - return JsonResponse({'error': 'insufficient fee in address {}'.format(json_obj['fee_address'])}) - ins += [utxo_to_txin(utxo) for utxo in inputs] - # the exchange part - for tx in json_obj['txs']: - addr1_in = addr_in_map.setdefault(tx['address1'], {}) - addr1_in[tx['color1']] = addr1_in.get(tx['color1'], 0) + tx['amount1'] - addr2_in = addr_in_map.setdefault(tx['address2'], {}) - addr2_in[tx['color2']] = addr1_in.get(tx['color2'], 0) + tx['amount2'] - - addr1_out = addr_out_map.setdefault(tx['address1'], {}) - addr1_out[tx['color2']] = addr1_in.get(tx['color2'], 0) + tx['amount2'] - addr2_out = addr_out_map.setdefault(tx['address2'], {}) - addr2_out[tx['color1']] = addr1_in.get(tx['color1'], 0) + tx['amount1'] - - for addr, color_amount_map in addr_in_map.items(): - utxos = get_rpc_connection().gettxoutaddress(addr) - for color, amount in color_amount_map.items(): - inputs = select_utxo(utxos=utxos, color=color, sum=amount) + inputs_value = balance_from_utxos(inputs)[color_id] + change = inputs_value - amount + + if color_id == 1 and address == fee_address: + inputs = select_utxo(utxos, color_id, amount + 1) if not inputs: - return JsonResponse({ - 'error': 'address {} does not have enough color {} to exchange'.format(addr, color) - }) - ins += [utxo_to_txin(utxo) for utxo in inputs] + return JsonResponse({'error': 'insufficient fee'}, status=httplib.BAD_REQUEST) + fee_included = True + inputs_value = balance_from_utxos(inputs)[color_id] + change = inputs_value - amount - 1 - for addr, color_amount_map in addr_out_map.items(): - for color, amount in color_amount_map.items(): - outs = [{'address': addr, 'value': int(amount * 10**8), 'color': color}] + if change: + outs.append({'address': address, + 'value': int(change * 10**8), 'color': color_id}) + ins += [utxo_to_txin(utxo) for utxo in inputs] + + if not fee_included: + fee_address_utxos = get_rpc_connection().gettxoutaddress(fee_address) + inputs = select_utxo(fee_address_utxos, 1, 1) + if not inputs: + return JsonResponse({'error': 'insufficient fee in fee_address'}, status=httplib.BAD_REQUEST) + ins += [utxo_to_txin(utxo) for utxo in inputs] + inputs_value = balance_from_utxos(inputs)[1] + change = inputs_value - 1 + + if change: + outs.append({'address': fee_address, + 'value': int(change * 10**8), 'color': 1}) + + outs.append({'address': address1, 'value': int(amount2 * 10**8), 'color': color_id2}) + outs.append({'address': address2, 'value': int(amount1 * 10**8), 'color': color_id1}) raw_tx = make_raw_tx(ins, outs) return JsonResponse({'raw_tx': raw_tx}) - - def _validate_json_obj(self, json_obj): - for key in ('fee_address', 'txs'): - if key not in json_obj: - raise KeyError('missing key: {}'.format(key)) - for tx in json_obj['txs']: - for key in ('address1', 'color1', 'amount1', 'address2', 'color2', 'amount2'): - if key not in tx: - raise KeyError('missing key in tx: {}'.format(key)) - - def _extract_addr_in_json(self, json_obj): - addr_set = set(json_obj['fee_address']) - for tx in json_obj['txs']: - addr_set.add(tx['address1']) - addr_set.add(tx['address2']) - return addr_set diff --git a/oss_server/oss_server/settings/base.py b/oss_server/oss_server/settings/base.py index 58068ca..bf66da7 100644 --- a/oss_server/oss_server/settings/base.py +++ b/oss_server/oss_server/settings/base.py @@ -71,10 +71,10 @@ # Gcoin RPC GCOIN_RPC = { - 'user': 'gcoin', - 'password': 'abc123', - 'host': 'store1.diqi.us', - 'port': '9876', + 'user': '', + 'password': '', + 'host': '', + 'port': '', }