summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2020-01-21 16:54:50 +0100
committerFlorian Dold <florian.dold@gmail.com>2020-01-21 16:54:50 +0100
commitb00298b72489214fa27c5ca93e63d05d43b3f820 (patch)
tree234e9eb4d02d57a3438fa5be79e12b368cea2ade
parent523591ec702a6896c9c5c70dfdab3b5b31f33cd0 (diff)
downloadbank-b00298b72489214fa27c5ca93e63d05d43b3f820.tar.gz
bank-b00298b72489214fa27c5ca93e63d05d43b3f820.tar.bz2
bank-b00298b72489214fa27c5ca93e63d05d43b3f820.zip
transfer API idempotency
-rw-r--r--talerbank/app/migrations/0001_initial.py104
-rw-r--r--talerbank/app/models.py1
-rw-r--r--talerbank/app/views.py119
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":