예제 #1
0
def callback(account: str, request: Request) -> Response:
    if request.data.get("id"):
        if not isinstance(request.data.get("id"), str):
            return render_error_response(_("bad ID value, expected str"))
        elif (
            request.data.get("account")
            or request.data.get("memo")
            or request.data.get("memo_type")
        ):
            return render_error_response(
                _(
                    "requests with 'id' cannot also have 'account', 'memo', or 'memo_type'"
                )
            )
    elif account != request.data.get("account"):
        return render_error_response(
            _("The account specified does not match authorization token"),
            status_code=403,
        )

    try:
        # validate memo and memo_type
        make_memo(request.data.get("memo"), request.data.get("memo_type"))
    except ValueError:
        return render_error_response(_("invalid 'memo' for 'memo_type'"))

    callback_url = request.data.get("url")
    if not callback_url:
        return render_error_response(_("callback 'url' required"))
    schemes = ["https"] if not settings.LOCAL_MODE else ["https", "http"]
    try:
        URLValidator(schemes=schemes)(request.data.get("url"))
    except ValidationError:
        return render_error_response(_("'url' must be a valid URL"))

    try:
        rci.callback(
            {
                "id": request.data.get("id"),
                "account": account,
                "memo": request.data.get("memo"),
                "memo_type": request.data.get("memo_type"),
                "url": callback_url,
            }
        )
    except ValueError as e:
        return render_error_response(str(e), status_code=400)
    except ObjectDoesNotExist as e:
        return render_error_response(str(e), status_code=404)
    except NotImplementedError:
        return render_error_response(_("not implemented"), status_code=501)

    return Response({"success": True})
예제 #2
0
def delete(account_from_auth: str, request: Request, account: str,) -> Response:
    if account_from_auth != account:
        return render_error_response(_("account not found"), status_code=404)
    try:
        make_memo(request.data.get("memo"), request.data.get("memo_type"))
    except ValueError:
        return render_error_response(_("invalid 'memo' for 'memo_type'"))
    try:
        rci.delete(account, request.data.get("memo"), request.data.get("memo_type"))
    except ObjectDoesNotExist:
        return render_error_response(_("account not found"), status_code=404)
    else:
        return Response({"status": "success"}, status=200)
예제 #3
0
    def get(account: str, request: Request) -> Response:
        if request.GET.get("account") and account != request.GET.get("account"):
            return render_error_response(
                _("The account specified does not match authorization token"),
                status_code=403,
            )
        elif request.GET.get("id") and (
            request.GET.get("account")
            or request.GET.get("memo")
            or request.GET.get("memo_type")
        ):
            return render_error_response(
                _(
                    "requests with 'id' cannot also have 'account', 'memo', or 'memo_type'"
                )
            )

        try:
            # validate memo and memo_type
            make_memo(request.GET.get("memo"), request.GET.get("memo_type"))
        except ValueError:
            return render_error_response(_("invalid 'memo' for 'memo_type'"))

        try:
            response_data = rci.get(
                {
                    "id": request.GET.get("id"),
                    "sep10_client_account": account,
                    "account": request.GET.get("account"),
                    "memo": request.GET.get("memo"),
                    "memo_type": request.GET.get("memo_type"),
                    "type": request.GET.get("type"),
                    "lang": request.GET.get("lang"),
                }
            )
        except ValueError as e:
            return render_error_response(str(e), status_code=400)
        except ObjectDoesNotExist as e:
            return render_error_response(str(e), status_code=404)

        try:
            validate_response_data(response_data)
        except ValueError:
            logger.exception(
                _("An exception was raised validating GET /customer response")
            )
            return render_error_response(
                _("unable to process request."), status_code=500
            )

        return Response(response_data)
예제 #4
0
    def put(account: str, request: Request) -> Response:
        if request.data.get("id"):
            if not isinstance(request.data.get("id"), str):
                return render_error_response(_("bad ID value, expected str"))
            elif (
                request.data.get("account")
                or request.data.get("memo")
                or request.data.get("memo_type")
            ):
                return render_error_response(
                    _(
                        "requests with 'id' cannot also have 'account', 'memo', or 'memo_type'"
                    )
                )
        elif account != request.data.get("account"):
            return render_error_response(
                _("The account specified does not match authorization token"),
                status_code=403,
            )

        try:
            # validate memo and memo_type
            make_memo(request.data.get("memo"), request.data.get("memo_type"))
        except ValueError:
            return render_error_response(_("invalid 'memo' for 'memo_type'"))

        try:
            customer_id = rci.put(
                {
                    "id": request.data.get("id"),
                    "account": account,
                    "memo": request.data.get("memo"),
                    "memo_type": request.data.get("memo_type"),
                    "type": request.data.get("type"),
                    **extract_sep9_fields(request.data),
                }
            )
        except ValueError as e:
            return render_error_response(str(e), status_code=400)
        except ObjectDoesNotExist as e:
            return render_error_response(str(e), status_code=404)

        if not isinstance(customer_id, str):
            logger.error(
                "Invalid customer ID returned from put() integration. Must be str."
            )
            return render_error_response(_("unable to process request"))

        return Response({"id": customer_id}, status=202)
예제 #5
0
def parse_request_args(request: Request) -> Dict:
    asset = Asset.objects.filter(code=request.GET.get("asset_code"),
                                 sep6_enabled=True,
                                 withdrawal_enabled=True).first()
    if not asset:
        return {"error": render_error_response(_("invalid 'asset_code'"))}

    lang = request.GET.get("lang")
    if lang:
        err_resp = validate_language(lang)
        if err_resp:
            return {"error": err_resp}
        activate_lang_for_request(lang)

    memo_type = request.GET.get("memo_type")
    if memo_type and memo_type not in Transaction.MEMO_TYPES:
        return {"error": render_error_response(_("invalid 'memo_type'"))}

    try:
        make_memo(request.GET.get("memo"), memo_type)
    except (ValueError, MemoInvalidException):
        return {
            "error": render_error_response(_("invalid 'memo' for 'memo_type'"))
        }

    if not request.GET.get("type"):
        return {"error": render_error_response(_("'type' is required"))}
    if not request.GET.get("dest"):
        return {"error": render_error_response(_("'dest' is required"))}

    args = {
        "asset": asset,
        "memo_type": memo_type,
        "memo": request.GET.get("memo"),
        "lang": request.GET.get("lang"),
        "type": request.GET.get("type"),
        "dest": request.GET.get("dest"),
        "dest_extra": request.GET.get("dest_extra"),
        **extract_sep9_fields(request.GET),
    }

    # add remaining extra params, it's on the anchor to validate them
    for param, value in request.GET.items():
        if param not in args:
            args[param] = value

    return args
 def create_deposit_envelope(transaction,
                             source_account) -> TransactionEnvelope:
     payment_amount = round(
         Decimal(transaction.amount_in) - Decimal(transaction.amount_fee),
         transaction.asset.significant_decimals,
     )
     builder = TransactionBuilder(
         source_account=source_account,
         network_passphrase=settings.STELLAR_NETWORK_PASSPHRASE,
         # only one operation, so base_fee will be multipled by 1
         base_fee=settings.MAX_TRANSACTION_FEE_STROOPS
         or settings.HORIZON_SERVER.fetch_base_fee(),
     )
     _, json_resp = get_account_obj(
         Keypair.from_public_key(transaction.stellar_account))
     if transaction.claimable_balance_supported and is_pending_trust(
             transaction, json_resp):
         logger.debug(
             f"Crafting claimable balance operation for Transaction {transaction.id}"
         )
         claimant = Claimant(destination=transaction.stellar_account)
         asset = Asset(code=transaction.asset.code,
                       issuer=transaction.asset.issuer)
         builder.append_create_claimable_balance_op(
             claimants=[claimant],
             asset=asset,
             amount=str(payment_amount),
             source=transaction.asset.distribution_account,
         )
     else:
         builder.append_payment_op(
             destination=transaction.stellar_account,
             asset_code=transaction.asset.code,
             asset_issuer=transaction.asset.issuer,
             amount=str(payment_amount),
             source=transaction.asset.distribution_account,
         )
     if transaction.memo:
         builder.add_memo(make_memo(transaction.memo,
                                    transaction.memo_type))
     return builder.build()
예제 #7
0
def deposit(account: str, client_domain: Optional[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.data.get("asset_code")
    stellar_account = request.data.get("account")
    lang = request.data.get("lang")
    sep9_fields = extract_sep9_fields(request.data)
    claimable_balance_supported = request.data.get(
        "claimable_balance_supported")
    if not claimable_balance_supported:
        claimable_balance_supported = False
    elif isinstance(claimable_balance_supported, str):
        if claimable_balance_supported.lower() not in ["true", "false"]:
            return render_error_response(
                _("'claimable_balance_supported' value must be 'true' or 'false'"
                  ))
        claimable_balance_supported = claimable_balance_supported.lower(
        ) == "true"
    elif not isinstance(claimable_balance_supported, bool):
        return render_error_response(
            _("unexpected data type for 'claimable_balance_supprted'. Expected string or boolean."
              ))

    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"))

    # Ensure memo won't cause stellar transaction to fail when submitted
    try:
        make_memo(request.data.get("memo"), request.data.get("memo_type"))
    except ValueError:
        return render_error_response(_("invalid 'memo' for 'memo_type'"))

    # 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 and asset.sep24_enabled):
        return render_error_response(
            _("invalid operation for asset %s") % asset_code)

    amount = None
    if request.data.get("amount"):
        try:
            amount = Decimal(request.data.get("amount"))
        except DecimalException:
            return render_error_response(_("invalid 'amount'"))
        if not (asset.deposit_min_amount <= amount <=
                asset.deposit_max_amount):
            return render_error_response(_("invalid 'amount'"))

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

    try:
        rdi.save_sep9_fields(
            stellar_account,
            sep9_fields,
            lang,
        )
    except ValueError as e:
        # The anchor found a validation error in the sep-9 fields POSTed by
        # the wallet. The error string returned should be in the language
        # specified in the request.
        return render_error_response(str(e))

    # 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,
        protocol=Transaction.PROTOCOL.sep24,
        claimable_balance_supported=claimable_balance_supported,
        memo=request.data.get("memo"),
        memo_type=request.data.get("memo_type") or Transaction.MEMO_TYPES.hash,
        more_info_url=request.build_absolute_uri(
            f"{reverse('more_info')}?id={transaction_id}"),
        client_domain=client_domain,
    )
    logger.info(f"Created deposit transaction {transaction_id}")

    url = interactive_url(
        request,
        str(transaction_id),
        account,
        asset_code,
        settings.OPERATION_DEPOSIT,
        amount,
    )
    return Response(
        {
            "type": "interactive_customer_info_needed",
            "url": url,
            "id": transaction_id
        },
        status=status.HTTP_200_OK,
    )
예제 #8
0
def parse_request_args(request: Request) -> Dict:
    asset = Asset.objects.filter(code=request.GET.get("asset_code"),
                                 sep6_enabled=True,
                                 withdrawal_enabled=True).first()
    if not asset:
        return {"error": render_error_response(_("invalid 'asset_code'"))}

    lang = request.GET.get("lang")
    if lang:
        err_resp = validate_language(lang)
        if err_resp:
            return {"error": err_resp}
        activate_lang_for_request(lang)

    memo_type = request.GET.get("memo_type")
    if memo_type and memo_type not in Transaction.MEMO_TYPES:
        return {"error": render_error_response(_("invalid 'memo_type'"))}

    try:
        make_memo(request.GET.get("memo"), memo_type)
    except (ValueError, MemoInvalidException):
        return {
            "error": render_error_response(_("invalid 'memo' for 'memo_type'"))
        }

    if not request.GET.get("type"):
        return {"error": render_error_response(_("'type' is required"))}
    if not request.GET.get("dest"):
        return {"error": render_error_response(_("'dest' is required"))}

    on_change_callback = request.GET.get("on_change_callback")
    if on_change_callback and on_change_callback.lower() != "postmessage":
        schemes = ["https"] if not settings.LOCAL_MODE else ["https", "http"]
        try:
            URLValidator(schemes=schemes)(on_change_callback)
        except ValidationError:
            return {
                "error":
                render_error_response(_("invalid callback URL provided"))
            }
        if any(domain in on_change_callback
               for domain in settings.CALLBACK_REQUEST_DOMAIN_DENYLIST):
            on_change_callback = None

    amount = request.GET.get("amount")
    if amount:
        try:
            amount = round(Decimal(amount), asset.significant_decimals)
        except DecimalException:
            return {"error": render_error_response(_("invalid 'amount'"))}
        min_amount = round(asset.withdrawal_min_amount,
                           asset.significant_decimals)
        max_amount = round(asset.withdrawal_max_amount,
                           asset.significant_decimals)
        if not (min_amount <= amount <= max_amount):
            return {
                "error":
                render_error_response(
                    _("'amount' must be within [%s, %s]") %
                    (min_amount, min_amount))
            }

    args = {
        "asset": asset,
        "memo_type": memo_type,
        "memo": request.GET.get("memo"),
        "lang": request.GET.get("lang"),
        "type": request.GET.get("type"),
        "dest": request.GET.get("dest"),
        "dest_extra": request.GET.get("dest_extra"),
        "on_change_callback": on_change_callback,
        "amount": amount,
        "country_code": request.GET.get("country_code"),
        **extract_sep9_fields(request.GET),
    }

    # add remaining extra params, it's on the anchor to validate them
    for param, value in request.GET.items():
        if param not in args:
            args[param] = value

    return args