Skip to content

feat(mint): add quote lookup by pubkey - #1123

Closed
callebtc wants to merge 1 commit into
cashubtc:mainfrom
callebtc:feat/get-quotes-by-pubkey
Closed

feat(mint): add quote lookup by pubkey#1123
callebtc wants to merge 1 commit into
cashubtc:mainfrom
callebtc:feat/get-quotes-by-pubkey

Conversation

@callebtc

@callebtc callebtc commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • implement the mint-side quote lookup from NUT-XX: Get quotes by pubkeys nuts#341 at POST /v1/mint/quote/bolt11/pubkey
  • verify every mint-bound, domain-separated Schnorr signature before querying quotes
  • return method-scoped NUT-04 quote responses and advertise provisional NUT-XX support
  • cap lookup requests at 50 pubkeys and add an indexed pubkey lookup
  • add focused signature, authorization, routing, response, mint-info, and migration coverage

Scope and compatibility

Testing

  • make check
  • poetry run pytest tests/mint/test_mint_quote_lookup.py tests/mint/test_mint_migrations.py tests/mint/test_mint_app_router.py -q (30 passed)

@callebtc
callebtc force-pushed the feat/get-quotes-by-pubkey branch from 3003ad3 to 2c8306b Compare August 18, 2026 13:03
@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
955 1 954 65
View the top 1 failed test(s) by shortest run time
tests.mint.test_mint_melt::test_melt_lightning_unknown_status_keeps_proofs_pending
Stack Traces | 0.073s run time
self = <sqlalchemy.engine.base.Connection object at 0x7f5140982990>
dialect = <sqlalchemy.dialects.sqlite.aiosqlite.SQLiteDialect_aiosqlite object at 0x7f514147b9b0>
context = <sqlalchemy.dialects.sqlite.aiosqlite.SQLiteExecutionContext_aiosqlite object at 0x7f514300bf20>
statement = <sqlalchemy.dialects.sqlite.base.SQLiteCompiler object at 0x7f514300a060>
parameters = [()]

    def _exec_single_context(
        self,
        dialect: Dialect,
        context: ExecutionContext,
        statement: Union[str, Compiled],
        parameters: Optional[_AnyMultiExecuteParams],
    ) -> CursorResult[Any]:
        """continue the _execute_context() method for a single DBAPI
        cursor.execute() or cursor.executemany() call.
    
        """
        if dialect.bind_typing is BindTyping.SETINPUTSIZES:
            generic_setinputsizes = context._prepare_set_input_sizes()
    
            if generic_setinputsizes:
                try:
                    dialect.do_set_input_sizes(
                        context.cursor, generic_setinputsizes, context
                    )
                except BaseException as e:
                    self._handle_dbapi_exception(
                        e, str(statement), parameters, None, context
                    )
    
        cursor, str_statement, parameters = (
            context.cursor,
            context.statement,
            context.parameters,
        )
    
        effective_parameters: Optional[_AnyExecuteParams]
    
        if not context.executemany:
            effective_parameters = parameters[0]
        else:
            effective_parameters = parameters
    
        if self._has_events or self.engine._has_events:
            for fn in self.dispatch.before_cursor_execute:
                str_statement, effective_parameters = fn(
                    self,
                    cursor,
                    str_statement,
                    effective_parameters,
                    context,
                    context.executemany,
                )
    
        if self._echo:
            self._log_info(str_statement)
    
            stats = context._get_cache_stats()
    
            if not self.engine.hide_parameters:
                self._log_info(
                    "[%s] %r",
                    stats,
                    sql_util._repr_params(
                        effective_parameters,
                        batches=10,
                        ismulti=context.executemany,
                    ),
                )
            else:
                self._log_info(
                    "[%s] [SQL parameters hidden due to hide_parameters=True]",
                    stats,
                )
    
        evt_handled: bool = False
        try:
            if context.execute_style is ExecuteStyle.EXECUTEMANY:
                effective_parameters = cast(
                    "_CoreMultiExecuteParams", effective_parameters
                )
                if self.dialect._has_events:
                    for fn in self.dialect.dispatch.do_executemany:
                        if fn(
                            cursor,
                            str_statement,
                            effective_parameters,
                            context,
                        ):
                            evt_handled = True
                            break
                if not evt_handled:
                    self.dialect.do_executemany(
                        cursor,
                        str_statement,
                        effective_parameters,
                        context,
                    )
            elif not effective_parameters and context.no_parameters:
                if self.dialect._has_events:
                    for fn in self.dialect.dispatch.do_execute_no_params:
                        if fn(cursor, str_statement, context):
                            evt_handled = True
                            break
                if not evt_handled:
                    self.dialect.do_execute_no_params(
                        cursor, str_statement, context
                    )
            else:
                effective_parameters = cast(
                    "_CoreSingleExecuteParams", effective_parameters
                )
                if self.dialect._has_events:
                    for fn in self.dialect.dispatch.do_execute:
                        if fn(
                            cursor,
                            str_statement,
                            effective_parameters,
                            context,
                        ):
                            evt_handled = True
                            break
                if not evt_handled:
>                   self.dialect.do_execute(
                        cursor, str_statement, effective_parameters, context
                    )

../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../sqlalchemy/engine/base.py:1964: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../sqlalchemy/engine/default.py:942: in do_execute
    cursor.execute(statement, parameters)
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../dialects/sqlite/aiosqlite.py:150: in execute
    self._adapt_connection._handle_exception(error)
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../dialects/sqlite/aiosqlite.py:301: in _handle_exception
    raise error
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../dialects/sqlite/aiosqlite.py:132: in execute
    self.await_(_cursor.execute(operation, parameters))
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../sqlalchemy/util/_concurrency_py3k.py:132: in await_only
    return current.parent.switch(awaitable)  # type: ignore[no-any-return,attr-defined] # noqa: E501
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../sqlalchemy/util/_concurrency_py3k.py:196: in greenlet_spawn
    value = await result
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12............/site-packages/aiosqlite/cursor.py:48: in execute
    await self._execute(self._cursor.execute, sql, parameters)
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12............/site-packages/aiosqlite/cursor.py:40: in _execute
    return await self._conn._execute(fn, *args, **kwargs)
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12............/site-packages/aiosqlite/core.py:132: in _execute
    return await future
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Connection(Thread-14016, stopped daemon 139986338838208)>

    def run(self) -> None:
        """
        Execute function calls on a separate thread.
    
        :meta private:
        """
        while True:
            # Continues running until all queue items are processed,
            # even after connection is closed (so we can finalize all
            # futures)
    
            tx_item = self._tx.get()
            if tx_item is _STOP_RUNNING_SENTINEL:
                break
    
            future, function = tx_item
    
            try:
                LOG.debug("executing %s", function)
>               result = function()
E               sqlite3.OperationalError: disk I/O error

../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12............/site-packages/aiosqlite/core.py:115: OperationalError

The above exception was the direct cause of the following exception:

request = <SubRequest 'ledger' for <Coroutine test_melt_lightning_unknown_status_keeps_proofs_pending>>
kwargs = {}, func = <function ledger at 0x7f5145725a80>
event_loop_fixture_id = 'event_loop'
setup = <function _wrap_asyncgen_fixture.<locals>._asyncgen_fixture_wrapper.<locals>.setup at 0x7f5140e060c0>
finalizer = <function _wrap_asyncgen_fixture.<locals>._asyncgen_fixture_wrapper.<locals>.finalizer at 0x7f5140e737e0>

    @functools.wraps(fixture)
    def _asyncgen_fixture_wrapper(request: FixtureRequest, **kwargs: Any):
        func = _perhaps_rebind_fixture_func(fixture, request.instance)
        event_loop_fixture_id = _get_event_loop_fixture_id_for_async_fixture(
            request, func
        )
        event_loop = request.getfixturevalue(event_loop_fixture_id)
        kwargs.pop(event_loop_fixture_id, None)
        gen_obj = func(**_add_kwargs(func, kwargs, event_loop, request))
    
        async def setup():
            res = await gen_obj.__anext__()  # type: ignore[union-attr]
            return res
    
        def finalizer() -> None:
            """Yield again, to finalize."""
    
            async def async_finalizer() -> None:
                try:
                    await gen_obj.__anext__()  # type: ignore[union-attr]
                except StopAsyncIteration:
                    pass
                else:
                    msg = "Async generator fixture didn't stop."
                    msg += "Yield only once."
                    raise ValueError(msg)
    
            event_loop.run_until_complete(async_finalizer())
    
>       result = event_loop.run_until_complete(setup())

../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12....../site-packages/pytest_asyncio/plugin.py:343: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.12.13.../x64/lib/python3.12/asyncio/base_events.py:691: in run_until_complete
    return future.result()
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12....../site-packages/pytest_asyncio/plugin.py:325: in setup
    res = await gen_obj.__anext__()  # type: ignore[union-attr]
tests/conftest.py:127: in ledger
    ledger = await start_mint_init(ledger)
tests/conftest.py:87: in start_mint_init
    await migrate_databases(ledger.db, migrations_mint)
cashu/core/migrations.py:90: in migrate_databases
    exists = await conn.fetchone(
cashu/core/db.py:90: in fetchone
    result = await self.conn.execute(self.rewrite_query(query), values)
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../ext/asyncio/session.py:463: in execute
    result = await greenlet_spawn(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../sqlalchemy/util/_concurrency_py3k.py:201: in greenlet_spawn
    result = context.throw(*sys.exc_info())
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../sqlalchemy/orm/session.py:2365: in execute
    return self._execute_internal(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../sqlalchemy/orm/session.py:2260: in _execute_internal
    result = conn.execute(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../sqlalchemy/engine/base.py:1416: in execute
    return meth(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../sqlalchemy/sql/elements.py:515: in _execute_on_connection
    return connection._execute_clauseelement(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../sqlalchemy/engine/base.py:1638: in _execute_clauseelement
    ret = self._execute_context(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../sqlalchemy/engine/base.py:1843: in _execute_context
    return self._exec_single_context(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../sqlalchemy/engine/base.py:1983: in _exec_single_context
    self._handle_dbapi_exception(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../sqlalchemy/engine/base.py:2352: in _handle_dbapi_exception
    raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../sqlalchemy/engine/base.py:1964: in _exec_single_context
    self.dialect.do_execute(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../sqlalchemy/engine/default.py:942: in do_execute
    cursor.execute(statement, parameters)
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../dialects/sqlite/aiosqlite.py:150: in execute
    self._adapt_connection._handle_exception(error)
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../dialects/sqlite/aiosqlite.py:301: in _handle_exception
    raise error
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../dialects/sqlite/aiosqlite.py:132: in execute
    self.await_(_cursor.execute(operation, parameters))
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../sqlalchemy/util/_concurrency_py3k.py:132: in await_only
    return current.parent.switch(awaitable)  # type: ignore[no-any-return,attr-defined] # noqa: E501
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12.../sqlalchemy/util/_concurrency_py3k.py:196: in greenlet_spawn
    value = await result
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12............/site-packages/aiosqlite/cursor.py:48: in execute
    await self._execute(self._cursor.execute, sql, parameters)
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12............/site-packages/aiosqlite/cursor.py:40: in _execute
    return await self._conn._execute(fn, *args, **kwargs)
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12............/site-packages/aiosqlite/core.py:132: in _execute
    return await future
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Connection(Thread-14016, stopped daemon 139986338838208)>

    def run(self) -> None:
        """
        Execute function calls on a separate thread.
    
        :meta private:
        """
        while True:
            # Continues running until all queue items are processed,
            # even after connection is closed (so we can finalize all
            # futures)
    
            tx_item = self._tx.get()
            if tx_item is _STOP_RUNNING_SENTINEL:
                break
    
            future, function = tx_item
    
            try:
                LOG.debug("executing %s", function)
>               result = function()
E               sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) disk I/O error
E               [SQL: SELECT * FROM sqlite_master WHERE type='table' AND name='dbversions']
E               (Background on this error at: https://sqlalche..../e/20/e3q8)

../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.12/lib/python3.12............/site-packages/aiosqlite/core.py:115: OperationalError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@callebtc callebtc closed this by deleting the head repository Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant