gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-bank] 02/02: transfer API idempotency


From: gnunet
Subject: [taler-bank] 02/02: transfer API idempotency
Date: Tue, 21 Jan 2020 16:54:55 +0100

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository bank.

commit b00298b72489214fa27c5ca93e63d05d43b3f820
Author: Florian Dold <address@hidden>
AuthorDate: Tue Jan 21 16:54:50 2020 +0100

    transfer API idempotency
---
 talerbank/app/migrations/0001_initial.py | 104 ++++++---------------------
 talerbank/app/models.py                  |   1 +
 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":

-- 
To stop receiving notification emails like this one, please contact
address@hidden.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]