diff --git a/polaris/polaris/deposit/views.py b/polaris/polaris/deposit/views.py index 4a0218d66..a05db1484 100644 --- a/polaris/polaris/deposit/views.py +++ b/polaris/polaris/deposit/views.py @@ -12,6 +12,7 @@ from polaris import settings from django.urls import reverse +from django.shortcuts import redirect from django.views.decorators.clickjacking import xframe_options_exempt from rest_framework import status from rest_framework.decorators import api_view, renderer_classes @@ -158,17 +159,20 @@ def interactive_deposit(request: Request) -> Response: if not transaction_id: return render_error_response("no 'transaction_id' provided", content_type="text/html") + try: + transaction = Transaction.objects.get(id=transaction_id) + except Transaction.objects.DoesNotExist: + return render_error_response( + "Transaction with ID not found", + content_type="text/html", + status_code=status.HTTP_404_NOT_FOUND + ) + # GET: The server needs to display the form for the user to input the deposit information. if request.method == "GET": form = registered_deposit_integration.form() return Response({"form": form}, template_name="deposit/form.html") - # POST: The user submitted a form with the amount to deposit. - if Transaction.objects.filter(id=transaction_id).exists(): - return render_error_response( - "transaction with matching 'transaction_id' already exists", - content_type="text/html" - ) form = registered_deposit_integration.form(request.POST) asset = Asset.objects.get(code=asset_code) form.asset = asset @@ -179,33 +183,13 @@ def interactive_deposit(request: Request) -> Response: if hasattr(form, "after_validation") and callable(form.after_validation): form.after_validation() - amount_in = form.cleaned_data["amount"] - amount_fee = calc_fee(asset, settings.OPERATION_DEPOSIT, amount_in) - transaction = Transaction( - id=transaction_id, - stellar_account=account, - asset=asset, - kind=Transaction.KIND.deposit, - status=Transaction.STATUS.pending_user_transfer_start, - amount_in=amount_in, - amount_fee=amount_fee, - to_address=account, + transaction.amount_in = form.cleaned_data["amount"] + transaction.amount_fee = calc_fee( + asset, settings.OPERATION_DEPOSIT, transaction.amount_in ) transaction.save() - serializer = TransactionSerializer( - transaction, - context={"more_info_url": _construct_more_info_url(request)}, - ) - tx_json = json.dumps({"transaction": serializer.data}) - return Response( - { - "tx_json": tx_json, - "transaction": transaction, - "asset_code": transaction.asset.code, - }, - template_name="transaction/more_info.html", - ) + return redirect(f"{reverse('more_info')}?{urlencode({'id': transaction_id})}") else: return Response({"form": form}, template_name="deposit/form.html") @@ -244,6 +228,14 @@ def deposit(account: str, request: Request) -> Response: # Construct interactive deposit pop-up URL. transaction_id = create_transaction_id() + Transaction.objects.create( + id=transaction_id, + stellar_account=account, + asset=asset, + kind=Transaction.KIND.deposit, + status=Transaction.STATUS.pending_user_transfer_start, + to_address=account + ) url = _construct_interactive_url(request, asset_code, stellar_account, transaction_id) return Response( {"type": "interactive_customer_info_needed", "url": url, "id": transaction_id}, diff --git a/polaris/polaris/fee/views.py b/polaris/polaris/fee/views.py index ff90e2cc8..34e6ee376 100644 --- a/polaris/polaris/fee/views.py +++ b/polaris/polaris/fee/views.py @@ -45,7 +45,7 @@ def fee(account: str, request: Request) -> Response: amount_str = request.GET.get("amount") try: amount = float(amount_str) - except (TypeError, ValueError): + except (ValueError, TypeError): return render_error_response("invalid 'amount'") # Validate that the operation, and the specified type (if provided) diff --git a/polaris/polaris/tests/conftest.py b/polaris/polaris/tests/conftest.py index 06f44a05d..1b93383d0 100644 --- a/polaris/polaris/tests/conftest.py +++ b/polaris/polaris/tests/conftest.py @@ -7,8 +7,10 @@ from polaris.models import Asset, Transaction -STELLAR_ACCOUNT_1 = "GBCTKB22TYTLXHDWVENZGWMJWJ5YK2GTSF7LHAGMTSNAGLLSZVXRGXEW" -STELLAR_ACCOUNT_2 = "GAB4FHP66SOQ4L22WQGW7BQCHGWRFWXQ6MWBZV2YRVTXSK3QPNFOTM3T" +STELLAR_ACCOUNT_1 = "GAIRMDK7VDAXKXCX54UQ7WQUXZVITPBBYH33ADXQIADMDTDVJMQGBQ6V" +STELLAR_ACCOUNT_1_SEED = "SBB57BRFU7OFBVGUNJH4PMTQR72VCGKKFXBRQJJX7CHRSTZATAB5645L" +STELLAR_ACCOUNT_2 = "GAWGLF7Y6WFNPMFLIZ7AZU7TCHRRMTVKSB64XUSLJUGMXS3KFCOZXJWC" +STELLAR_ACCOUNT_2_SEED = "SAANDCFGMTWUQX27URREU47QL2HSJRCTB6YXZIOBHZJCAUBBEFJTGASY" @pytest.fixture(scope="session", name="usd_asset_factory") diff --git a/polaris/polaris/tests/deposit_test.py b/polaris/polaris/tests/deposit_test.py index 74649d8e5..83d76b2e4 100644 --- a/polaris/polaris/tests/deposit_test.py +++ b/polaris/polaris/tests/deposit_test.py @@ -13,6 +13,7 @@ from django.core.management import call_command from polaris import settings +from polaris.tests.conftest import STELLAR_ACCOUNT_1_SEED from polaris.management.commands.create_stellar_deposit import ( SUCCESS_XDR, TRUSTLINE_FAILURE_XDR, @@ -304,6 +305,7 @@ def test_deposit_confirm_success( follow=True, **header ) + assert response.status_code == 200 content = json.loads(response.content) transaction = content["transaction"] @@ -429,9 +431,7 @@ def test_deposit_stellar_success( @pytest.mark.django_db @patch("stellar_sdk.server.Server.fetch_base_fee", return_value=100) @patch("stellar_sdk.server.Server.submit_transaction", return_value=HORIZON_SUCCESS_RESPONSE) -@patch("polaris.helpers.check_auth", side_effect=mock_check_auth_success) def test_deposit_interactive_confirm_success( - mock_check, mock_submit, mock_base_fee, client, @@ -441,11 +441,16 @@ def test_deposit_interactive_confirm_success( `GET /deposit` and `GET /transactions/deposit/webapp` succeed with valid `account` and `asset_code`. """ - del mock_check, mock_submit, mock_base_fee + del mock_submit, mock_base_fee deposit = acc1_usd_deposit_transaction_factory() + + encoded_jwt = sep10(client, deposit.stellar_account, STELLAR_ACCOUNT_1_SEED) + header = {"HTTP_AUTHORIZATION": f"Bearer {encoded_jwt}"} + response = client.post( DEPOSIT_PATH, {"asset_code": "USD", "account": deposit.stellar_account}, - follow=True + follow=True, + **header ) content = json.loads(response.content) assert response.status_code == 200 @@ -455,14 +460,12 @@ def test_deposit_interactive_confirm_success( url = content["url"] amount = 20 response = client.post(url, {"amount": amount}) - assert response.status_code == 200 + assert response.status_code == 302 assert ( Transaction.objects.get(id=transaction_id).status == Transaction.STATUS.pending_user_transfer_start ) - encoded_jwt = sep10(client, client_address, client_seed) - header = {"HTTP_AUTHORIZATION": f"Bearer {encoded_jwt}"} response = client.get( f"/transactions/deposit/confirm_transaction?amount={amount}&transaction_id={transaction_id}", follow=True, diff --git a/polaris/polaris/tests/withdraw_test.py b/polaris/polaris/tests/withdraw_test.py index 27b792cb5..15d7f1c43 100644 --- a/polaris/polaris/tests/withdraw_test.py +++ b/polaris/polaris/tests/withdraw_test.py @@ -124,7 +124,7 @@ def test_withdraw_interactive_failure_no_memotype( response = client.post( url, {"amount": 20, "bank_account": "123456", "bank": "Bank"} ) - assert response.status_code == 200 + assert response.status_code == 302 assert ( Transaction.objects.get(id=transaction_id).status == Transaction.STATUS.pending_user_transfer_start @@ -154,7 +154,7 @@ def test_withdraw_interactive_failure_incorrect_memotype( response = client.post( url, {"amount": 20, "bank_account": "123456", "bank": "Bank"} ) - assert response.status_code == 200 + assert response.status_code == 302 assert ( Transaction.objects.get(id=transaction_id).status == Transaction.STATUS.pending_user_transfer_start @@ -184,7 +184,7 @@ def test_withdraw_interactive_failure_no_memo( response = client.post( url, {"amount": 20, "bank_account": "123456", "bank": "Bank"} ) - assert response.status_code == 200 + assert response.status_code == 302 assert ( Transaction.objects.get(id=transaction_id).status == Transaction.STATUS.pending_user_transfer_start @@ -214,7 +214,7 @@ def test_withdraw_interactive_failure_incorrect_memo( response = client.post( url, {"amount": 20, "bank_account": "123456", "bank": "Bank"} ) - assert response.status_code == 200 + assert response.status_code == 302 assert ( Transaction.objects.get(id=transaction_id).status == Transaction.STATUS.pending_user_transfer_start @@ -241,7 +241,7 @@ def test_withdraw_interactive_success_transaction_unsuccessful( response = client.post( url, {"amount": 50, "bank_account": "123456", "bank": "Bank"} ) - assert response.status_code == 200 + assert response.status_code == 302 transaction = Transaction.objects.get(id=transaction_id) assert transaction.status == Transaction.STATUS.pending_user_transfer_start @@ -280,7 +280,7 @@ def test_withdraw_interactive_success_transaction_successful( response = client.post( url, {"amount": 50, "bank_account": "123456", "bank": "Bank"} ) - assert response.status_code == 200 + assert response.status_code == 302 transaction = Transaction.objects.get(id=transaction_id) assert transaction.status == Transaction.STATUS.pending_user_transfer_start diff --git a/polaris/polaris/withdraw/views.py b/polaris/polaris/withdraw/views.py index 85a580ad4..b0b76aaa7 100644 --- a/polaris/polaris/withdraw/views.py +++ b/polaris/polaris/withdraw/views.py @@ -8,11 +8,14 @@ from polaris import settings from django.urls import reverse +from django.core.exceptions import ValidationError from django.views.decorators.clickjacking import xframe_options_exempt +from django.shortcuts import redirect from rest_framework.decorators import api_view, renderer_classes from rest_framework.response import Response from rest_framework.request import Request from rest_framework.renderers import TemplateHTMLRenderer, JSONRenderer +from rest_framework import status from polaris.helpers import ( render_error_response, @@ -66,18 +69,21 @@ def interactive_withdraw(request: Request) -> Response: if not asset_code or not Asset.objects.filter(code=asset_code).exists(): return render_error_response("invalid 'asset_code'", content_type="text/html") + try: + transaction = Transaction.objects.get(id=transaction_id) + except (Transaction.DoesNotExist, ValidationError): + return render_error_response( + "Transaction with ID not found", + content_type="text/html", + status_code=status.HTTP_404_NOT_FOUND + ) + # GET: The server needs to display the form for the user to input withdrawal information. if request.method == "GET": form = registered_withdrawal_integration.form() resp_data = {"form": form, "account": request.GET.get("account")} return Response(resp_data, template_name="withdraw/form.html") - # POST: The user submitted a form with the withdrawal info. - if Transaction.objects.filter(id=transaction_id).exists(): - return render_error_response( - "transaction with matching 'transaction_id' already exists", - content_type="text/html" - ) form = registered_withdrawal_integration.form(request.POST) asset = Asset.objects.get(code=asset_code) form.asset = asset @@ -89,44 +95,13 @@ def interactive_withdraw(request: Request) -> Response: if hasattr(form, "after_validation") and callable(form.after_validation): form.after_validation() - amount_in = form.cleaned_data["amount"] - amount_fee = calc_fee(asset, settings.OPERATION_WITHDRAWAL, amount_in) - - # We use the transaction ID as a memo on the Stellar transaction for the - # payment in the withdrawal. This lets us identify that as uniquely - # corresponding to this `Transaction` in the database. But a UUID4 is a 32 - # character hex string, while the Stellar HashMemo requires a 64 character - # hex-encoded (32 byte) string. So, we zero-pad the ID to create an - # appropriately sized string for the `HashMemo`. - transaction_id_hex = uuid.UUID(transaction_id).hex - withdraw_memo = "0" * (64 - len(transaction_id_hex)) + transaction_id_hex - transaction = Transaction( - id=transaction_id, - stellar_account=request.POST.get("account"), - asset=asset, - kind=Transaction.KIND.withdrawal, - status=Transaction.STATUS.pending_user_transfer_start, - amount_in=amount_in, - amount_fee=amount_fee, - withdraw_anchor_account=settings.STELLAR_DISTRIBUTION_ACCOUNT_ADDRESS, - withdraw_memo=withdraw_memo, - withdraw_memo_type=Transaction.MEMO_TYPES.hash, + transaction.amount_in = form.cleaned_data["amount"] + transaction.amount_fee = calc_fee( + asset, settings.OPERATION_WITHDRAWAL, transaction.amount_in ) transaction.save() - serializer = TransactionSerializer( - transaction, - context={"more_info_url": _construct_more_info_url(request)}, - ) - tx_json = json.dumps({"transaction": serializer.data}) - return Response( - { - "tx_json": tx_json, - "transaction": transaction, - "asset_code": asset_code, - }, - template_name="transaction/more_info.html" - ) + return redirect(f"{reverse('more_info')}?{urlencode({'id': transaction_id})}") else: resp_data = {"form": form, "account": request.POST.get("account")} return Response(resp_data, template_name="withdraw/form.html") @@ -151,7 +126,25 @@ def withdraw(account: str, request: Request) -> Response: if not asset or not asset.withdrawal_enabled: return render_error_response(f"invalid operation for asset {asset_code}") + # We use the transaction ID as a memo on the Stellar transaction for the + # payment in the withdrawal. This lets us identify that as uniquely + # corresponding to this `Transaction` in the database. But a UUID4 is a 32 + # character hex string, while the Stellar HashMemo requires a 64 character + # hex-encoded (32 byte) string. So, we zero-pad the ID to create an + # appropriately sized string for the `HashMemo`. transaction_id = create_transaction_id() + transaction_id_hex = transaction_id.hex + withdraw_memo = "0" * (64 - len(transaction_id_hex)) + transaction_id_hex + Transaction.objects.create( + id=transaction_id, + stellar_account=account, + asset=asset, + kind=Transaction.KIND.withdrawal, + status=Transaction.STATUS.pending_user_transfer_start, + withdraw_anchor_account=settings.STELLAR_DISTRIBUTION_ACCOUNT_ADDRESS, + withdraw_memo=withdraw_memo, + withdraw_memo_type=Transaction.MEMO_TYPES.hash, + ) url = _construct_interactive_url(request, asset_code, transaction_id, account) return Response( {"type": "interactive_customer_info_needed", "url": url, "id": transaction_id},