Skip to content

Commit

Permalink
Merge branch 'main' into nut-19
Browse files Browse the repository at this point in the history
  • Loading branch information
callebtc committed Dec 4, 2024
2 parents 434c460 + 399c201 commit 0d1e69a
Show file tree
Hide file tree
Showing 20 changed files with 1,126 additions and 417 deletions.
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ MINT_INFO_MOTD="Message to users"
MINT_INFO_ICON_URL="https://mint.host/icon.jpg"
MINT_INFO_URLS=["https://mint.host", "http://mint8gv0sq5ul602uxt2fe0t80e3c2bi9fy0cxedp69v1vat6ruj81wv.onion"]

MINT_PRIVATE_KEY=supersecretprivatekey
# This is used to derive your mint's private keys. Store it securely.
# MINT_PRIVATE_KEY=<openssl rand -hex 32>

# Increment derivation path to rotate to a new keyset
# Example: m/0'/0'/0' -> m/0'/0'/1'
Expand Down
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
jobs:
checks:
uses: ./.github/workflows/checks.yml

tests:
strategy:
fail-fast: false
Expand All @@ -25,6 +26,22 @@ jobs:
poetry-version: ${{ matrix.poetry-version }}
mint-only-deprecated: ${{ matrix.mint-only-deprecated }}
mint-database: ${{ matrix.mint-database }}

tests_redis_cache:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
python-version: ["3.10"]
poetry-version: ["1.7.1"]
mint-database: ["./test_data/test_mint", "postgres://cashu:cashu@localhost:5432/cashu"]
uses: ./.github/workflows/tests_redis_cache.yml
with:
os: ${{ matrix.os }}
python-version: ${{ matrix.python-version }}
poetry-version: ${{ matrix.poetry-version }}
mint-database: ${{ matrix.mint-database }}

regtest:
uses: ./.github/workflows/regtest.yml
strategy:
Expand Down
63 changes: 63 additions & 0 deletions .github/workflows/tests_redis_cache.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: tests_redis_cache

on:
workflow_call:
inputs:
python-version:
default: "3.10.4"
type: string
poetry-version:
default: "1.7.1"
type: string
mint-database:
default: ""
type: string
os:
default: "ubuntu-latest"
type: string
mint-only-deprecated:
default: "false"
type: string

jobs:
poetry:
name: Run (db ${{ inputs.mint-database }}, deprecated api ${{ inputs.mint-only-deprecated }})
runs-on: ${{ inputs.os }}
steps:
- name: Start PostgreSQL service
if: contains(inputs.mint-database, 'postgres')
run: |
docker run -d --name postgres -e POSTGRES_USER=cashu -e POSTGRES_PASSWORD=cashu -e POSTGRES_DB=cashu -p 5432:5432 postgres:latest
until docker exec postgres pg_isready; do sleep 1; done
- name: Checkout repository
uses: actions/checkout@v2

- name: Prepare environment
uses: ./.github/actions/prepare
with:
python-version: ${{ inputs.python-version }}
poetry-version: ${{ inputs.poetry-version }}

- name: Start Redis service
run: |
docker compose -f docker/docker-compose.yml up -d redis
- name: Run tests
env:
MINT_BACKEND_BOLT11_SAT: FakeWallet
WALLET_NAME: test_wallet
MINT_HOST: localhost
MINT_PORT: 3337
MINT_TEST_DATABASE: ${{ inputs.mint-database }}
TOR: false
MINT_REDIS_CACHE_ENABLED: true
MINT_REDIS_CACHE_URL: redis://localhost:6379
run: |
poetry run pytest tests/test_mint_api_cache.py -v --cov=mint --cov-report=xml
- name: Stop and clean up Docker Compose
run: |
docker compose -f docker/docker-compose.yml down
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
6 changes: 5 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@ repos:
rev: v1.6.0
hooks:
- id: mypy
args: [--ignore-missing, --check-untyped-defs]
args:
- --ignore-missing
- --check-untyped-defs
additional_dependencies:
- types-redis
6 changes: 6 additions & 0 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,12 @@ def __init__(
input_fee_ppk: Optional[int] = None,
id: str = "",
):
DEFAULT_SEED = "supersecretprivatekey"
if seed == DEFAULT_SEED:
raise Exception(
f"Seed is set to default value '{DEFAULT_SEED}'. Please change it."
)

self.derivation_path = derivation_path

if encrypted_seed and not settings.mint_seed_decryption_key:
Expand Down
2 changes: 1 addition & 1 deletion cashu/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ def preprocess_deprecated_contact_field(cls, values):
class Nut15MppSupport(BaseModel):
method: str
unit: str
mpp: bool


class GetInfoResponse_deprecated(BaseModel):
Expand Down Expand Up @@ -133,6 +132,7 @@ class PostMintQuoteRequest(BaseModel):
default=None, max_length=settings.mint_max_request_length
) # quote lock pubkey


class PostMintQuoteResponse(BaseModel):
quote: str # quote id
request: str # input payment request
Expand Down
3 changes: 2 additions & 1 deletion cashu/core/nuts/nuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@
HTLC_NUT = 14
MPP_NUT = 15
WEBSOCKETS_NUT = 17
MINT_QUOTE_SIGNATURE_NUT = 19 # TODO: change to actual number
CACHE_NUT = 19
MINT_QUOTE_SIGNATURE_NUT = 20
7 changes: 7 additions & 0 deletions cashu/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,12 @@ class CoreLightningRestFundingSource(MintSettings):
mint_corelightning_rest_cert: Optional[str] = Field(default=None)


class MintRedisCache(MintSettings):
mint_redis_cache_enabled: bool = Field(default=False)
mint_redis_cache_url: Optional[str] = Field(default=None)
mint_redis_cache_ttl: Optional[int] = Field(default=60 * 60 * 24 * 7) # 1 week


class Settings(
EnvSettings,
LndRPCFundingSource,
Expand All @@ -241,6 +247,7 @@ class Settings(
FakeWalletSettings,
MintLimits,
MintBackends,
MintRedisCache,
MintDeprecationFlags,
MintSettings,
MintInformation,
Expand Down
43 changes: 22 additions & 21 deletions cashu/mint/app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import asyncio
import sys
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from traceback import print_exception

from fastapi import FastAPI, status
Expand All @@ -10,7 +13,7 @@
from ..core.errors import CashuError
from ..core.logging import configure_logger
from ..core.settings import settings
from .router import router
from .router import redis, router
from .router_deprecated import router_deprecated
from .startup import shutdown_mint as shutdown_mint_init
from .startup import start_mint_init
Expand All @@ -23,27 +26,35 @@

from .middleware import add_middlewares, request_validation_exception_handler

# 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

@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
await start_mint_init()
try:
yield
except asyncio.CancelledError:
# Handle the cancellation gracefully
logger.info("Shutdown process interrupted by CancelledError")
finally:
try:
await redis.disconnect()
await shutdown_mint_init()
except asyncio.CancelledError:
logger.info("CancelledError during shutdown, shutting down forcefully")


def create_app(config_object="core.settings") -> FastAPI:
configure_logger()

app = FastAPI(
title="Nutshell Cashu Mint",
description="Ecash wallet and mint based on the Cashu protocol.",
title="Nutshell Mint",
description="Ecash mint based on the Cashu protocol.",
version=settings.version,
license_info={
"name": "MIT License",
"url": "https://raw.githubusercontent.com/cashubtc/cashu/main/LICENSE",
},
lifespan=lifespan,
)

return app
Expand Down Expand Up @@ -99,13 +110,3 @@ 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()


@app.on_event("shutdown")
async def shutdown_mint():
await shutdown_mint_init()
67 changes: 67 additions & 0 deletions cashu/mint/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import asyncio
import functools
import json

from fastapi import Request
from loguru import logger
from pydantic import BaseModel
from redis.asyncio import from_url
from redis.exceptions import ConnectionError

from ..core.errors import CashuError
from ..core.settings import settings


class RedisCache:
expiry = settings.mint_redis_cache_ttl

def __init__(self):
if settings.mint_redis_cache_enabled:
if settings.mint_redis_cache_url is None:
raise CashuError("Redis cache url not provided")
self.redis = from_url(settings.mint_redis_cache_url)
asyncio.create_task(self.test_connection())

async def test_connection(self):
# PING
try:
await self.redis.ping()
logger.success("Connected to Redis caching server.")
except ConnectionError as e:
logger.error("Redis connection error.")
raise e

def cache(self):
def passthrough(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
logger.trace(f"cache wrapper on route {func.__name__}")
result = await func(*args, **kwargs)
return result

return wrapper

def decorator(func):
@functools.wraps(func)
async def wrapper(request: Request, payload: BaseModel):
logger.trace(f"cache wrapper on route {func.__name__}")
key = request.url.path + payload.json()
logger.trace(f"KEY: {key}")
# Check if we have a value under this key
if await self.redis.exists(key):
logger.trace("Returning a cached response...")
resp = await self.redis.get(key)
if resp:
return json.loads(resp)
else:
raise Exception(f"Found no cached response for key {key}")
result = await func(request, payload)
await self.redis.set(name=key, value=result.json(), ex=self.expiry)
return result

return wrapper

return passthrough if not settings.mint_redis_cache_enabled else decorator

async def disconnect(self):
await self.redis.close()
5 changes: 4 additions & 1 deletion cashu/mint/events/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import json
from typing import List, Union

from fastapi import WebSocket
from fastapi import WebSocket, WebSocketDisconnect
from loguru import logger

from ...core.base import MeltQuote, MintQuote, ProofState
Expand Down Expand Up @@ -122,6 +122,9 @@ async def start(self):
resp = await self._handle_request(req)
# Send the response
await self._send_msg(resp)
except WebSocketDisconnect as e:
logger.debug(f"Websocket disconnected: {e}")
raise e
except Exception as e:
err = JSONRPCErrorResponse(
error=JSONRPCError(
Expand Down
Loading

0 comments on commit 0d1e69a

Please sign in to comment.