Пример #1
0
def parse_request_args(request: Request) -> Dict:
    asset = Asset.objects.filter(code=request.GET.get("asset_code"),
                                 sep6_enabled=True,
                                 deposit_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:
        memo = memo_str(request.GET.get("memo"), memo_type)
    except (ValueError, MemoInvalidException):
        return {
            "error": render_error_response(_("invalid 'memo' for 'memo_type'"))
        }

    return {
        "asset": asset,
        "memo_type": memo_type,
        "memo": memo,
        "lang": lang,
        "type": request.GET.get("type"),
        **extract_sep9_fields(request.GET),
    }
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")
    sep9_fields = extract_sep9_fields(request.POST)
    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"))
    elif request.POST.get("memo"):
        # Polaris SEP-24 doesn't support custodial wallets that depend on memos
        # to disambiguate users using the same stellar account. Support would
        # require new or adjusted integration points.
        return render_error_response(_("`memo` parameter is not supported"))

    # Verify that the asset code exists in our database, with withdraw enabled.
    asset = Asset.objects.filter(code=asset_code).first()
    if not (asset and asset.withdrawal_enabled and asset.sep24_enabled):
        return render_error_response(
            _("invalid operation for asset %s") % asset_code)
    elif not asset.distribution_account:
        return render_error_response(
            _("unsupported asset type: %s") % asset_code)

    try:
        rwi.save_sep9_fields(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))

    transaction_id = create_transaction_id()
    Transaction.objects.create(
        id=transaction_id,
        stellar_account=account,
        asset=asset,
        kind=Transaction.KIND.withdrawal,
        status=Transaction.STATUS.incomplete,
        receiving_anchor_account=asset.distribution_account,
        memo_type=Transaction.MEMO_TYPES.hash,
        protocol=Transaction.PROTOCOL.sep24,
    )
    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
    })
Пример #3
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:
        memo = memo_str(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": 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
Пример #4
0
def validate_post_request(request: Request) -> Dict:
    asset_args = {"code": request.data.get("asset_code")}
    if request.data.get("asset_issuer"):
        asset_args["issuer"] = request.data.get("asset_issuer")
    asset = Asset.objects.filter(**asset_args).first()
    if not (asset and asset.sep31_enabled):
        raise ValueError(_("invalid 'asset_code' and 'asset_issuer'"))
    try:
        amount = round(Decimal(request.data.get("amount")),
                       asset.significant_decimals)
    except (InvalidOperation, TypeError):
        raise ValueError(_("invalid 'amount'"))
    if asset.send_min_amount > amount or amount > asset.send_max_amount:
        raise ValueError(_("invalid 'amount'"))
    lang = request.data.get("lang")
    if lang:
        if not _is_supported_language(lang):
            raise ValueError("unsupported 'lang'")
        activate_lang_for_request(lang)
    if not isinstance(request.data.get("fields"), dict):
        raise ValueError(_("'fields' must serialize to a JSON object"))
    elif len(request.data["fields"]) not in [0, 1]:
        raise ValueError("'fields' should only have one key, 'transaction'")
    elif request.data["fields"] and not isinstance(
            request.data["fields"].get("transaction"), dict):
        raise ValueError(_("'transaction' value in 'fields' must be a dict"))
    if not (type(request.data.get("sender_id")) in [str, type(None)]
            and type(request.data.get("receiver_id")) in [str, type(None)]):
        raise ValueError(
            _("'sender_id' and 'receiver_id' values must be strings"))
    return {
        "asset": asset,
        "amount": amount,
        "lang": lang,
        # fields are validated in validate_fields()
        "sender_id": request.data.get("sender_id"),
        "receiver_id": request.data.get("receiver_id"),
        "fields": request.data.get("fields"),
    }
Пример #5
0
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")
    sep9_fields = extract_sep9_fields(request.POST)
    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:
        memo = memo_str(request.POST.get("memo"),
                        request.POST.get("memo_type"))
    except ValueError:
        return render_error_response(_("invalid 'memo' for 'memo_type'"))

    amount = None
    if request.POST.get("amount"):
        try:
            amount = Decimal(request.POST.get("amount"))
        except DecimalException as e:
            return render_error_response(_("Invalid 'amount'"))

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

    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,
        memo=memo,
        memo_type=request.POST.get("memo_type") or Transaction.MEMO_TYPES.hash,
    )
    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,
    )
Пример #6
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,
    )
Пример #7
0
def withdraw(
    account: str,
    client_domain: Optional[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.data.get("lang")
    asset_code = request.data.get("asset_code")
    sep9_fields = extract_sep9_fields(request.data)
    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 and asset.withdrawal_enabled and asset.sep24_enabled
            and asset.distribution_account):
        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.withdrawal_min_amount <= amount <=
                asset.withdrawal_max_amount):
            return render_error_response(_("invalid 'amount'"))

    try:
        rwi.save_sep9_fields(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))

    transaction_id = create_transaction_id()
    Transaction.objects.create(
        id=transaction_id,
        stellar_account=account,
        asset=asset,
        kind=Transaction.KIND.withdrawal,
        status=Transaction.STATUS.incomplete,
        receiving_anchor_account=asset.distribution_account,
        memo_type=Transaction.MEMO_TYPES.hash,
        protocol=Transaction.PROTOCOL.sep24,
        more_info_url=request.build_absolute_uri(
            f"{reverse('more_info')}?id={transaction_id}"),
        client_domain=client_domain,
    )
    logger.info(f"Created withdrawal transaction {transaction_id}")

    url = interactive_url(
        request,
        str(transaction_id),
        account,
        asset_code,
        settings.OPERATION_WITHDRAWAL,
        amount,
    )
    return Response({
        "type": "interactive_customer_info_needed",
        "url": url,
        "id": transaction_id
    })
Пример #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