def validate_fields(fields: Dict):
    if not isinstance(fields, Dict):
        raise ValueError(
            "invalid fields type in SEP-12 GET /customer response, should be dict"
        )
    if len(extract_sep9_fields(fields)) < len(fields):
        raise ValueError(
            "SEP-12 GET /customer response fields must be from SEP-9")
    accepted_types = ["string", "binary", "number", "date"]
    for key, value in fields.items():
        if not value.get("type") or value.get("type") not in accepted_types:
            raise ValueError(
                f"bad type value for {key} in SEP-12 GET /customer response")
        elif not (value.get("description")
                  and isinstance(value.get("description"), str)):
            raise ValueError(
                f"bad description value for {key} in SEP-12 GET /customer response"
            )
        elif value.get("choices") and not isinstance(value.get("choices"),
                                                     list):
            raise ValueError(
                f"bad choices value for {key} in SEP-12 GET /customer response"
            )
        elif value.get("optional") and not isinstance(value.get("optional"),
                                                      bool):
            raise ValueError(
                f"bad optional value for {key} in SEP-12 GET /customer response"
            )
Exemple #2
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
    })
Exemple #4
0
def validate_fields(fields: Dict):
    if not isinstance(fields, Dict):
        raise ValueError(
            "invalid fields type in SEP-12 GET /customer response, should be dict"
        )
    if len(extract_sep9_fields(fields)) < len(fields):
        raise ValueError(
            "SEP-12 GET /customer response fields must be from SEP-9")
    accepted_field_attrs = [
        "type",
        "description",
        "choices",
        "optional",
        "status",
        "error",
    ]
    accepted_types = ["string", "binary", "number", "date"]
    accepted_statuses = [
        "ACCEPTED",
        "PROCESSING",
        "NOT_PROVIDED",
        "REJECTED",
        "VERIFICATION_REQUIRED",
    ]
    for key, value in fields.items():
        if not set(value.keys()).issubset(set(accepted_field_attrs)):
            raise ValueError(
                f"unexpected attribute in {key} object in SEP-12 GET /customer response, "
                f"accepted values: {', '.join(accepted_field_attrs)}")
        if not value.get("type") or value.get("type") not in accepted_types:
            raise ValueError(
                f"bad type value for {key} in SEP-12 GET /customer response")
        elif not (value.get("description")
                  and isinstance(value.get("description"), str)):
            raise ValueError(
                f"bad description value for {key} in SEP-12 GET /customer response"
            )
        elif value.get("choices") and not isinstance(value.get("choices"),
                                                     list):
            raise ValueError(
                f"bad choices value for {key} in SEP-12 GET /customer response"
            )
        elif value.get("optional") and not isinstance(value.get("optional"),
                                                      bool):
            raise ValueError(
                f"bad optional value for {key} in SEP-12 GET /customer response"
            )
        elif value.get("status") and value.get(
                "status") not in accepted_statuses:
            raise ValueError(
                f"bad field status value for {key} in SEP-12 GET /customer response"
            )
        elif value.get("error") and not isinstance(value.get("error"), str):
            raise ValueError(
                f"bad error value for {key} in SEP-12 GET /customer response")
    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)
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
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,
    )
Exemple #8
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,
    )
Exemple #9
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
    })
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