Skip to content

Commit

Permalink
Merge branch 'main' into cleanup_wishlist
Browse files Browse the repository at this point in the history
  • Loading branch information
callebtc committed Nov 2, 2023
2 parents 57cb16e + a4abbc2 commit d379192
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 66 deletions.
19 changes: 18 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,14 +42,15 @@ jobs:
cache: "poetry"
- name: Install dependencies
run: |
poetry install
poetry install --extras pgsql
shell: bash
- name: Run tests
env:
LIGHTNING: false
WALLET_NAME: test_wallet
MINT_HOST: localhost
MINT_PORT: 3337
MINT_DATABASE: ${{ inputs.db-url }}
TOR: false
run: |
make test
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions cashu/lightning/fake.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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()

Expand Down
97 changes: 45 additions & 52 deletions cashu/lightning/lnbits.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# type: ignore
from typing import Dict, Optional
from typing import Optional

import httpx

Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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"],
)
7 changes: 6 additions & 1 deletion cashu/mint/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,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.
Expand Down Expand Up @@ -403,6 +407,7 @@ async def melt(
invoice_amount=invoice_amount,
ln_fee_msat=payment.fee_msat,
outputs=outputs,
keyset=keyset,
)

except Exception as e:
Expand Down
2 changes: 1 addition & 1 deletion cashu/mint/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
13 changes: 9 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
9 changes: 8 additions & 1 deletion tests/test_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,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(
Expand Down

0 comments on commit d379192

Please sign in to comment.