From eaa00643db8a782c98253fecc17f38b5bf0c03ab Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 16 Mar 2024 23:57:09 +0100 Subject: [PATCH 1/5] Add slowapi --- .env.example | 14 +++- cashu/core/settings.py | 17 ++-- cashu/mint/app.py | 79 ++++--------------- cashu/mint/middleware.py | 81 ++++++++++++++++++++ poetry.lock | 162 ++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 6 files changed, 282 insertions(+), 72 deletions(-) create mode 100644 cashu/mint/middleware.py diff --git a/.env.example b/.env.example index d8e1b7cd..ff98f330 100644 --- a/.env.example +++ b/.env.example @@ -84,10 +84,16 @@ LIGHTNING_FEE_PERCENT=1.0 # minimum fee to reserve LIGHTNING_RESERVE_FEE_MIN=2000 -# Management -# max peg-in amount in satoshis +# Limits + +# Max peg-in amount in satoshis # MINT_MAX_PEG_IN=100000 -# max peg-out amount in satoshis +# Max peg-out amount in satoshis # MINT_MAX_PEG_OUT=100000 -# use to allow only peg-out to LN +# Use to allow only peg-out to LN # MINT_PEG_OUT_ONLY=FALSE + +# Rate limit requests to mint. Make sure that you can see request IPs in the logs. +# You may need to adjust your reverse proxy if you only see requests originating from 127.0.0.1 +# MINT_RATE_LIMIT=TRUE +# MINT_RATE_LIMIT_PER_MINUTE=20 diff --git a/cashu/core/settings.py b/cashu/core/settings.py index a27fddac..d7de8a9b 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -58,6 +58,17 @@ class MintSettings(CashuSettings): mint_lightning_backend: str = Field(default="LNbitsWallet") mint_database: str = Field(default="data/mint") mint_test_database: str = Field(default="test_data/test_mint") + + mint_lnbits_endpoint: str = Field(default=None) + mint_lnbits_key: str = Field(default=None) + mint_strike_key: str = Field(default=None) + mint_blink_key: str = Field(default=None) + + +class MintLimits(MintSettings): + mint_rate_limit: bool = Field(default=False) + mint_rate_limit_per_minute: int = Field(default=20, gt=0) + mint_peg_out_only: bool = Field( default=False, title="Peg-out only", @@ -90,11 +101,6 @@ class MintSettings(CashuSettings): ), ) - mint_lnbits_endpoint: str = Field(default=None) - mint_lnbits_key: str = Field(default=None) - mint_strike_key: str = Field(default=None) - mint_blink_key: str = Field(default=None) - class FakeWalletSettings(MintSettings): fakewallet_brr: bool = Field(default=True) @@ -167,6 +173,7 @@ class Settings( LndRestFundingSource, CoreLightningRestFundingSource, FakeWalletSettings, + MintLimits, MintSettings, MintInformation, WalletSettings, diff --git a/cashu/mint/app.py b/cashu/mint/app.py index fe7737bb..39d90eff 100644 --- a/cashu/mint/app.py +++ b/cashu/mint/app.py @@ -1,15 +1,11 @@ import sys +from contextlib import asynccontextmanager from traceback import print_exception from fastapi import FastAPI, status -from fastapi.exception_handlers import ( - request_validation_exception_handler as _request_validation_exception_handler, -) from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from loguru import logger -from starlette.middleware import Middleware -from starlette.middleware.cors import CORSMiddleware from starlette.requests import Request from ..core.errors import CashuError @@ -20,43 +16,25 @@ from .startup import start_mint_init if settings.debug_profiling: - from fastapi_profiler import PyInstrumentProfilerMiddleware + pass -# from starlette_context import context -# from starlette_context.middleware import RawContextMiddleware +if settings.mint_rate_limit: + pass +from .middleware import add_middlewares, request_validation_exception_handler -# class CustomHeaderMiddleware(BaseHTTPMiddleware): -# """ -# Middleware for starlette that can set the context from request headers -# """ -# async def dispatch(self, request, call_next): -# context["client-version"] = request.headers.get("Client-version") -# response = await call_next(request) -# return response +@asynccontextmanager +async def lifespan(app: FastAPI): + # startup routines here + await start_mint_init() + yield + # shutdown routines here def create_app(config_object="core.settings") -> FastAPI: configure_logger() - # middleware = [ - # Middleware( - # RawContextMiddleware, - # ), - # Middleware(CustomHeaderMiddleware), - # ] - - middleware = [ - Middleware( - CORSMiddleware, - allow_origins=["*"], - allow_methods=["*"], - allow_headers=["*"], - expose_headers=["*"], - ) - ] - app = FastAPI( title="Nutshell Cashu Mint", description="Ecash wallet and mint based on the Cashu protocol.", @@ -65,18 +43,16 @@ def create_app(config_object="core.settings") -> FastAPI: "name": "MIT License", "url": "https://raw.githubusercontent.com/cashubtc/cashu/main/LICENSE", }, - middleware=middleware, ) - if settings.debug_profiling: - assert PyInstrumentProfilerMiddleware is not None - app.add_middleware(PyInstrumentProfilerMiddleware) - return app app = create_app() +# Add middlewares +add_middlewares(app) + @app.middleware("http") async def catch_exceptions(request: Request, call_next): @@ -113,33 +89,12 @@ async def catch_exceptions(request: Request, call_next): ) -async def request_validation_exception_handler( - request: Request, exc: RequestValidationError -) -> JSONResponse: - """ - This is a wrapper to the default RequestValidationException handler of FastAPI. - This function will be called when client input is not valid. - """ - query_params = request.query_params._dict - detail = { - "errors": exc.errors(), - "query_params": query_params, - } - # log the error - logger.error(detail) - # pass on - return await _request_validation_exception_handler(request, exc) - - -@app.on_event("startup") -async def startup_mint(): - await start_mint_init() - +# Add exception handlers +app.add_exception_handler(RequestValidationError, request_validation_exception_handler) +# Add routers if settings.debug_mint_only_deprecated: app.include_router(router=router_deprecated, tags=["Deprecated"], deprecated=True) else: app.include_router(router=router, tags=["Mint"]) app.include_router(router=router_deprecated, tags=["Deprecated"], deprecated=True) - -app.add_exception_handler(RequestValidationError, request_validation_exception_handler) diff --git a/cashu/mint/middleware.py b/cashu/mint/middleware.py new file mode 100644 index 00000000..69fc396b --- /dev/null +++ b/cashu/mint/middleware.py @@ -0,0 +1,81 @@ +from fastapi import FastAPI, status +from fastapi.exception_handlers import ( + request_validation_exception_handler as _request_validation_exception_handler, +) +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse +from loguru import logger +from starlette.middleware.cors import CORSMiddleware +from starlette.requests import Request + +from ..core.settings import settings + +if settings.debug_profiling: + from fastapi_profiler import PyInstrumentProfilerMiddleware + +if settings.mint_rate_limit: + from slowapi import Limiter + from slowapi.errors import RateLimitExceeded + from slowapi.middleware import SlowAPIMiddleware + from slowapi.util import get_remote_address + + +def add_middlewares(app: FastAPI): + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], + expose_headers=["*"], + ) + + if settings.debug_profiling: + assert PyInstrumentProfilerMiddleware is not None + app.add_middleware(PyInstrumentProfilerMiddleware) + + if settings.mint_rate_limit: + + def get_remote_address_excluding_local(request: Request) -> str: + remote_address = get_remote_address(request) + if remote_address == "127.0.0.1": + return "" + return remote_address + + limiter = Limiter( + key_func=get_remote_address_excluding_local, + default_limits=[f"{settings.mint_rate_limit_per_minute}/minute"], + ) + app.state.limiter = limiter + + def _rate_limit_exceeded_handler( + request: Request, exc: Exception + ) -> JSONResponse: + remote_address = get_remote_address(request) + logger.warning( + f"Rate limit {settings.mint_rate_limit_per_minute}/minute exceeded: {remote_address}" + ) + return JSONResponse( + status_code=status.HTTP_429_TOO_MANY_REQUESTS, + content={"detail": "Rate limit exceeded."}, + ) + + app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) + app.add_middleware(SlowAPIMiddleware) + + +async def request_validation_exception_handler( + request: Request, exc: RequestValidationError +) -> JSONResponse: + """ + This is a wrapper to the default RequestValidationException handler of FastAPI. + This function will be called when client input is not valid. + """ + query_params = request.query_params._dict + detail = { + "errors": exc.errors(), + "query_params": query_params, + } + # log the error + logger.error(detail) + # pass on + return await _request_validation_exception_handler(request, exc) diff --git a/poetry.lock b/poetry.lock index ee07069b..d34e7582 100644 --- a/poetry.lock +++ b/poetry.lock @@ -400,6 +400,23 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] +[[package]] +name = "deprecated" +version = "1.2.14" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, + {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, +] + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] + [[package]] name = "distlib" version = "0.3.8" @@ -615,6 +632,24 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.link perf = ["ipython"] testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] +[[package]] +name = "importlib-resources" +version = "6.3.1" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_resources-6.3.1-py3-none-any.whl", hash = "sha256:4811639ca7fa830abdb8e9ca0a104dc6ad13de691d9fe0d3173a71304f068159"}, + {file = "importlib_resources-6.3.1.tar.gz", hash = "sha256:29a3d16556e330c3c8fb8202118c5ff41241cc34cbfb25989bbad226d99b7995"}, +] + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.collections", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -626,6 +661,35 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "limits" +version = "3.10.0" +description = "Rate limiting utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "limits-3.10.0-py3-none-any.whl", hash = "sha256:3e617a580f57a21b39393f833c27ad0378c87b309e908c154ee69e6740041959"}, + {file = "limits-3.10.0.tar.gz", hash = "sha256:6e657dccafce64fd8ee023ebf4593cd47e9eac841fd1dec3448f48673ba10b7c"}, +] + +[package.dependencies] +deprecated = ">=1.2" +importlib-resources = ">=1.3" +packaging = ">=21,<24" +typing-extensions = "*" + +[package.extras] +all = ["aetcd", "coredis (>=3.4.0,<5)", "emcache (>=0.6.1)", "emcache (>=1)", "etcd3", "motor (>=3,<4)", "pymemcache (>3,<5.0.0)", "pymongo (>4.1,<5)", "redis (>3,!=4.5.2,!=4.5.3,<6.0.0)", "redis (>=4.2.0,!=4.5.2,!=4.5.3)"] +async-etcd = ["aetcd"] +async-memcached = ["emcache (>=0.6.1)", "emcache (>=1)"] +async-mongodb = ["motor (>=3,<4)"] +async-redis = ["coredis (>=3.4.0,<5)"] +etcd = ["etcd3"] +memcached = ["pymemcache (>3,<5.0.0)"] +mongodb = ["pymongo (>4.1,<5)"] +redis = ["redis (>3,!=4.5.2,!=4.5.3,<6.0.0)"] +rediscluster = ["redis (>=4.2.0,!=4.5.2,!=4.5.3)"] + [[package]] name = "loguru" version = "0.7.2" @@ -1316,6 +1380,23 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "slowapi" +version = "0.1.9" +description = "A rate limiting extension for Starlette and Fastapi" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "slowapi-0.1.9-py3-none-any.whl", hash = "sha256:cfad116cfb84ad9d763ee155c1e5c5cbf00b0d47399a769b227865f5df576e36"}, + {file = "slowapi-0.1.9.tar.gz", hash = "sha256:639192d0f1ca01b1c6d95bf6c71d794c3a9ee189855337b4821f7f457dddad77"}, +] + +[package.dependencies] +limits = ">=2.3" + +[package.extras] +redis = ["redis (>=3.4.1,<4.0.0)"] + [[package]] name = "sniffio" version = "1.3.0" @@ -1537,6 +1618,85 @@ files = [ [package.extras] dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] +[[package]] +name = "wrapt" +version = "1.16.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.6" +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, +] + [[package]] name = "zipp" version = "3.17.0" @@ -1558,4 +1718,4 @@ pgsql = ["psycopg2-binary"] [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "94a66019b5c9fd191e33aa9c9a2a6a22a2a0db1d60110e858673738738ece902" +content-hash = "d941bf9a1f3f01b6d9e9e16118b1ae6dfa2244b80a6433728a4e67a77420a527" diff --git a/pyproject.toml b/pyproject.toml index 917a8dba..0448d665 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ bip32 = "^3.4" mnemonic = "^0.20" bolt11 = "^2.0.5" pre-commit = "^3.5.0" +slowapi = "^0.1.9" [tool.poetry.extras] pgsql = ["psycopg2-binary"] From 2d40cc32597022dc35b4bfb4c37f7de91853af54 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 17 Mar 2024 00:14:24 +0100 Subject: [PATCH 2/5] fix startup --- cashu/mint/app.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/cashu/mint/app.py b/cashu/mint/app.py index 39d90eff..619481a4 100644 --- a/cashu/mint/app.py +++ b/cashu/mint/app.py @@ -1,5 +1,4 @@ import sys -from contextlib import asynccontextmanager from traceback import print_exception from fastapi import FastAPI, status @@ -23,13 +22,14 @@ from .middleware import add_middlewares, request_validation_exception_handler - -@asynccontextmanager -async def lifespan(app: FastAPI): - # startup routines here - await start_mint_init() - yield - # shutdown routines here +# this errors with the tests but is the appropriate way to handle startup and shutdown +# until then, we use @app.on_event("startup") +# @asynccontextmanager +# async def lifespan(app: FastAPI): +# # startup routines here +# await start_mint_init() +# yield +# # shutdown routines here def create_app(config_object="core.settings") -> FastAPI: @@ -98,3 +98,8 @@ async def catch_exceptions(request: Request, call_next): else: app.include_router(router=router, tags=["Mint"]) app.include_router(router=router_deprecated, tags=["Deprecated"], deprecated=True) + + +@app.on_event("startup") +async def startup_mint(): + await start_mint_init() From d95a467a320ddd0753d0c0ec0b5feea07edeebdd Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 17 Mar 2024 00:30:14 +0100 Subject: [PATCH 3/5] adjust settings --- cashu/core/settings.py | 47 ++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/cashu/core/settings.py b/cashu/core/settings.py index d7de8a9b..766d200a 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -55,9 +55,20 @@ class MintSettings(CashuSettings): mint_derivation_path_list: List[str] = Field(default=[]) mint_listen_host: str = Field(default="127.0.0.1") mint_listen_port: int = Field(default=3338) - mint_lightning_backend: str = Field(default="LNbitsWallet") mint_database: str = Field(default="data/mint") mint_test_database: str = Field(default="test_data/test_mint") + mint_duplicate_keysets: bool = Field( + default=True, + title="Duplicate keysets", + description=( + "Whether to duplicate keysets for backwards compatibility before v1 API" + " (Nutshell 0.15.0)." + ), + ) + + +class MintBackends(MintSettings): + mint_lightning_backend: str = Field(default="FakeWallet") mint_lnbits_endpoint: str = Field(default=None) mint_lnbits_key: str = Field(default=None) @@ -66,8 +77,20 @@ class MintSettings(CashuSettings): class MintLimits(MintSettings): - mint_rate_limit: bool = Field(default=False) - mint_rate_limit_per_minute: int = Field(default=20, gt=0) + mint_rate_limit: bool = Field( + default=False, title="Rate limit", description="IP-based rate limiter." + ) + mint_rate_limit_per_minute: int = Field( + default=20, + gt=0, + title="Rate limit per minute", + description="Number of requests an IP can make per minute.", + ) + mint_max_request_length: int = Field( + default=1000, + title="Maximum request length", + description="Maximum length of REST API request arrays.", + ) mint_peg_out_only: bool = Field( default=False, @@ -84,22 +107,9 @@ class MintLimits(MintSettings): title="Maximum peg-out", description="Maximum amount for a melt operation.", ) - mint_max_request_length: int = Field( - default=1000, - title="Maximum request length", - description="Maximum length of REST API request arrays.", - ) mint_max_balance: int = Field( default=None, title="Maximum mint balance", description="Maximum mint balance." ) - mint_duplicate_keysets: bool = Field( - default=True, - title="Duplicate keysets", - description=( - "Whether to duplicate keysets for backwards compatibility before v1 API" - " (Nutshell 0.15.0)." - ), - ) class FakeWalletSettings(MintSettings): @@ -140,13 +150,13 @@ class WalletSettings(CashuSettings): "wss://relay.damus.io", "wss://nostr.mom", "wss://relay.snort.social", - "wss://nostr.fmt.wiz.biz", + "wss://nostr.mutinywallet.com", "wss://relay.minibits.cash", "wss://nos.lol", "wss://relay.nostr.band", "wss://relay.bitcoiner.social", "wss://140.f7z.io", - "wss://relayable.org", + "wss://relay.primal.net", ] ) @@ -174,6 +184,7 @@ class Settings( CoreLightningRestFundingSource, FakeWalletSettings, MintLimits, + MintBackends, MintSettings, MintInformation, WalletSettings, From e8bb33b24836985557238f5955437cb06240f7d3 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 23 Mar 2024 02:00:38 +0100 Subject: [PATCH 4/5] add rate limits to tx routes --- .env.example | 2 +- cashu/core/settings.py | 12 +++++++--- cashu/mint/limit.py | 39 +++++++++++++++++++++++++++++++++ cashu/mint/middleware.py | 36 +++++------------------------- cashu/mint/router.py | 26 +++++++++++++++++----- cashu/mint/router_deprecated.py | 14 ++++++++++-- 6 files changed, 86 insertions(+), 43 deletions(-) create mode 100644 cashu/mint/limit.py diff --git a/.env.example b/.env.example index 4eeb2fe4..459d1b67 100644 --- a/.env.example +++ b/.env.example @@ -98,4 +98,4 @@ LIGHTNING_RESERVE_FEE_MIN=2000 # Rate limit requests to mint. Make sure that you can see request IPs in the logs. # You may need to adjust your reverse proxy if you only see requests originating from 127.0.0.1 # MINT_RATE_LIMIT=TRUE -# MINT_RATE_LIMIT_PER_MINUTE=20 +# MINT_GLOBAL_RATE_LIMIT_PER_MINUTE=20 diff --git a/cashu/core/settings.py b/cashu/core/settings.py index 1db8a8a7..1c0ee68b 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -83,11 +83,17 @@ class MintLimits(MintSettings): mint_rate_limit: bool = Field( default=False, title="Rate limit", description="IP-based rate limiter." ) - mint_rate_limit_per_minute: int = Field( + mint_global_rate_limit_per_minute: int = Field( + default=60, + gt=0, + title="Global rate limit per minute", + description="Number of requests an IP can make per minute to all endpoints.", + ) + mint_transaction_rate_limit_per_minute: int = Field( default=20, gt=0, - title="Rate limit per minute", - description="Number of requests an IP can make per minute.", + title="Transaction rate limit per minute", + description="Number of requests an IP can make per minute to transaction endpoints.", ) mint_max_request_length: int = Field( default=1000, diff --git a/cashu/mint/limit.py b/cashu/mint/limit.py new file mode 100644 index 00000000..9171dc64 --- /dev/null +++ b/cashu/mint/limit.py @@ -0,0 +1,39 @@ +from fastapi import status +from fastapi.responses import JSONResponse +from loguru import logger +from slowapi import Limiter +from slowapi.util import get_remote_address +from starlette.requests import Request + +from ..core.settings import settings + + +def _rate_limit_exceeded_handler(request: Request, exc: Exception) -> JSONResponse: + remote_address = get_remote_address(request) + logger.warning( + f"Rate limit {settings.mint_global_rate_limit_per_minute}/minute exceeded: {remote_address}" + ) + return JSONResponse( + status_code=status.HTTP_429_TOO_MANY_REQUESTS, + content={"detail": "Rate limit exceeded."}, + ) + + +def get_remote_address_excluding_local(request: Request) -> str: + remote_address = get_remote_address(request) + if remote_address == "127.0.0.1": + return "" + return remote_address + + +limiter_global = Limiter( + key_func=get_remote_address_excluding_local, + default_limits=[f"{settings.mint_global_rate_limit_per_minute}/minute"], + enabled=settings.mint_rate_limit, +) + +limiter = Limiter( + key_func=get_remote_address_excluding_local, + default_limits=[f"{settings.mint_transaction_rate_limit_per_minute}/minute"], + enabled=settings.mint_rate_limit, +) diff --git a/cashu/mint/middleware.py b/cashu/mint/middleware.py index 69fc396b..f70dbc9c 100644 --- a/cashu/mint/middleware.py +++ b/cashu/mint/middleware.py @@ -1,4 +1,4 @@ -from fastapi import FastAPI, status +from fastapi import FastAPI from fastapi.exception_handlers import ( request_validation_exception_handler as _request_validation_exception_handler, ) @@ -9,15 +9,13 @@ from starlette.requests import Request from ..core.settings import settings +from .limit import _rate_limit_exceeded_handler, limiter_global if settings.debug_profiling: from fastapi_profiler import PyInstrumentProfilerMiddleware -if settings.mint_rate_limit: - from slowapi import Limiter - from slowapi.errors import RateLimitExceeded - from slowapi.middleware import SlowAPIMiddleware - from slowapi.util import get_remote_address +from slowapi.errors import RateLimitExceeded +from slowapi.middleware import SlowAPIMiddleware def add_middlewares(app: FastAPI): @@ -34,31 +32,7 @@ def add_middlewares(app: FastAPI): app.add_middleware(PyInstrumentProfilerMiddleware) if settings.mint_rate_limit: - - def get_remote_address_excluding_local(request: Request) -> str: - remote_address = get_remote_address(request) - if remote_address == "127.0.0.1": - return "" - return remote_address - - limiter = Limiter( - key_func=get_remote_address_excluding_local, - default_limits=[f"{settings.mint_rate_limit_per_minute}/minute"], - ) - app.state.limiter = limiter - - def _rate_limit_exceeded_handler( - request: Request, exc: Exception - ) -> JSONResponse: - remote_address = get_remote_address(request) - logger.warning( - f"Rate limit {settings.mint_rate_limit_per_minute}/minute exceeded: {remote_address}" - ) - return JSONResponse( - status_code=status.HTTP_429_TOO_MANY_REQUESTS, - content={"detail": "Rate limit exceeded."}, - ) - + app.state.limiter = limiter_global app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.add_middleware(SlowAPIMiddleware) diff --git a/cashu/mint/router.py b/cashu/mint/router.py index 19e6f46b..cd7b6cf8 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -1,6 +1,6 @@ from typing import Any, Dict, List -from fastapi import APIRouter +from fastapi import APIRouter, Request from loguru import logger from ..core.base import ( @@ -28,6 +28,7 @@ from ..core.errors import CashuError from ..core.settings import settings from ..mint.startup import ledger +from .limit import limiter router: APIRouter = APIRouter() @@ -178,7 +179,10 @@ async def keysets() -> KeysetsResponse: response_model=PostMintQuoteResponse, response_description="A payment request to mint tokens of a denomination", ) -async def mint_quote(payload: PostMintQuoteRequest) -> PostMintQuoteResponse: +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") +async def mint_quote( + request: Request, payload: PostMintQuoteRequest +) -> PostMintQuoteResponse: """ Request minting of new tokens. The mint responds with a Lightning invoice. This endpoint can be used for a Lightning invoice UX flow. @@ -203,7 +207,8 @@ async def mint_quote(payload: PostMintQuoteRequest) -> PostMintQuoteResponse: response_model=PostMintQuoteResponse, response_description="Get an existing mint quote to check its status.", ) -async def get_mint_quote(quote: str) -> PostMintQuoteResponse: +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") +async def get_mint_quote(request: Request, quote: str) -> PostMintQuoteResponse: """ Get mint quote state. """ @@ -228,7 +233,9 @@ async def get_mint_quote(quote: str) -> PostMintQuoteResponse: "A list of blinded signatures that can be used to create proofs." ), ) +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") async def mint( + request: Request, payload: PostMintRequest, ) -> PostMintResponse: """ @@ -250,7 +257,10 @@ async def mint( response_model=PostMeltQuoteResponse, response_description="Melt tokens for a payment on a supported payment method.", ) -async def get_melt_quote(payload: PostMeltQuoteRequest) -> PostMeltQuoteResponse: +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") +async def get_melt_quote( + request: Request, payload: PostMeltQuoteRequest +) -> PostMeltQuoteResponse: """ Request a quote for melting tokens. """ @@ -266,7 +276,8 @@ async def get_melt_quote(payload: PostMeltQuoteRequest) -> PostMeltQuoteResponse response_model=PostMeltQuoteResponse, response_description="Get an existing melt quote to check its status.", ) -async def melt_quote(quote: str) -> PostMeltQuoteResponse: +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") +async def melt_quote(request: Request, quote: str) -> PostMeltQuoteResponse: """ Get melt quote state. """ @@ -296,7 +307,8 @@ async def melt_quote(quote: str) -> PostMeltQuoteResponse: " promises for change." ), ) -async def melt(payload: PostMeltRequest) -> PostMeltResponse: +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") +async def melt(request: Request, payload: PostMeltRequest) -> PostMeltResponse: """ Requests tokens to be destroyed and sent out via Lightning. """ @@ -320,7 +332,9 @@ async def melt(payload: PostMeltRequest) -> PostMeltResponse: "An array of blinded signatures that can be used to create proofs." ), ) +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") async def swap( + request: Request, payload: PostSplitRequest, ) -> PostSplitResponse: """ diff --git a/cashu/mint/router_deprecated.py b/cashu/mint/router_deprecated.py index e1901d7b..4a970c4f 100644 --- a/cashu/mint/router_deprecated.py +++ b/cashu/mint/router_deprecated.py @@ -1,6 +1,6 @@ from typing import Dict, List, Optional -from fastapi import APIRouter +from fastapi import APIRouter, Request from loguru import logger from ..core.base import ( @@ -28,6 +28,7 @@ ) from ..core.errors import CashuError from ..core.settings import settings +from .limit import limiter from .startup import ledger router_deprecated: APIRouter = APIRouter() @@ -129,7 +130,10 @@ async def keysets_deprecated() -> KeysetsResponse_deprecated: ), deprecated=True, ) -async def request_mint_deprecated(amount: int = 0) -> GetMintResponse_deprecated: +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") +async def request_mint_deprecated( + request: Request, amount: int = 0 +) -> GetMintResponse_deprecated: """ Request minting of new tokens. The mint responds with a Lightning invoice. This endpoint can be used for a Lightning invoice UX flow. @@ -157,7 +161,9 @@ async def request_mint_deprecated(amount: int = 0) -> GetMintResponse_deprecated ), deprecated=True, ) +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") async def mint_deprecated( + request: Request, payload: PostMintRequest_deprecated, hash: Optional[str] = None, payment_hash: Optional[str] = None, @@ -204,7 +210,9 @@ async def mint_deprecated( ), deprecated=True, ) +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") async def melt_deprecated( + request: Request, payload: PostMeltRequest_deprecated, ) -> PostMeltResponse_deprecated: """ @@ -267,7 +275,9 @@ async def check_fees( ), deprecated=True, ) +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") async def split_deprecated( + request: Request, payload: PostSplitRequest_Deprecated, # ) -> Union[PostSplitResponse_Very_Deprecated, PostSplitResponse_Deprecated]: ): From 7a94b1b2802be4d93393081639f055cde0ac1d35 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 23 Mar 2024 02:22:41 +0100 Subject: [PATCH 5/5] elastic --- .env.example | 5 ++++- cashu/mint/limit.py | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 459d1b67..1112a801 100644 --- a/.env.example +++ b/.env.example @@ -98,4 +98,7 @@ LIGHTNING_RESERVE_FEE_MIN=2000 # Rate limit requests to mint. Make sure that you can see request IPs in the logs. # You may need to adjust your reverse proxy if you only see requests originating from 127.0.0.1 # MINT_RATE_LIMIT=TRUE -# MINT_GLOBAL_RATE_LIMIT_PER_MINUTE=20 +# Determines the number of all requests allowed per minute per IP +# MINT_GLOBAL_RATE_LIMIT_PER_MINUTE=60 +# Determines the number of transactions (mint, melt, swap) allowed per minute per IP +# MINT_TRANSACTION_RATE_LIMIT_PER_MINUTE=20 diff --git a/cashu/mint/limit.py b/cashu/mint/limit.py index 9171dc64..1a8a4c28 100644 --- a/cashu/mint/limit.py +++ b/cashu/mint/limit.py @@ -28,12 +28,14 @@ def get_remote_address_excluding_local(request: Request) -> str: limiter_global = Limiter( key_func=get_remote_address_excluding_local, + strategy="fixed-window-elastic-expiry", default_limits=[f"{settings.mint_global_rate_limit_per_minute}/minute"], enabled=settings.mint_rate_limit, ) limiter = Limiter( key_func=get_remote_address_excluding_local, + strategy="fixed-window-elastic-expiry", default_limits=[f"{settings.mint_transaction_rate_limit_per_minute}/minute"], enabled=settings.mint_rate_limit, )