def more_info(request: Request) -> Response: """ Popup to display more information about a specific transaction. See table: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#4-customer-information-status """ try: request_transaction = _get_transaction_from_request(request) except AttributeError as exc: return render_error_response(str(exc), content_type="text/html") except Transaction.DoesNotExist: return render_error_response( "transaction not found", status_code=status.HTTP_404_NOT_FOUND, content_type="text/html", ) serializer = TransactionSerializer(request_transaction, context={"request": request}) tx_json = json.dumps({"transaction": serializer.data}) resp_data = { "tx_json": tx_json, "transaction": request_transaction, "asset_code": request_transaction.asset.code, "instructions": None, } if (request_transaction.kind == Transaction.KIND.deposit and request_transaction.status == Transaction.STATUS.pending_user_transfer_start): resp_data["instructions"] = rdi.instructions_for_pending_deposit( request_transaction) return Response(resp_data, template_name="transaction/more_info.html")
def fee(account: str, request: Request) -> Response: """ Definition of the /fee endpoint, in accordance with SEP-0024. See: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.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(code=asset_code).exists(): return render_error_response("invalid 'asset_code'") asset = Asset.objects.get(code=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 = Decimal(amount_str) except (DecimalException, TypeError): 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, operation, amount)})
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(code=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 more_info(request): """ Popup to display more information about a specific transaction. See table: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#4-customer-information-status """ try: request_transaction = _get_transaction_from_request(request) except AttributeError as exc: return render_error_response(str(exc), content_type="text/html") except Transaction.DoesNotExist: return render_error_response("transaction not found", status_code=status.HTTP_404_NOT_FOUND, content_type="text/html") serializer = TransactionSerializer( request_transaction, context={"more_info_url": _construct_more_info_url(request)}, ) tx_json = json.dumps({"transaction": serializer.data}) return Response( { "tx_json": tx_json, "transaction": request_transaction, "asset_code": request_transaction.asset.code, }, template_name="transaction/more_info.html")
def deposit(account: str, request: Request) -> Response: """ POST /transactions/deposit/interactive Creates an `incomplete` deposit Transaction object in the database and returns the URL entry-point for the interactive flow. """ asset_code = request.POST.get("asset_code") stellar_account = request.POST.get("account") lang = request.POST.get("lang") if lang: err_resp = validate_language(lang) if err_resp: return err_resp activate_lang_for_request(lang) # 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: return render_error_response(_("unknown asset: %s") % asset_code) elif not asset.deposit_enabled: return render_error_response(_("invalid operation for asset %s") % asset_code) try: Keypair.from_public_key(stellar_account) except Ed25519PublicKeyInvalidError: return render_error_response(_("invalid 'account'")) # 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.incomplete, to_address=account, ) logger.info(f"Created deposit transaction {transaction_id}") url = interactive_url( request, str(transaction_id), stellar_account, asset_code, settings.OPERATION_DEPOSIT, ) return Response( {"type": "interactive_customer_info_needed", "url": url, "id": transaction_id}, status=status.HTTP_200_OK, )
def interactive_withdraw(request: Request) -> Response: """ """ transaction_id = request.GET.get("transaction_id") asset_code = request.GET.get("asset_code") asset = Asset.objects.filter(code=asset_code).first() if not transaction_id: return render_error_response("no 'transaction_id' provided", content_type="text/html") elif not (asset_code and asset): return render_error_response("invalid 'asset_code'", content_type="text/html") try: transaction = Transaction.objects.get(id=transaction_id, asset=asset) except (Transaction.DoesNotExist, ValidationError): return render_error_response( "Transaction with ID and asset_code not found", content_type="text/html", status_code=status.HTTP_404_NOT_FOUND, ) if request.method == "GET": form_class = rwi.form_for_transaction(transaction) return Response({"form": form_class()}, template_name="withdraw/form.html") # request.method == "POST" form = rwi.form_for_transaction(transaction)(request.POST) is_transaction_form = issubclass(form.__class__, TransactionForm) if is_transaction_form: form.asset = asset if form.is_valid(): if is_transaction_form: transaction.amount_in = form.cleaned_data["amount"] transaction.amount_fee = calc_fee(asset, settings.OPERATION_WITHDRAWAL, transaction.amount_in) transaction.save() # Perform any defined post-validation logic defined by Polaris users rwi.after_form_validation(form, transaction) # Check to see if there is another form to render form_class = rwi.form_for_transaction(transaction) if form_class: return Response({"form": form_class()}, template_name="withdraw/form.html") else: # Last form has been submitted invalidate_session(request) transaction.status = Transaction.STATUS.pending_user_transfer_start transaction.save() url, args = reverse("more_info"), urlencode({"id": transaction_id}) return redirect(f"{url}?{args}") else: return Response({"form": form}, template_name="withdraw/form.html")
def withdraw(account: str, request: Request) -> Response: """ POST /transactions/withdraw/interactive Creates an `incomplete` withdraw Transaction object in the database and returns the URL entry-point for the interactive flow. """ lang = request.POST.get("lang") asset_code = request.POST.get("asset_code") if lang: err_resp = validate_language(lang) if err_resp: return err_resp activate_lang_for_request(lang) if not asset_code: return render_error_response(_("'asset_code' is required")) # Verify that the asset code exists in our database, with withdraw enabled. asset = Asset.objects.filter(code=asset_code).first() if not asset or not asset.withdrawal_enabled: return render_error_response( _("invalid operation for asset %s") % asset_code) elif asset.code not in settings.ASSETS: return render_error_response( _("unsupported asset type: %s") % asset_code) distribution_address = settings.ASSETS[ asset.code]["DISTRIBUTION_ACCOUNT_ADDRESS"] # 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.incomplete, withdraw_anchor_account=distribution_address, withdraw_memo=withdraw_memo, withdraw_memo_type=Transaction.MEMO_TYPES.hash, ) logger.info(f"Created withdrawal transaction {transaction_id}") url = interactive_url(request, str(transaction_id), account, asset_code, settings.OPERATION_WITHDRAWAL) return Response({ "type": "interactive_customer_info_needed", "url": url, "id": transaction_id })
def deposit(account: str, request: Request) -> Response: """ `POST /transactions/deposit/interactive` initiates the deposit and returns an interactive deposit form to the user. """ asset_code = request.POST.get("asset_code") stellar_account = request.POST.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() Transaction.objects.create( id=transaction_id, stellar_account=account, asset=asset, kind=Transaction.KIND.deposit, status=Transaction.STATUS.incomplete, to_address=account, ) url = rdi.interactive_url(request, str(transaction_id), stellar_account, asset_code) return Response( { "type": "interactive_customer_info_needed", "url": url, "id": transaction_id }, status=status.HTTP_200_OK, )
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 24 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)}) # launch the deposit Stellar transaction. call_command("create_stellar_deposit", transaction.id) return Response({"transaction": serializer.data})
def transactions(account: str, request: Request) -> Response: """ Definition of the /transactions endpoint, in accordance with SEP-0024. See: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#transaction-history """ try: limit = _validate_limit(request.GET.get("limit")) except ValueError: return render_error_response("invalid limit", status_code=status.HTTP_400_BAD_REQUEST) if not request.GET.get("asset_code"): return render_error_response("asset_code is required") elif not Asset.objects.filter(code=request.GET.get("asset_code")).exists(): return render_error_response("invalid asset_code") translation_dict = { "asset_code": "asset__code", "no_older_than": "started_at__gte", "kind": "kind", } qset_filter = _compute_qset_filters(request.GET, translation_dict) qset_filter["stellar_account"] = account # Since the Transaction IDs are UUIDs, rather than in the chronological # order of their creation, we map the paging ID (if provided) to the # started_at field of a Transaction. paging_id = request.GET.get("paging_id") if paging_id: try: start_transaction = Transaction.objects.get(id=paging_id) except Transaction.DoesNotExist: return render_error_response( "invalid paging_id", status_code=status.HTTP_400_BAD_REQUEST) qset_filter["started_at__lt"] = start_transaction.started_at transactions_qset = Transaction.objects.filter(**qset_filter)[:limit] serializer = TransactionSerializer( transactions_qset, many=True, context={ "request": request, "same_asset": True }, ) return Response({"transactions": serializer.data})
def complete_interactive_deposit(request: Request) -> Response: """ GET /transactions/deposit/interactive/complete Updates the transaction status to pending_user_transfer_start and redirects to GET /more_info. A `callback` can be passed in the URL to be used by the more_info template javascript. """ transaction_id = request.GET.get("transaction_id") callback = request.GET.get("callback") if not transaction_id: return render_error_response(_("Missing id parameter in URL"), content_type="text/html") Transaction.objects.filter(id=transaction_id).update( status=Transaction.STATUS.pending_user_transfer_start) logger.info( f"Hands-off interactive flow complete for transaction {transaction_id}" ) url, args = ( reverse("more_info"), urlencode({ "id": transaction_id, "callback": callback }), ) return redirect(f"{url}?{args}")
def fee(account: str, request: Request) -> Response: """ Definition of the /fee endpoint, in accordance with SEP-0024. See: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#fee """ operation = request.GET.get("operation") op_type = request.GET.get("type") asset_code = request.GET.get("asset_code") amount_str = request.GET.get("amount") # Verify that the asset code exists in our database: if not asset_code or not Asset.objects.filter(code=asset_code).exists(): return render_error_response("invalid 'asset_code'") asset = Asset.objects.get(code=asset_code) # Verify that amount is provided, and that can be parsed into a decimal: try: amount = Decimal(amount_str) except (DecimalException, TypeError): return render_error_response("invalid 'amount'") error_resp = None # Verify that the requested operation is valid: if operation not in (OPERATION_DEPOSIT, OPERATION_WITHDRAWAL): error_resp = render_error_response( f"'operation' should be either '{OPERATION_DEPOSIT}' or '{OPERATION_WITHDRAWAL}'" ) # Verify asset is enabled and within the specified limits elif operation == OPERATION_DEPOSIT: error_resp = verify_valid_asset_operation(asset, amount, Transaction.KIND.deposit) elif operation == OPERATION_WITHDRAWAL: error_resp = verify_valid_asset_operation(asset, amount, Transaction.KIND.withdrawal) if error_resp: return error_resp else: return Response({ "fee": registered_fee_func({ "operation": operation, "type": op_type, "asset_code": asset_code, "amount": amount, }) })
def transaction(account: str, request: Request) -> Response: """ Definition of the /transaction endpoint, in accordance with SEP-0024. See: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#single-historical-transaction """ try: request_transaction = _get_transaction_from_request(request, account=account) except (AttributeError, ValidationError) as exc: return render_error_response(str(exc), status_code=status.HTTP_400_BAD_REQUEST) except Transaction.DoesNotExist: return render_error_response( "transaction not found", status_code=status.HTTP_404_NOT_FOUND ) serializer = TransactionSerializer( request_transaction, context={"request": request}, ) return Response({"transaction": serializer.data})
def confirm_email(request: Request) -> Response: if not (request.GET.get("token") and request.GET.get("email")): return render_error_response("email and token arguments required.", content_type="text/html") try: user = PolarisUser.objects.get( email=request.GET.get("email"), confirmation_token=request.GET.get("token")) except PolarisUser.DoesNotExist: return render_error_response( "User with email and token does not exist", content_type="text/html") user.confirmed = True user.save() return Response(template_name="email_confirmed.html")
def withdraw(account: str, request: Request) -> Response: """ `POST /transactions/withdraw` initiates the withdrawal and returns an interactive withdrawal form to the user. """ asset_code = request.POST.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(code=asset_code).first() 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.incomplete, withdraw_anchor_account=settings.STELLAR_DISTRIBUTION_ACCOUNT_ADDRESS, withdraw_memo=withdraw_memo, withdraw_memo_type=Transaction.MEMO_TYPES.hash, ) url = _construct_interactive_url(request, str(transaction_id), account, asset_code) return Response({ "type": "interactive_customer_info_needed", "url": url, "id": transaction_id })
def _verify_optional_args(request): """Verify the optional arguments to `GET /deposit`.""" memo_type = request.POST.get("memo_type") if memo_type and memo_type not in ("text", "id", "hash"): return render_error_response("invalid 'memo_type'") memo = request.POST.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(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 check_middleware(content_type: str = "text/html") -> Optional[Response]: """ Ensures the Django app running Polaris has the correct middleware configuration for GET /webapp requests. """ err_msg = None session_middleware_path = "django.contrib.sessions.middleware.SessionMiddleware" if import_path not in django_settings.MIDDLEWARE: err_msg = f"{import_path} is not installed" elif session_middleware_path not in django_settings.MIDDLEWARE: err_msg = f"{session_middleware_path} is not installed" elif django_settings.MIDDLEWARE.index( import_path) > django_settings.MIDDLEWARE.index( session_middleware_path): err_msg = f"{import_path} must be listed before {session_middleware_path}" if err_msg: return render_error_response(err_msg, content_type=content_type, status_code=501) else: return None
def post_interactive_deposit(request: Request) -> Response: """ POST /transactions/deposit/webapp This endpoint processes form submissions during the deposit interactive flow. The following steps are taken during this process: 1. URL arguments are parsed and validated. 2. content_for_transaction() is called to retrieve the form used to submit this request. This function is implemented by the anchor. 3. The form is used to validate the data submitted, and if the form is a TransactionForm, the fee for the transaction is calculated. 4. after_form_validation() is called to allow the anchor to process the data submitted. This function should change the application state such that the next call to content_for_transaction() returns the next form in the flow. 5. content_for_transaction() is called again to retrieve the next form to be served to the user. If a form is returned, the function redirects to GET /transaction/deposit/webapp. Otherwise, The user's session is invalidated, the transaction status is updated, and the function redirects to GET /more_info. """ args_or_error = interactive_args_validation(request) if "error" in args_or_error: return args_or_error["error"] transaction = args_or_error["transaction"] asset = args_or_error["asset"] callback = args_or_error["callback"] amount = args_or_error["amount"] content = rdi.content_for_transaction(transaction) if not (content and content.get("form")): logger.error("Initial content_for_transaction() call returned None in " f"POST request for transaction: {transaction.id}") if transaction.status != transaction.STATUS.incomplete: return render_error_response( _("The anchor did not provide content, is the interactive flow already complete?" ), status_code=422, content_type="text/html", ) return render_error_response( _("The anchor did not provide form content, unable to serve page." ), status_code=500, content_type="text/html", ) try: form_class, form_args = content.get("form") except TypeError: logger.exception( "content_for_transaction(): 'form' key value must be a tuple") return render_error_response( _("The anchor did not provide content, unable to serve page."), status_code=500, content_type="text/html", ) is_transaction_form = issubclass(form_class, TransactionForm) if is_transaction_form: form = form_class(asset, request.POST, **form_args) else: form = form_class(request.POST, **form_args) if form.is_valid(): if is_transaction_form: fee_params = { "operation": settings.OPERATION_DEPOSIT, "asset_code": asset.code, **form.cleaned_data, } transaction.amount_in = form.cleaned_data["amount"] transaction.amount_fee = registered_fee_func(fee_params) transaction.save() rdi.after_form_validation(form, transaction) content = rdi.content_for_transaction(transaction) if content: args = {"transaction_id": transaction.id, "asset_code": asset.code} if amount: args["amount"] = amount if callback: args["callback"] = callback url = reverse("get_interactive_deposit") return redirect(f"{url}?{urlencode(args)}") else: # Last form has been submitted logger.info( f"Finished data collection and processing for transaction {transaction.id}" ) invalidate_session(request) transaction.status = Transaction.STATUS.pending_user_transfer_start transaction.save() url = reverse("more_info") args = urlencode({"id": transaction.id, "callback": callback}) return redirect(f"{url}?{args}") else: content.update(form=form) return Response(content, template_name="deposit/form.html", status=422)
def get_interactive_deposit(request: Request) -> Response: """ GET /transactions/deposit/webapp This endpoint retrieves the next form to be served to the user in the interactive flow. The following steps are taken during this process: 1. URL arguments are parsed and validated. 2. interactive_url() is called to determine whether or not the anchor uses an external service for the interactive flow. If a URL is returned, this function redirects to the URL. However, the session cookie should still be included in the response so future calls to GET /transactions/deposit/interactive/complete are authenticated. 3. content_for_transaction() is called to retrieve the next form to render to the user. `amount` is prepopulated in the form if it was passed as a parameter to this endpoint and the form is a subclass of TransactionForm. 4. get and post URLs are constructed with the appropriate arguments and passed to the response to be rendered to the user. """ args_or_error = interactive_args_validation(request) if "error" in args_or_error: return args_or_error["error"] transaction = args_or_error["transaction"] asset = args_or_error["asset"] callback = args_or_error["callback"] amount = args_or_error["amount"] url = rdi.interactive_url(request, transaction, asset, amount, callback) if url: # The anchor uses a standalone interactive flow return redirect(url) content = rdi.content_for_transaction(transaction) if not content: logger.error( "The anchor did not provide content, unable to serve page.") if transaction.status != transaction.STATUS.incomplete: return render_error_response( _("The anchor did not provide content, is the interactive flow already complete?" ), status_code=422, content_type="text/html", ) return render_error_response( _("The anchor did not provide content, unable to serve page."), status_code=500, content_type="text/html", ) scripts = registered_scripts_func(content) if content.get("form"): try: form_class, form_args = content.get("form") except TypeError: logger.exception( "content_for_transaction(): 'form' key value must be a tuple") return render_error_response( _("The anchor did not provide content, unable to serve page."), content_type="text/html", ) is_transaction_form = issubclass(form_class, TransactionForm) if is_transaction_form: content["form"] = form_class(asset, initial={"amount": amount}, test_value="103", **form_args) else: content["form"] = form_class(**form_args) url_args = {"transaction_id": transaction.id, "asset_code": asset.code} if callback: url_args["callback"] = callback if amount: url_args["amount"] = amount post_url = f"{reverse('post_interactive_deposit')}?{urlencode(url_args)}" get_url = f"{reverse('get_interactive_deposit')}?{urlencode(url_args)}" content.update(post_url=post_url, get_url=get_url, scripts=scripts) return Response(content, template_name="deposit/form.html")
def complete_interactive_withdraw(request: Request) -> Response: transaction_id = request.GET("id") if not transaction_id: render_error_response("Missing id parameter in URL") url, args = reverse("more_info"), urlencode({"id": transaction_id}) return redirect(f"{url}?{args}")
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", content_type="text/html") asset_code = request.GET.get("asset_code") if not asset_code or not Asset.objects.filter(code=asset_code).exists(): return render_error_response("invalid 'asset_code'", content_type="text/html") # 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", content_type="text/html") form = WithdrawForm(request.POST) asset = Asset.objects.get(code=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_DISTRIBUTION_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_DISTRIBUTION_ACCOUNT_ADDRESS, withdraw_memo=withdraw_memo, withdraw_memo_type=Transaction.MEMO_TYPES.hash, ) 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 Response({"form": form}, template_name="withdraw/form.html")
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", content_type="text/html") asset_code = request.GET.get("asset_code") if not asset_code or not Asset.objects.filter(code=asset_code).exists(): return render_error_response("invalid 'asset_code'", content_type="text/html") transaction_id = request.GET.get("transaction_id") if not transaction_id: return render_error_response("no 'transaction_id' provided", content_type="text/html") # 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", content_type="text/html") form = DepositForm(request.POST) asset = Asset.objects.get(code=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_user_transfer_start, 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 Response( { "tx_json": tx_json, "transaction": transaction, "asset_code": transaction.asset.code, }, template_name="transaction/more_info.html", ) return Response({"form": form}, template_name="deposit/form.html")