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