def fee(request):
    """
    Definition of the /fee endpoint, in accordance with SEP-0006.
    See: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md#fee
    """

    # Verify that the asset code exists in our database:
    asset_code = request.GET.get("asset_code")
    if not asset_code or not Asset.objects.filter(name=asset_code).exists():
        return render_error_response("invalid 'asset_code'")

    # Verify that the requested operation is valid:
    operation = request.GET.get("operation")
    if operation not in (OPERATION_DEPOSIT, OPERATION_WITHDRAWAL):
        return render_error_response(
            f"'operation' should be either '{OPERATION_DEPOSIT}' or '{OPERATION_WITHDRAWAL}'"
        )

    # Verify that amount is provided, and that it is parseable into a float:
    amount_str = request.GET.get("amount")
    try:
        amount = float(amount_str)
    except (TypeError, ValueError):
        return render_error_response("invalid 'amount'")

    # Validate that the operation, and the specified type (if provided)
    # are applicable to the given asset:
    op_type = request.GET.get("type", "")
    if not _op_type_is_valid(asset_code, operation, op_type):
        return render_error_response(
            f"the specified operation is not available for '{asset_code}'")

    return Response({"fee": _calc_fee(asset_code, operation, amount)})
def confirm_transaction(request):
    # Validate the provided transaction_id and amount.
    transaction_id = request.GET.get("transaction_id")
    if not transaction_id:
        return render_error_response("no 'transaction_id' provided")

    transaction = Transaction.objects.filter(id=transaction_id).first()
    if not transaction:
        return render_error_response(
            "no transaction with matching 'transaction_id' exists")

    amount_str = request.GET.get("amount")
    if not amount_str:
        return render_error_response("no 'amount' provided")
    try:
        amount = float(amount_str)
    except ValueError:
        return render_error_response("non-float 'amount' provided")

    if transaction.amount_in != amount:
        return render_error_response(
            "incorrect 'amount' value for transaction with given 'transaction_id'"
        )

    # The external deposit has been completed, so the transaction
    # status must now be updated to pending_anchor.
    transaction.status = Transaction.STATUS.pending_anchor
    transaction.save()
    serializer = TransactionSerializer(transaction)
    return Response({"transaction": serializer.data})
def deposit(request):
    asset_code = request.GET.get("asset_code")
    stellar_account = request.GET.get("account")

    # Verify that the request is valid.
    if not all([asset_code, stellar_account]):
        return render_error_response("asset_code and account are required parameters")

    # Verify that the asset code exists in our database, with deposit enabled.
    asset = Asset.objects.filter(name=asset_code).first()
    if not asset or not asset.deposit_enabled:
        return render_error_response(f"invalid operation for asset {asset_code}")

    try:
        address = Address(address=stellar_account)
    except (StellarAddressInvalidError, NotValidParamError):
        return render_error_response("invalid 'account'")

    # Verify the optional request arguments.
    verify_optional_args = _verify_optional_args(request)
    if verify_optional_args:
        return verify_optional_args

    # TODO: Check if the provided Stellar account exists, and if not, create it.

    # Construct interactive deposit pop-up URL.
    transaction_id = _create_transaction_id()
    url = _construct_interactive_url(request, transaction_id)
    return Response(
        {"type": "interactive_customer_info_needed", "url": url, "id": transaction_id},
        status=status.HTTP_403_FORBIDDEN,
    )
def withdraw(request):
    """
    `GET /withdraw` initiates the withdrawal and returns an interactive
    withdrawal form to the user.
    """
    asset_code = request.GET.get("asset_code")
    if not asset_code:
        return render_error_response("'asset_code' is required")

    # TODO: Verify optional arguments.

    # Verify that the asset code exists in our database, with withdraw enabled.
    asset = Asset.objects.filter(name=asset_code).first()
    if not asset or not asset.withdrawal_enabled:
        return render_error_response(
            f"invalid operation for asset {asset_code}")

    transaction_id = create_transaction_id()
    url = _construct_interactive_url(request, transaction_id)
    return Response(
        {
            "type": "interactive_customer_info_needed",
            "url": url,
            "id": transaction_id
        },
        status=status.HTTP_403_FORBIDDEN,
    )
def interactive_deposit(request):
    """
    `GET /deposit/interactive_deposit` opens a form used to input information
    about the deposit. This creates a corresponding transaction in our
    database, pending processing by the external agent.
    """
    # Validate query parameters: account, asset_code, transaction_id.
    account = request.GET.get("account")
    if not account:
        return render_error_response("no 'account' provided")

    asset_code = request.GET.get("asset_code")
    if not asset_code or not Asset.objects.filter(name=asset_code).exists():
        return render_error_response("invalid 'asset_code'")

    transaction_id = request.GET.get("transaction_id")
    if not transaction_id:
        return render_error_response("no 'transaction_id' provided")

    # GET: The server needs to display the form for the user to input the deposit information.
    if request.method == "GET":
        form = DepositForm()
    # POST: The user submitted a form with the amount to deposit.
    else:
        if Transaction.objects.filter(id=transaction_id).exists():
            return render_error_response(
                "transaction with matching 'transaction_id' already exists"
            )
        form = DepositForm(request.POST)
        asset = Asset.objects.get(name=asset_code)
        form.asset = asset
        # If the form is valid, we create a transaction pending external action
        # and render the success page.
        if form.is_valid():
            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_external,
                amount_in=amount_in,
                amount_fee=amount_fee,
                to_address=account,
            )
            transaction.save()

            serializer = TransactionSerializer(
                transaction,
                context={"more_info_url": _construct_more_info_url(request)},
            )
            tx_json = json.dumps({"transaction": serializer.data})
            return render(request, "deposit/success.html", context={"tx_json": tx_json})
    return render(request, "deposit/form.html", {"form": form})
def confirm_transaction(request):
    """
    `GET /deposit/confirm_transaction` is used by an external agent to confirm
    that they have processed the transaction. This triggers submission of the
    corresponding Stellar transaction.

    Note that this endpoint is not part of the SEP 6 workflow, it is merely
    a mechanism for confirming the external transaction for demonstration purposes.
    If reusing this technique in a real-life scenario, add a strictly secure
    authentication system.
    """
    # Validate the provided transaction_id and amount.
    transaction_id = request.GET.get("transaction_id")
    if not transaction_id:
        return render_error_response("no 'transaction_id' provided")

    transaction = Transaction.objects.filter(id=transaction_id).first()
    if not transaction:
        return render_error_response(
            "no transaction with matching 'transaction_id' exists"
        )

    amount_str = request.GET.get("amount")
    if not amount_str:
        return render_error_response("no 'amount' provided")
    try:
        amount = float(amount_str)
    except ValueError:
        return render_error_response("non-float 'amount' provided")

    if transaction.amount_in != amount:
        return render_error_response(
            "incorrect 'amount' value for transaction with given 'transaction_id'"
        )

    external_transaction_id = request.GET.get("external_transaction_id")

    # The external deposit has been completed, so the transaction
    # status must now be updated to pending_anchor.
    transaction.status = Transaction.STATUS.pending_anchor
    transaction.status_eta = 5  # Ledger close time.
    transaction.external_transaction_id = external_transaction_id
    transaction.save()
    serializer = TransactionSerializer(
        transaction, context={"more_info_url": _construct_more_info_url(request)}
    )

    # Asynchronously launch the deposit Stellar transaction.
    create_stellar_deposit.delay(transaction.id)
    return Response({"transaction": serializer.data})
def interactive_deposit(request):
    # Validate query parameters: account, asset_code, transaction_id.
    account = request.GET.get("account")
    if not account:
        return render_error_response("no 'account' provided")

    asset_code = request.GET.get("asset_code")
    if not asset_code or not Asset.objects.filter(name=asset_code).exists():
        return render_error_response("invalid 'asset_code'")

    transaction_id = request.GET.get("transaction_id")
    if not transaction_id:
        return render_error_response("no 'transaction_id' provided")

    # GET: The server needs to display the form for the user to input the deposit information.
    if request.method == "GET":
        form = DepositForm()
    # POST: The user submitted a form with the amount to deposit.
    else:
        if Transaction.objects.filter(id=transaction_id).exists():
            return render_error_response(
                "transaction with matching 'transaction_id' already exists"
            )
        form = DepositForm(request.POST)
        asset = Asset.objects.get(name=asset_code)
        form.asset = asset
        # If the form is valid, we create a transaction pending external action
        # and render the success page.
        if form.is_valid():
            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_external,
                amount_in=amount_in,
                amount_fee=amount_fee,
            )
            transaction.save()
            create_stellar_deposit.delay(transaction.id)
            # TODO: Use the proposed callback approach.
            return render(request, "deposit/success.html")
    return render(request, "deposit/form.html", {"form": form})
def _verify_optional_args(request):
    memo_type = request.GET.get("memo_type")
    if memo_type and memo_type not in ("text", "id", "hash"):
        return render_error_response("invalid 'memo_type'")

    memo = request.GET.get("memo")
    if memo_type and not memo:
        return render_error_response("'memo_type' provided with no 'memo'")

    if memo and not memo_type:
        return render_error_response("'memo' provided with no 'memo_type'")

    if memo_type == "hash":
        try:
            base64.b64encode(base64.b64decode(memo))
        except binascii.Error:
            return render_error_response("'memo' does not match memo_type' hash")
    return None
def deposit(request):
    """
    `GET /deposit` initiates the deposit and returns an interactive
    deposit form to the user.
    """
    asset_code = request.GET.get("asset_code")
    stellar_account = request.GET.get("account")

    # Verify that the request is valid.
    if not all([asset_code, stellar_account]):
        return render_error_response(
            "`asset_code` and `account` are required parameters")

    # Verify that the asset code exists in our database, with deposit enabled.
    asset = Asset.objects.filter(name=asset_code).first()
    if not asset or not asset.deposit_enabled:
        return render_error_response(
            f"invalid operation for asset {asset_code}")

    try:
        Address(
            address=stellar_account,
            network=settings.STELLAR_NETWORK,
            horizon_uri=settings.HORIZON_URI,
        )
    except (StellarAddressInvalidError, NotValidParamError):
        return render_error_response("invalid 'account'")

    # Verify the optional request arguments.
    verify_optional_args = _verify_optional_args(request)
    if verify_optional_args:
        return verify_optional_args

    # Construct interactive deposit pop-up URL.
    transaction_id = create_transaction_id()
    url = _construct_interactive_url(request, transaction_id)
    return Response(
        {
            "type": "interactive_customer_info_needed",
            "url": url,
            "id": transaction_id
        },
        status=status.HTTP_403_FORBIDDEN,
    )
def deposit(request):
    """
    `GET /deposit` initiates the deposit and returns an interactive
    deposit form to the user.
    """
    asset_code = request.GET.get("asset_code")
    stellar_account = request.GET.get("account")

    # Verify that the request is valid.
    if not all([asset_code, stellar_account]):
        return render_error_response(
            "`asset_code` and `account` are required parameters")

    # Verify that the asset code exists in our database, with deposit enabled.
    asset = Asset.objects.filter(code=asset_code).first()
    if not asset or not asset.deposit_enabled:
        return render_error_response(
            f"invalid operation for asset {asset_code}")

    try:
        Keypair.from_public_key(stellar_account)
    except Ed25519PublicKeyInvalidError:
        return render_error_response("invalid 'account'")

    # Verify the optional request arguments.
    verify_optional_args = _verify_optional_args(request)
    if verify_optional_args:
        return verify_optional_args

    # Construct interactive deposit pop-up URL.
    transaction_id = create_transaction_id()
    url = _construct_interactive_url(request, transaction_id)
    return Response(
        {
            "type": "interactive_customer_info_needed",
            "url": url,
            "id": transaction_id
        },
        status=status.HTTP_403_FORBIDDEN,
    )
def interactive_withdraw(request):
    """
    `GET /withdraw/interactive_withdraw` opens a form used to input information about
    the withdrawal. This creates a corresponding transaction in our database.
    """
    transaction_id = request.GET.get("transaction_id")
    if not transaction_id:
        return render_error_response("no 'transaction_id' provided")

    asset_code = request.GET.get("asset_code")
    if not asset_code or not Asset.objects.filter(name=asset_code).exists():
        return render_error_response("invalid 'asset_code'")

    # GET: The server needs to display the form for the user to input withdrawal information.
    if request.method == "GET":
        form = WithdrawForm()

    # POST: The user submitted a form with the withdrawal info.
    else:
        if Transaction.objects.filter(id=transaction_id).exists():
            return render_error_response(
                "transaction with matching 'transaction_id' already exists")
        form = WithdrawForm(request.POST)
        asset = Asset.objects.get(name=asset_code)
        form.asset = asset

        # If the form is valid, we create a transaction pending user action
        # and render the success page.
        if form.is_valid():
            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=settings.STELLAR_ACCOUNT_ADDRESS,
                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_ACCOUNT_ADDRESS,
                withdraw_memo=withdraw_memo,
                withdraw_memo_type=Transaction.MEMO_TYPES.hash,
            )
            transaction.save()

            serializer = TransactionSerializer(transaction)
            tx_json = json.dumps({"transaction": serializer.data})
            return render(request,
                          "withdraw/success.html",
                          context={"tx_json": tx_json})
    return render(request, "withdraw/form.html", {"form": form})