diff options
author | Florian Dold <florian.dold@gmail.com> | 2020-01-21 16:54:50 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2020-01-21 16:54:50 +0100 |
commit | b00298b72489214fa27c5ca93e63d05d43b3f820 (patch) | |
tree | 234e9eb4d02d57a3438fa5be79e12b368cea2ade | |
parent | 523591ec702a6896c9c5c70dfdab3b5b31f33cd0 (diff) | |
download | bank-b00298b72489214fa27c5ca93e63d05d43b3f820.tar.gz bank-b00298b72489214fa27c5ca93e63d05d43b3f820.tar.bz2 bank-b00298b72489214fa27c5ca93e63d05d43b3f820.zip |
transfer API idempotency
-rw-r--r-- | talerbank/app/migrations/0001_initial.py | 104 | ||||
-rw-r--r-- | talerbank/app/models.py | 1 | ||||
-rw-r--r-- | talerbank/app/views.py | 119 |
3 files changed, 95 insertions, 129 deletions
diff --git a/talerbank/app/migrations/0001_initial.py b/talerbank/app/migrations/0001_initial.py index 91c0e98..73a5206 100644 --- a/talerbank/app/migrations/0001_initial.py +++ b/talerbank/app/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.0.2 on 2020-01-13 12:57 +# Generated by Django 3.0.2 on 2020-01-21 15:47 from django.conf import settings from django.db import migrations, models @@ -17,95 +17,37 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name="BankAccount", + name='BankAccount', fields=[ - ("is_public", models.BooleanField(default=False)), - ("account_no", models.AutoField(primary_key=True, serialize=False)), - ( - "balance", - talerbank.app.models.SignedAmountField( - default=talerbank.app.models.get_zero_signed_amount - ), - ), - ( - "user", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), + ('is_public', models.BooleanField(default=False)), + ('account_no', models.AutoField(primary_key=True, serialize=False)), + ('balance', talerbank.app.models.SignedAmountField(default=talerbank.app.models.get_zero_signed_amount)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], ), migrations.CreateModel( - name="TalerWithdrawOperation", + name='TalerWithdrawOperation', fields=[ - ( - "withdraw_id", - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - ), - ), - ("amount", talerbank.app.models.AmountField(default=False)), - ("selection_done", models.BooleanField(default=False)), - ("withdraw_done", models.BooleanField(default=False)), - ("selected_reserve_pub", models.TextField(null=True)), - ( - "selected_exchange_account", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="selected_exchange_account", - to="app.BankAccount", - ), - ), - ( - "withdraw_account", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="withdraw_account", - to="app.BankAccount", - ), - ), + ('withdraw_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('amount', talerbank.app.models.AmountField(default=False)), + ('selection_done', models.BooleanField(default=False)), + ('withdraw_done', models.BooleanField(default=False)), + ('selected_reserve_pub', models.TextField(null=True)), + ('selected_exchange_account', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='selected_exchange_account', to='app.BankAccount')), + ('withdraw_account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='withdraw_account', to='app.BankAccount')), ], ), migrations.CreateModel( - name="BankTransaction", + name='BankTransaction', fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("amount", talerbank.app.models.AmountField(default=False)), - ( - "subject", - models.CharField(default="(no subject given)", max_length=200), - ), - ("date", models.DateTimeField(auto_now=True, db_index=True)), - ("cancelled", models.BooleanField(default=False)), - ( - "credit_account", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="credit_account", - to="app.BankAccount", - ), - ), - ( - "debit_account", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="debit_account", - to="app.BankAccount", - ), - ), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', talerbank.app.models.AmountField(default=False)), + ('subject', models.CharField(default='(no subject given)', max_length=200)), + ('date', models.DateTimeField(auto_now=True, db_index=True)), + ('cancelled', models.BooleanField(default=False)), + ('request_uid', models.CharField(max_length=128, unique=True)), + ('credit_account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='credit_account', to='app.BankAccount')), + ('debit_account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='debit_account', to='app.BankAccount')), ], ), ] diff --git a/talerbank/app/models.py b/talerbank/app/models.py index 08da111..3833c22 100644 --- a/talerbank/app/models.py +++ b/talerbank/app/models.py @@ -192,6 +192,7 @@ class BankTransaction(models.Model): subject = models.CharField(default="(no subject given)", max_length=200) date = models.DateTimeField(auto_now=True, db_index=True) cancelled = models.BooleanField(default=False) + request_uid = models.CharField(max_length=128, unique=True) class TalerWithdrawOperation(models.Model): diff --git a/talerbank/app/views.py b/talerbank/app/views.py index 9cd941d..f3fdbcc 100644 --- a/talerbank/app/views.py +++ b/talerbank/app/views.py @@ -27,6 +27,7 @@ import random import re import time import base64 +import uuid from urllib.parse import urlparse import django.contrib.auth import django.contrib.auth.views @@ -716,22 +717,25 @@ def serve_history(request, user_account): return JsonResponse(dict(data=history), status=200) + def expect_json_body_str(request, param_name): - body = json.loads(request.body) # FIXME: cache! + body = json.loads(request.body) # FIXME: cache! val = body[param_name] if not isinstance(val, str): # FIXME: throw right exception to be handled by middleware raise Exception(f"expected string for {param_name}") return val + def expect_json_body_amount(request, param_name): - body = json.loads(request.body) # FIXME: cache! + body = json.loads(request.body) # FIXME: cache! val = body[param_name] if not isinstance(val, str): # FIXME: throw right exception to be handled by middleware raise Exception(f"expected string for {param_name}") return Amount.parse(val) + def expect_param_str(request, param_name): val = request.GET[param_name] if not isinstance(val, str): @@ -770,24 +774,23 @@ def twg_add_incoming(request, user_account, acct_id): if acct_id != user_account.username: # FIXME: respond nicely - raise Exception(f"credentials do not match URL ('{acct_id}' vs '{user_account.username}')") + raise Exception( + f"credentials do not match URL ('{acct_id}' vs '{user_account.username}')" + ) reserve_pub = expect_json_body_str(request, "reserve_pub") debit_account_payto = expect_json_body_str(request, "debit_account") amount = expect_json_body_amount(request, "amount") debit_account_name = get_acct_from_payto(debit_account_payto) - print(f"adding incoming balance to exchange ({acct_id}) from account {debit_account_payto} ({debit_account_name})") + print( + f"adding incoming balance to exchange ({acct_id}) from account {debit_account_payto} ({debit_account_name})" + ) debit_user = User.objects.get(username=debit_account_name) debit_account = BankAccount.objects.get(user=debit_user) subject = f"{reserve_pub}" - wtrans = wire_transfer( - amount, - debit_account, - exchange_account, - subject, - ) + wtrans = wire_transfer(amount, debit_account, exchange_account, subject,) return JsonResponse( { @@ -809,7 +812,9 @@ def twg_transfer(request, user_account, acct_id): if acct_id != user_account.username: # FIXME: respond nicely - raise Exception(f"credentials do not match URL ('{acct_id}' vs '{user_account.username}')") + raise Exception( + f"credentials do not match URL ('{acct_id}' vs '{user_account.username}')" + ) request_uid = expect_json_body_str(request, "request_uid") wtid = expect_json_body_str(request, "wtid") @@ -823,17 +828,14 @@ def twg_transfer(request, user_account, acct_id): except User.DoesNotExist: LOGGER.error(f"credit account '{credit_account_name}' does not exist") # FIXME: use EC from taler-util library - return JsonResponse(dict(code=5110, error="credit account does not exist"), status=404) + return JsonResponse( + dict(code=5110, error="credit account does not exist"), status=404 + ) credit_account = BankAccount.objects.get(user=credit_user) subject = f"{wtid} {exchange_base_url}" - wtrans = wire_transfer( - amount, - exchange_account, - credit_account, - subject, - ) + wtrans = wire_transfer(amount, exchange_account, credit_account, subject, request_uid) return JsonResponse( { @@ -853,6 +855,7 @@ def get_payto_from_account(request, acct): h = get_plain_host(request) return f"payto://x-taler-bank/{h}/{acct.user.username}" + @require_GET @login_via_headers def twg_history_incoming(request, user_account, acct_id): @@ -863,21 +866,18 @@ def twg_history_incoming(request, user_account, acct_id): start = None else: start = int(start_str) - qs = query_history( - user_account.bankaccount, - "credit", - delta, - start, - ) + qs = query_history(user_account.bankaccount, "credit", delta, start,) for item in qs: - history.append(dict( - row_id=item.id, - amount=item.amount.stringify(settings.TALER_DIGITS), - date=dict(t_ms=(int(item.date.timestamp()) * 1000)), - reserve_pub=item.subject, # fixme: parse/truncate? - credit_account=get_payto_from_account(request, item.credit_account), - debit_account=get_payto_from_account(request, item.debit_account), - )) + history.append( + dict( + row_id=item.id, + amount=item.amount.stringify(settings.TALER_DIGITS), + date=dict(t_ms=(int(item.date.timestamp()) * 1000)), + reserve_pub=item.subject, # fixme: parse/truncate? + credit_account=get_payto_from_account(request, item.credit_account), + debit_account=get_payto_from_account(request, item.debit_account), + ) + ) return JsonResponse(dict(incoming_transactions=history), status=200) @@ -891,24 +891,21 @@ def twg_history_outgoing(request, user_account, acct_id): start = None else: start = int(start_str) - qs = query_history( - user_account.bankaccount, - "debit", - delta, - start, - ) + qs = query_history(user_account.bankaccount, "debit", delta, start,) for item in qs: # FIXME: proper parsing, more structure in subject wtid, exchange_base_url = item.subject.split(" ") - history.append(dict( - row_id=item.id, - amount=item.amount.stringify(settings.TALER_DIGITS), - date=dict(t_ms=(int(item.date.timestamp()) * 1000)), - wtid=wtid, - exchange_base_url=exchange_base_url, - credit_account=get_payto_from_account(request, item.credit_account), - debit_account=get_payto_from_account(request, item.debit_account), - )) + history.append( + dict( + row_id=item.id, + amount=item.amount.stringify(settings.TALER_DIGITS), + date=dict(t_ms=(int(item.date.timestamp()) * 1000)), + wtid=wtid, + exchange_base_url=exchange_base_url, + credit_account=get_payto_from_account(request, item.credit_account), + debit_account=get_payto_from_account(request, item.debit_account), + ) + ) return JsonResponse(dict(outgoing_transactions=history), status=200) @@ -1152,7 +1149,7 @@ def confirm_withdrawal(request, withdraw_id): raise Exception("not reached") -def wire_transfer(amount, debit_account, credit_account, subject): +def wire_transfer(amount, debit_account, credit_account, subject, request_uid=None): """ Make a wire transfer between two accounts of this demo bank. """ @@ -1160,6 +1157,31 @@ def wire_transfer(amount, debit_account, credit_account, subject): LOGGER.error("Debit and credit account are the same!") raise SameAccountException() + if request_uid is None: + request_uid = str(uuid.uuid4()) + pass + else: + # check for existing transfer + try: + etx = BankTransaction.objects.get(request_uid=request_uid) + except BankTransaction.DoesNotExist: + # We're good, no existing transaction with the same request_uid exists + pass + else: + if ( + etx.amount != amount + or etx.debit_account != debit_account + or etx.credit_account != debit_account + or etx.subject != subject + ): + return JsonResponse( + data=dict( + hint="conflicting transfer with same request_uid exists", + ec=5600, + ), + status=409, + ) + LOGGER.debug( "transfering %s => %s, %s, %s" % ( @@ -1175,6 +1197,7 @@ def wire_transfer(amount, debit_account, credit_account, subject): credit_account=credit_account, debit_account=debit_account, subject=subject, + request_uid=request_uid, ) if debit_account.user.username == "Bank": |