From 22e0ad4b579de3b5b83fbb07968cbd42792315a3 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 29 Oct 2023 08:57:25 -0300 Subject: [PATCH 1/3] use specific keyset for returned change (#351) --- cashu/mint/ledger.py | 7 ++++++- tests/test_wallet.py | 9 ++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index ed1a0329..371da7dd 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -315,7 +315,11 @@ async def mint( return promises async def melt( - self, proofs: List[Proof], invoice: str, outputs: Optional[List[BlindedMessage]] + self, + proofs: List[Proof], + invoice: str, + outputs: Optional[List[BlindedMessage]], + keyset: Optional[MintKeyset] = None, ) -> Tuple[bool, str, List[BlindedSignature]]: """Invalidates proofs and pays a Lightning invoice. @@ -386,6 +390,7 @@ async def melt( invoice_amount=invoice_amount, ln_fee_msat=payment.fee_msat, outputs=outputs, + keyset=keyset, ) except Exception as e: diff --git a/tests/test_wallet.py b/tests/test_wallet.py index a869a85a..e2bab85c 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -243,10 +243,17 @@ async def test_melt(wallet1: Wallet): assert fee_reserve_sat == 2 _, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount) - await wallet1.pay_lightning( + melt_response = await wallet1.pay_lightning( send_proofs, invoice=invoice.bolt11, fee_reserve_sat=fee_reserve_sat ) + assert melt_response.change + assert len(melt_response.change) == 1 + # NOTE: we assume that we will get a token back from the same keyset as the ones we melted + # this could be wrong if we melted tokens from an old keyset but the returned ones are + # from a newer one. + assert melt_response.change[0].id == send_proofs[0].id + # verify that proofs in proofs_used db have the same melt_id as the invoice in the db assert invoice.payment_hash invoice_db = await get_lightning_invoice( From aa366516290b9fbb6859ca7d6e1be84dc0e366d7 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 1 Nov 2023 08:59:32 -0300 Subject: [PATCH 2/3] Fix LNbits backend to use proper BaseModels (#352) --- cashu/lightning/fake.py | 3 +- cashu/lightning/lnbits.py | 97 ++++++++++++++++++--------------------- 2 files changed, 46 insertions(+), 54 deletions(-) diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index ae956195..a79c1a81 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -30,7 +30,7 @@ class FakeWallet(Wallet): """https://github.com/lnbits/lnbits""" - queue: asyncio.Queue = asyncio.Queue(0) + queue: asyncio.Queue[Bolt11] = asyncio.Queue(0) paid_invoices: Set[str] = set() secret: str = "FAKEWALLET SECRET" privkey: str = hashlib.pbkdf2_hmac( @@ -52,7 +52,6 @@ async def create_invoice( unhashed_description: Optional[bytes] = None, expiry: Optional[int] = None, payment_secret: Optional[bytes] = None, - **_, ) -> InvoiceResponse: tags = Tags() diff --git a/cashu/lightning/lnbits.py b/cashu/lightning/lnbits.py index 796a3890..0f12b0b9 100644 --- a/cashu/lightning/lnbits.py +++ b/cashu/lightning/lnbits.py @@ -1,5 +1,5 @@ # type: ignore -from typing import Dict, Optional +from typing import Optional import httpx @@ -29,18 +29,25 @@ async def status(self) -> StatusResponse: r.raise_for_status() except Exception as exc: return StatusResponse( - f"Failed to connect to {self.endpoint} due to: {exc}", 0 + error_message=f"Failed to connect to {self.endpoint} due to: {exc}", + balance_msat=0, ) try: - data = r.json() + data: dict = r.json() except Exception: return StatusResponse( - f"Failed to connect to {self.endpoint}, got: '{r.text[:200]}...'", 0 + error_message=( + f"Failed to connect to {self.endpoint}, got: '{r.text[:200]}...'" + ), + balance_msat=0, ) if "detail" in data: - return StatusResponse(f"LNbits error: {data['detail']}", 0) - return StatusResponse(None, data["balance"]) + return StatusResponse( + error_message=f"LNbits error: {data['detail']}", balance_msat=0 + ) + + return StatusResponse(error_message=None, balance_msat=data["balance"]) async def create_invoice( self, @@ -49,7 +56,7 @@ async def create_invoice( description_hash: Optional[bytes] = None, unhashed_description: Optional[bytes] = None, ) -> InvoiceResponse: - data: Dict = {"out": False, "amount": amount} + data = {"out": False, "amount": amount} if description_hash: data["description_hash"] = description_hash.hex() if unhashed_description: @@ -62,18 +69,19 @@ async def create_invoice( ) r.raise_for_status() except Exception: - return InvoiceResponse(False, None, None, r.json()["detail"]) - ok, checking_id, payment_request, error_message = ( - True, - None, - None, - None, - ) + return InvoiceResponse( + ok=False, + error_message=r.json()["detail"], + ) data = r.json() checking_id, payment_request = data["checking_id"], data["payment_request"] - return InvoiceResponse(ok, checking_id, payment_request, error_message) + return InvoiceResponse( + ok=True, + checking_id=checking_id, + payment_request=payment_request, + ) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: try: @@ -84,20 +92,24 @@ async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse ) r.raise_for_status() except Exception: - error_message = r.json()["detail"] - return PaymentResponse(None, None, None, None, error_message) + return PaymentResponse(error_message=r.json()["detail"]) if r.status_code > 299: - return PaymentResponse(None, None, None, None, f"HTTP status: {r.reason}") + return PaymentResponse(error_message=(f"HTTP status: {r.reason_phrase}",)) if "detail" in r.json(): - return PaymentResponse(None, None, None, None, r.json()["detail"]) + return PaymentResponse(error_message=(r.json()["detail"],)) - data = r.json() + data: dict = r.json() checking_id = data["payment_hash"] # we do this to get the fee and preimage payment: PaymentStatus = await self.get_payment_status(checking_id) - return PaymentResponse(True, checking_id, payment.fee_msat, payment.preimage) + return PaymentResponse( + ok=True, + checking_id=checking_id, + fee_msat=payment.fee_msat, + preimage=payment.preimage, + ) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: try: @@ -106,10 +118,11 @@ async def get_invoice_status(self, checking_id: str) -> PaymentStatus: ) r.raise_for_status() except Exception: - return PaymentStatus(None) - if r.json().get("detail"): - return PaymentStatus(None) - return PaymentStatus(r.json()["paid"]) + return PaymentStatus(paid=None) + data: dict = r.json() + if data.get("detail"): + return PaymentStatus(paid=None) + return PaymentStatus(paid=r.json()["paid"]) async def get_payment_status(self, checking_id: str) -> PaymentStatus: try: @@ -118,33 +131,13 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus: ) r.raise_for_status() except Exception: - return PaymentStatus(None) + return PaymentStatus(paid=None) data = r.json() if "paid" not in data and "details" not in data: - return PaymentStatus(None) + return PaymentStatus(paid=None) - return PaymentStatus(data["paid"], data["details"]["fee"], data["preimage"]) - - # async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: - # url = f"{self.endpoint}/api/v1/payments/sse" - - # while True: - # try: - # async with requests.stream("GET", url) as r: - # async for line in r.aiter_lines(): - # if line.startswith("data:"): - # try: - # data = json.loads(line[5:]) - # except json.decoder.JSONDecodeError: - # continue - - # if type(data) is not dict: - # continue - - # yield data["payment_hash"] # payment_hash - - # except: - # pass - - # print("lost connection to lnbits /payments/sse, retrying in 5 seconds") - # await asyncio.sleep(5) + return PaymentStatus( + paid=data["paid"], + fee_msat=data["details"]["fee"], + preimage=data["preimage"], + ) From a4abbc2eee1465e0bf998fc6ed2ca8f6eb7ee595 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 1 Nov 2023 20:55:52 -0300 Subject: [PATCH 3/3] Postgres migrations: remove balance view before children (#353) * remove balance view before childredn * update readme * CI with postgres * install postgres in CI * run postgres tests in CI? * disable postgres tests for now --- .github/workflows/tests.yml | 19 ++++++++++++++++++- README.md | 12 ++++++++---- cashu/mint/migrations.py | 2 +- tests/conftest.py | 13 +++++++++---- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c99460b2..986e58e7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,11 +5,27 @@ on: [push, pull_request] jobs: poetry: runs-on: ${{ matrix.os }} + services: + postgres: + image: postgres:latest + env: + POSTGRES_USER: cashu + POSTGRES_PASSWORD: cashu + POSTGRES_DB: cashu + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 strategy: matrix: os: [ubuntu-latest] python-version: ["3.10.4"] poetry-version: ["1.5.1"] + # db-url: ["", "postgres://cashu:cashu@localhost:5432/test"] # TODO: Postgres test not working + db-url: [""] steps: - name: Checkout repository and submodules uses: actions/checkout@v2 @@ -26,7 +42,7 @@ jobs: cache: "poetry" - name: Install dependencies run: | - poetry install + poetry install --extras pgsql shell: bash - name: Run tests env: @@ -34,6 +50,7 @@ jobs: WALLET_NAME: test_wallet MINT_HOST: localhost MINT_PORT: 3337 + MINT_DATABASE: ${{ inputs.db-url }} TOR: false run: | make test diff --git a/README.md b/README.md index c359a570..190c25b0 100644 --- a/README.md +++ b/README.md @@ -76,18 +76,20 @@ source ~/.bashrc #### Poetry: Install Cashu ```bash # install cashu -git clone https://github.com/callebtc/cashu.git +git clone https://github.com/cashubtc/nutshell.git cashu cd cashu pyenv local 3.10.4 poetry install ``` +If you would like to use PostgreSQL as the mint database, use the command `poetry install --extras pgsql`. + #### Poetry: Update Cashu To update Cashu to the newest version enter ```bash git pull && poetry install ``` -#### Poetry: Using Cashu +#### Poetry: Using the Nutshell wallet Cashu should be now installed. To execute the following commands, activate your virtual Poetry environment via @@ -183,11 +185,13 @@ You can find the API docs at [http://localhost:4448/docs](http://localhost:4448/ # Running a mint This command runs the mint on your local computer. Skip this step if you want to use the [public test mint](#test-instance) instead. + +Before you can run your own mint, make sure to enable a Lightning backend in `MINT_LIGHTNING_BACKEND` and set `MINT_PRIVATE_KEY` in your `.env` file. ```bash -python -m cashu.mint +poetry run mint ``` -You can turn off Lightning support and mint as many tokens as you like by setting `LIGHTNING=FALSE` in the `.env` file. +For testing, you can use Nutshell without a Lightning backend by setting `MINT_LIGHTNING_BACKEND=FakeWallet` in the `.env` file. # Running tests diff --git a/cashu/mint/migrations.py b/cashu/mint/migrations.py index c2c7a937..5b9270ba 100644 --- a/cashu/mint/migrations.py +++ b/cashu/mint/migrations.py @@ -183,9 +183,9 @@ async def m009_add_out_to_invoices(db: Database): # column in invoices for marking whether the invoice is incoming (out=False) or outgoing (out=True) async with db.connect() as conn: # we have to drop the balance views first and recreate them later + await conn.execute(f"DROP VIEW {table_with_schema(db, 'balance')}") await conn.execute(f"DROP VIEW {table_with_schema(db, 'balance_issued')}") await conn.execute(f"DROP VIEW {table_with_schema(db, 'balance_redeemed')}") - await conn.execute(f"DROP VIEW {table_with_schema(db, 'balance')}") # rename column pr to bolt11 await conn.execute( diff --git a/tests/conftest.py b/tests/conftest.py index 02751323..39914079 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -57,11 +57,16 @@ async def start_mint_init(ledger: Ledger): await ledger.load_used_proofs() await ledger.init_keysets() - db_file = "test_data/mint/test.sqlite3" - if os.path.exists(db_file): - os.remove(db_file) + database_name = "test" + + if not settings.mint_database.startswith("postgres"): + # clear sqlite database + db_file = os.path.join(settings.mint_database, database_name + ".sqlite3") + if os.path.exists(db_file): + os.remove(db_file) + ledger = Ledger( - db=Database("test", "test_data/mint"), + db=Database(database_name, settings.mint_database), seed=settings.mint_private_key, derivation_path=settings.mint_derivation_path, lightning=FakeWallet(),