Mint: Fakewallet support for USD (#488)

* fakewallet usd wip

* FakeWallet support for USD

* fix api return for receive

* use MINT_BACKEND_BOLT11_SAT everywhere
This commit is contained in:
callebtc
2024-03-22 12:11:40 +01:00
committed by GitHub
parent f4621345f3
commit 3ba1e81fcb
21 changed files with 168 additions and 93 deletions

View File

@@ -55,9 +55,9 @@ MINT_DERIVATION_PATH="m/0'/0'/0'"
MINT_DATABASE=data/mint MINT_DATABASE=data/mint
# Lightning # Funding source backends
# Supported: FakeWallet, LndRestWallet, CoreLightningRestWallet, BlinkWallet, LNbitsWallet, StrikeUSDWallet # Supported: FakeWallet, LndRestWallet, CoreLightningRestWallet, BlinkWallet, LNbitsWallet, StrikeUSDWallet
MINT_LIGHTNING_BACKEND=FakeWallet MINT_BACKEND_BOLT11_SAT=FakeWallet
# for use with LNbitsWallet # for use with LNbitsWallet
MINT_LNBITS_ENDPOINT=https://legend.lnbits.com MINT_LNBITS_ENDPOINT=https://legend.lnbits.com

View File

@@ -49,7 +49,7 @@ jobs:
poetry-version: ${{ inputs.poetry-version }} poetry-version: ${{ inputs.poetry-version }}
- name: Run tests - name: Run tests
env: env:
MINT_LIGHTNING_BACKEND: FakeWallet MINT_BACKEND_BOLT11_SAT: FakeWallet
WALLET_NAME: test_wallet WALLET_NAME: test_wallet
MINT_HOST: localhost MINT_HOST: localhost
MINT_PORT: 3337 MINT_PORT: 3337

View File

@@ -30,7 +30,7 @@ test:
test-lndrest: test-lndrest:
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
DEBUG=true \ DEBUG=true \
MINT_LIGHTNING_BACKEND=LndRestWallet \ MINT_BACKEND_BOLT11_SAT=LndRestWallet \
MINT_LND_REST_ENDPOINT=https://localhost:8081/ \ MINT_LND_REST_ENDPOINT=https://localhost:8081/ \
MINT_LND_REST_CERT=../cashu-regtest-enviroment/data/lnd-3/tls.cert \ MINT_LND_REST_CERT=../cashu-regtest-enviroment/data/lnd-3/tls.cert \
MINT_LND_REST_MACAROON=../cashu-regtest-enviroment/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon \ MINT_LND_REST_MACAROON=../cashu-regtest-enviroment/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon \

View File

@@ -7,7 +7,7 @@
*Disclaimer: The author is NOT a cryptographer and this work has not been reviewed. This means that there is very likely a fatal flaw somewhere. Cashu is still experimental and not production-ready.* *Disclaimer: The author is NOT a cryptographer and this work has not been reviewed. This means that there is very likely a fatal flaw somewhere. Cashu is still experimental and not production-ready.*
Cashu is an Ecash implementation based on David Wagner's variant of Chaumian blinding ([protocol specs](https://github.com/cashubtc/nuts)). Token logic based on [minicash](https://github.com/phyro/minicash) ([description](https://gist.github.com/phyro/935badc682057f418842c72961cf096c)) which implements a [Blind Diffie-Hellman Key Exchange](https://cypherpunks.venona.com/date/1996/03/msg01848.html) scheme written down [here](https://gist.github.com/RubenSomsen/be7a4760dd4596d06963d67baf140406). The database mechanics in Cashu Nutshell and the Lightning backend uses parts from [LNbits](https://github.com/lnbits/lnbits-legend). Cashu is an Ecash implementation based on David Wagner's variant of Chaumian blinding ([protocol specs](https://github.com/cashubtc/nuts)). Token logic based on [minicash](https://github.com/phyro/minicash) ([description](https://gist.github.com/phyro/935badc682057f418842c72961cf096c)) which implements a [Blind Diffie-Hellman Key Exchange](https://cypherpunks.venona.com/date/1996/03/msg01848.html) scheme written down [here](https://gist.github.com/RubenSomsen/be7a4760dd4596d06963d67baf140406).
<p align="center"> <p align="center">
<a href="#the-cashu-protocol">Cashu protocol</a> · <a href="#the-cashu-protocol">Cashu protocol</a> ·
@@ -169,12 +169,19 @@ You can find the API docs at [http://localhost:4448/docs](http://localhost:4448/
# Running a mint # 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. 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. ## Docker
```
docker run -d -p 3338:3338 --name nutshell -e MINT_BACKEND_BOLT11_SAT=FakeWallet -e MINT_LISTEN_HOST=0.0.0.0 -e MINT_LISTEN_PORT=3338 -e MINT_PRIVATE_KEY=TEST_PRIVATE_KEY cashubtc/nutshell:0.15.2 poetry run mint
```
## From this repository
Before you can run your own mint, make sure to enable a Lightning backend in `MINT_BACKEND_BOLT11_SAT` and set `MINT_PRIVATE_KEY` in your `.env` file.
```bash ```bash
poetry run mint poetry run mint
``` ```
For testing, you can use Nutshell without a Lightning backend by setting `MINT_LIGHTNING_BACKEND=FakeWallet` in the `.env` file. For testing, you can use Nutshell without a Lightning backend by setting `MINT_BACKEND_BOLT11_SAT=FakeWallet` in the `.env` file.
# Running tests # Running tests
@@ -185,7 +192,7 @@ poetry install --with dev
Then, make sure to set up your mint's `.env` file to use a fake Lightning backend and disable Tor: Then, make sure to set up your mint's `.env` file to use a fake Lightning backend and disable Tor:
```bash ```bash
MINT_LIGHTNING_BACKEND=FakeWallet MINT_BACKEND_BOLT11_SAT=FakeWallet
TOR=FALSE TOR=FALSE
``` ```
You can run the tests with You can run the tests with

View File

@@ -55,7 +55,11 @@ class MintSettings(CashuSettings):
mint_derivation_path_list: List[str] = Field(default=[]) mint_derivation_path_list: List[str] = Field(default=[])
mint_listen_host: str = Field(default="127.0.0.1") mint_listen_host: str = Field(default="127.0.0.1")
mint_listen_port: int = Field(default=3338) mint_listen_port: int = Field(default=3338)
mint_lightning_backend: str = Field(default="LNbitsWallet")
mint_lightning_backend: str = Field(default="") # deprecated
mint_backend_bolt11_sat: str = Field(default="")
mint_backend_bolt11_usd: str = Field(default="")
mint_database: str = Field(default="data/mint") mint_database: str = Field(default="data/mint")
mint_test_database: str = Field(default="test_data/test_mint") mint_test_database: str = Field(default="test_data/test_mint")
mint_peg_out_only: bool = Field( mint_peg_out_only: bool = Field(
@@ -204,5 +208,9 @@ def startup_settings_tasks():
if settings.socks_host and settings.socks_port: if settings.socks_host and settings.socks_port:
settings.socks_proxy = f"socks5://{settings.socks_host}:{settings.socks_port}" settings.socks_proxy = f"socks5://{settings.socks_host}:{settings.socks_port}"
# backwards compatibility: set mint_backend_bolt11_sat from mint_lightning_backend
if settings.mint_lightning_backend:
settings.mint_backend_bolt11_sat = settings.mint_lightning_backend
startup_settings_tasks() startup_settings_tasks()

View File

@@ -7,5 +7,5 @@ from .lnbits import LNbitsWallet # noqa: F401
from .lndrest import LndRestWallet # noqa: F401 from .lndrest import LndRestWallet # noqa: F401
from .strike import StrikeUSDWallet # noqa: F401 from .strike import StrikeUSDWallet # noqa: F401
if settings.mint_lightning_backend is None: if settings.mint_backend_bolt11_sat is None or settings.mint_backend_bolt11_usd is None:
raise Exception("MINT_LIGHTNING_BACKEND not configured") raise Exception("MINT_BACKEND_BOLT11_SAT or MINT_BACKEND_BOLT11_USD not set")

View File

@@ -62,12 +62,17 @@ class PaymentStatus(BaseModel):
class LightningBackend(ABC): class LightningBackend(ABC):
units: set[Unit] supported_units: set[Unit]
unit: Unit
def assert_unit_supported(self, unit: Unit): def assert_unit_supported(self, unit: Unit):
if unit not in self.units: if unit not in self.supported_units:
raise Unsupported(f"Unit {unit} is not supported") raise Unsupported(f"Unit {unit} is not supported")
@abstractmethod
def __init__(self, unit: Unit, **kwargs):
pass
@abstractmethod @abstractmethod
def status(self) -> Coroutine[None, None, StatusResponse]: def status(self) -> Coroutine[None, None, StatusResponse]:
pass pass

View File

@@ -36,7 +36,6 @@ class BlinkWallet(LightningBackend):
Create API Key at: https://dashboard.blink.sv/ Create API Key at: https://dashboard.blink.sv/
""" """
units = set([Unit.sat, Unit.usd])
wallet_ids: Dict[Unit, str] = {} wallet_ids: Dict[Unit, str] = {}
endpoint = "https://api.blink.sv/graphql" endpoint = "https://api.blink.sv/graphql"
invoice_statuses = {"PENDING": None, "PAID": True, "EXPIRED": False} invoice_statuses = {"PENDING": None, "PAID": True, "EXPIRED": False}
@@ -47,7 +46,12 @@ class BlinkWallet(LightningBackend):
} }
payment_statuses = {"SUCCESS": True, "PENDING": None, "FAILURE": False} payment_statuses = {"SUCCESS": True, "PENDING": None, "FAILURE": False}
def __init__(self): supported_units = set([Unit.sat, Unit.msat])
unit = Unit.sat
def __init__(self, unit: Unit = Unit.sat, **kwargs):
self.assert_unit_supported(unit)
self.unit = unit
assert settings.mint_blink_key, "MINT_BLINK_KEY not set" assert settings.mint_blink_key, "MINT_BLINK_KEY not set"
self.client = httpx.AsyncClient( self.client = httpx.AsyncClient(
verify=not settings.debug, verify=not settings.debug,

View File

@@ -26,9 +26,12 @@ from .macaroon import load_macaroon
class CoreLightningRestWallet(LightningBackend): class CoreLightningRestWallet(LightningBackend):
units = set([Unit.sat, Unit.msat]) supported_units = set([Unit.sat, Unit.msat])
unit = Unit.sat
def __init__(self): def __init__(self, unit: Unit = Unit.sat, **kwargs):
self.assert_unit_supported(unit)
self.unit = unit
macaroon = settings.mint_corelightning_rest_macaroon macaroon = settings.mint_corelightning_rest_macaroon
assert macaroon, "missing cln-rest macaroon" assert macaroon, "missing cln-rest macaroon"

View File

@@ -1,5 +1,6 @@
import asyncio import asyncio
import hashlib import hashlib
import math
import random import random
from datetime import datetime from datetime import datetime
from os import urandom from os import urandom
@@ -28,7 +29,7 @@ from .base import (
class FakeWallet(LightningBackend): class FakeWallet(LightningBackend):
units = set([Unit.sat, Unit.msat]) fake_btc_price = 1e8 / 1337
queue: asyncio.Queue[Bolt11] = asyncio.Queue(0) queue: asyncio.Queue[Bolt11] = asyncio.Queue(0)
payment_secrets: Dict[str, str] = dict() payment_secrets: Dict[str, str] = dict()
paid_invoices: Set[str] = set() paid_invoices: Set[str] = set()
@@ -41,6 +42,13 @@ class FakeWallet(LightningBackend):
32, 32,
).hex() ).hex()
supported_units = set([Unit.sat, Unit.msat, Unit.usd])
unit = Unit.sat
def __init__(self, unit: Unit = Unit.sat, **kwargs):
self.assert_unit_supported(unit)
self.unit = unit
async def status(self) -> StatusResponse: async def status(self) -> StatusResponse:
return StatusResponse(error_message=None, balance=1337) return StatusResponse(error_message=None, balance=1337)
@@ -80,9 +88,19 @@ class FakeWallet(LightningBackend):
self.payment_secrets[payment_hash] = secret self.payment_secrets[payment_hash] = secret
amount_msat = 0
if self.unit == Unit.sat:
amount_msat = MilliSatoshi(amount.to(Unit.msat, round="up").amount)
elif self.unit == Unit.usd:
amount_msat = MilliSatoshi(
math.ceil(amount.amount / self.fake_btc_price * 1e9)
)
else:
raise NotImplementedError()
bolt11 = Bolt11( bolt11 = Bolt11(
currency="bc", currency="bc",
amount_msat=MilliSatoshi(amount.to(Unit.msat, round="up").amount), amount_msat=amount_msat,
date=int(datetime.now().timestamp()), date=int(datetime.now().timestamp()),
tags=tags, tags=tags,
) )
@@ -137,10 +155,19 @@ class FakeWallet(LightningBackend):
async def get_payment_quote(self, bolt11: str) -> PaymentQuoteResponse: async def get_payment_quote(self, bolt11: str) -> PaymentQuoteResponse:
invoice_obj = decode(bolt11) invoice_obj = decode(bolt11)
assert invoice_obj.amount_msat, "invoice has no amount." assert invoice_obj.amount_msat, "invoice has no amount."
if self.unit == Unit.sat:
amount_msat = int(invoice_obj.amount_msat) amount_msat = int(invoice_obj.amount_msat)
fees_msat = fee_reserve(amount_msat) fees_msat = fee_reserve(amount_msat)
fees = Amount(unit=Unit.msat, amount=fees_msat) fees = Amount(unit=Unit.msat, amount=fees_msat)
amount = Amount(unit=Unit.msat, amount=amount_msat) amount = Amount(unit=Unit.msat, amount=amount_msat)
elif self.unit == Unit.usd:
amount_usd = math.ceil(invoice_obj.amount_msat / 1e9 * self.fake_btc_price)
amount = Amount(unit=Unit.usd, amount=amount_usd)
fees = Amount(unit=Unit.usd, amount=1)
else:
raise NotImplementedError()
return PaymentQuoteResponse( return PaymentQuoteResponse(
checking_id=invoice_obj.payment_hash, fee=fees, amount=amount checking_id=invoice_obj.payment_hash, fee=fees, amount=amount
) )

View File

@@ -22,9 +22,12 @@ from .base import (
class LNbitsWallet(LightningBackend): class LNbitsWallet(LightningBackend):
"""https://github.com/lnbits/lnbits""" """https://github.com/lnbits/lnbits"""
units = set([Unit.sat]) supported_units = set([Unit.sat])
unit = Unit.sat
def __init__(self): def __init__(self, unit: Unit = Unit.sat, **kwargs):
self.assert_unit_supported(unit)
self.unit = unit
self.endpoint = settings.mint_lnbits_endpoint self.endpoint = settings.mint_lnbits_endpoint
self.client = httpx.AsyncClient( self.client = httpx.AsyncClient(
verify=not settings.debug, verify=not settings.debug,

View File

@@ -27,9 +27,12 @@ from .macaroon import load_macaroon
class LndRestWallet(LightningBackend): class LndRestWallet(LightningBackend):
"""https://api.lightning.community/rest/index.html#lnd-rest-api-reference""" """https://api.lightning.community/rest/index.html#lnd-rest-api-reference"""
units = set([Unit.sat, Unit.msat]) supported_units = set([Unit.sat, Unit.msat])
unit = Unit.sat
def __init__(self): def __init__(self, unit: Unit = Unit.sat, **kwargs):
self.assert_unit_supported(unit)
self.unit = unit
endpoint = settings.mint_lnd_rest_endpoint endpoint = settings.mint_lnd_rest_endpoint
cert = settings.mint_lnd_rest_cert cert = settings.mint_lnd_rest_cert

View File

@@ -19,9 +19,11 @@ from .base import (
class StrikeUSDWallet(LightningBackend): class StrikeUSDWallet(LightningBackend):
"""https://github.com/lnbits/lnbits""" """https://github.com/lnbits/lnbits"""
units = [Unit.usd] supported_units = [Unit.usd]
def __init__(self): def __init__(self, unit: Unit = Unit.usd, **kwargs):
self.assert_unit_supported(unit)
self.unit = unit
self.endpoint = "https://api.strike.me" self.endpoint = "https://api.strike.me"
# bearer auth with settings.mint_strike_key # bearer auth with settings.mint_strike_key

View File

@@ -1,6 +1,5 @@
import asyncio import asyncio
import copy import copy
import math
import time import time
from typing import Dict, List, Mapping, Optional, Tuple from typing import Dict, List, Mapping, Optional, Tuple
@@ -493,10 +492,10 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
if mint_quote: if mint_quote:
# internal transaction, validate and return amount from # internal transaction, validate and return amount from
# associated mint quote and demand zero fees # associated mint quote and demand zero fees
assert ( # assert (
Amount(unit, mint_quote.amount).to(Unit.msat).amount # Amount(unit, mint_quote.amount).to(Unit.msat).amount
== invoice_obj.amount_msat # == invoice_obj.amount_msat
), "amounts do not match" # ), "amounts do not match"
assert request == mint_quote.request, "bolt11 requests do not match" assert request == mint_quote.request, "bolt11 requests do not match"
assert mint_quote.unit == melt_quote.unit, "units do not match" assert mint_quote.unit == melt_quote.unit, "units do not match"
assert mint_quote.method == method.name, "methods do not match" assert mint_quote.method == method.name, "methods do not match"
@@ -506,7 +505,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
payment_quote = PaymentQuoteResponse( payment_quote = PaymentQuoteResponse(
checking_id=mint_quote.checking_id, checking_id=mint_quote.checking_id,
amount=Amount(unit, mint_quote.amount), amount=Amount(unit, mint_quote.amount),
fee=Amount(unit=Unit.msat, amount=0), fee=Amount(unit, amount=0),
) )
logger.info( logger.info(
f"Issuing internal melt quote: {request} ->" f"Issuing internal melt quote: {request} ->"
@@ -622,11 +621,12 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
bolt11_request = melt_quote.request bolt11_request = melt_quote.request
invoice_obj = bolt11.decode(bolt11_request) invoice_obj = bolt11.decode(bolt11_request)
assert invoice_obj.amount_msat, "invoice has no amount." assert invoice_obj.amount_msat, "invoice has no amount."
invoice_amount_sat = math.ceil(invoice_obj.amount_msat / 1000) # invoice_amount_sat = math.ceil(invoice_obj.amount_msat / 1000)
assert ( # assert (
Amount(Unit[melt_quote.unit], mint_quote.amount).to(Unit.sat).amount # Amount(Unit[melt_quote.unit], mint_quote.amount).to(Unit.sat).amount
== invoice_amount_sat # == invoice_amount_sat
), "amounts do not match" # ), "amounts do not match"
assert mint_quote.amount == melt_quote.amount, "amounts do not match"
assert bolt11_request == mint_quote.request, "bolt11 requests do not match" assert bolt11_request == mint_quote.request, "bolt11 requests do not match"
assert mint_quote.unit == melt_quote.unit, "units do not match" assert mint_quote.unit == melt_quote.unit, "units do not match"
assert mint_quote.method == melt_quote.method, "methods do not match" assert mint_quote.method == melt_quote.method, "methods do not match"

View File

@@ -3,6 +3,7 @@
import asyncio import asyncio
import importlib import importlib
from typing import Dict
from loguru import logger from loguru import logger
@@ -10,6 +11,7 @@ from ..core.base import Method, Unit
from ..core.db import Database from ..core.db import Database
from ..core.migrations import migrate_databases from ..core.migrations import migrate_databases
from ..core.settings import settings from ..core.settings import settings
from ..lightning.base import LightningBackend
from ..mint import migrations from ..mint import migrations
from ..mint.crud import LedgerCrudSqlite from ..mint.crud import LedgerCrudSqlite
from ..mint.ledger import Ledger from ..mint.ledger import Ledger
@@ -31,23 +33,24 @@ for key, value in settings.dict().items():
logger.debug(f"{key}: {value}") logger.debug(f"{key}: {value}")
wallets_module = importlib.import_module("cashu.lightning") wallets_module = importlib.import_module("cashu.lightning")
lightning_backend = getattr(wallets_module, settings.mint_lightning_backend)()
assert settings.mint_private_key is not None, "No mint private key set." backends: Dict[Method, Dict[Unit, LightningBackend]] = {}
if settings.mint_backend_bolt11_sat:
backend_bolt11_sat = getattr(wallets_module, settings.mint_backend_bolt11_sat)(
unit=Unit.sat
)
backends.setdefault(Method.bolt11, {})[Unit.sat] = backend_bolt11_sat
if settings.mint_backend_bolt11_usd:
backend_bolt11_usd = getattr(wallets_module, settings.mint_backend_bolt11_usd)(
unit=Unit.usd
)
backends.setdefault(Method.bolt11, {})[Unit.usd] = backend_bolt11_usd
if not backends:
raise Exception("No backends are set.")
if not settings.mint_private_key:
raise Exception("No mint private key is set.")
# strike_backend = getattr(wallets_module, "StrikeUSDWallet")()
# backends = {
# Method.bolt11: {Unit.sat: lightning_backend, Unit.usd: strike_backend},
# }
# backends = {
# Method.bolt11: {Unit.sat: lightning_backend, Unit.msat: lightning_backend},
# }
# backends = {
# Method.bolt11: {Unit.sat: lightning_backend, Unit.msat: lightning_backend,
# }
backends = {
Method.bolt11: {Unit.sat: lightning_backend},
}
ledger = Ledger( ledger = Ledger(
db=Database("mint", settings.mint_database), db=Database("mint", settings.mint_database),
seed=settings.mint_private_key, seed=settings.mint_private_key,

View File

@@ -265,10 +265,9 @@ async def receive_command(
if token: if token:
tokenObj: TokenV3 = deserialize_token_from_string(token) tokenObj: TokenV3 = deserialize_token_from_string(token)
await verify_mints(wallet, tokenObj) await verify_mints(wallet, tokenObj)
balance = await receive(wallet, tokenObj) await receive(wallet, tokenObj)
elif nostr: elif nostr:
await receive_nostr(wallet) await receive_nostr(wallet)
balance = wallet.available_balance
elif all: elif all:
reserved_proofs = await get_reserved_proofs(wallet.db) reserved_proofs = await get_reserved_proofs(wallet.db)
balance = None balance = None
@@ -278,10 +277,10 @@ async def receive_command(
token = await wallet.serialize_proofs(proofs) token = await wallet.serialize_proofs(proofs)
tokenObj = deserialize_token_from_string(token) tokenObj = deserialize_token_from_string(token)
await verify_mints(wallet, tokenObj) await verify_mints(wallet, tokenObj)
balance = await receive(wallet, tokenObj) await receive(wallet, tokenObj)
else: else:
raise Exception("enter token or use either flag --nostr or --all.") raise Exception("enter token or use either flag --nostr or --all.")
assert balance balance = wallet.available_balance
return ReceiveResponse(initial_balance=initial_balance, balance=balance) return ReceiveResponse(initial_balance=initial_balance, balance=balance)
@@ -359,7 +358,8 @@ async def pending(
reserved_date = datetime.utcfromtimestamp( reserved_date = datetime.utcfromtimestamp(
int(grouped_proofs[0].time_reserved) # type: ignore int(grouped_proofs[0].time_reserved) # type: ignore
).strftime("%Y-%m-%d %H:%M:%S") ).strftime("%Y-%m-%d %H:%M:%S")
result.update({ result.update(
{
f"{i}": { f"{i}": {
"amount": sum_proofs(grouped_proofs), "amount": sum_proofs(grouped_proofs),
"time": reserved_date, "time": reserved_date,
@@ -367,7 +367,8 @@ async def pending(
"token": token, "token": token,
"mint": mint, "mint": mint,
} }
}) }
)
return PendingResponse(pending_token=result) return PendingResponse(pending_token=result)
@@ -412,14 +413,16 @@ async def wallets():
if w == wallet.name: if w == wallet.name:
active_wallet = True active_wallet = True
if active_wallet: if active_wallet:
result.update({ result.update(
{
f"{w}": { f"{w}": {
"balance": sum_proofs(wallet.proofs), "balance": sum_proofs(wallet.proofs),
"available": sum_proofs([ "available": sum_proofs(
p for p in wallet.proofs if not p.reserved [p for p in wallet.proofs if not p.reserved]
]), ),
} }
}) }
)
except Exception: except Exception:
pass pass
return WalletsResponse(wallets=result) return WalletsResponse(wallets=result)

View File

@@ -184,7 +184,7 @@ async def cli(ctx: Context, host: str, walletname: str, unit: str, tests: bool):
async def pay(ctx: Context, invoice: str, yes: bool): async def pay(ctx: Context, invoice: str, yes: bool):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
await wallet.load_mint() await wallet.load_mint()
print_balance(ctx) await print_balance(ctx)
quote = await wallet.get_pay_amount_with_fees(invoice) quote = await wallet.get_pay_amount_with_fees(invoice)
logger.debug(f"Quote: {quote}") logger.debug(f"Quote: {quote}")
total_amount = quote.amount + quote.fee_reserve total_amount = quote.amount + quote.fee_reserve
@@ -219,7 +219,7 @@ async def pay(ctx: Context, invoice: str, yes: bool):
print(f" (Preimage: {melt_response.payment_preimage}).") print(f" (Preimage: {melt_response.payment_preimage}).")
else: else:
print(".") print(".")
print_balance(ctx) await print_balance(ctx)
@cli.command("invoice", help="Create Lighting invoice.") @cli.command("invoice", help="Create Lighting invoice.")
@@ -242,11 +242,12 @@ async def pay(ctx: Context, invoice: str, yes: bool):
) )
@click.pass_context @click.pass_context
@coro @coro
async def invoice(ctx: Context, amount: int, id: str, split: int, no_check: bool): async def invoice(ctx: Context, amount: float, id: str, split: int, no_check: bool):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
await wallet.load_mint() await wallet.load_mint()
print_balance(ctx) await print_balance(ctx)
amount = int(amount * 100) if wallet.unit == Unit.usd else int(amount) amount = int(amount * 100) if wallet.unit == Unit.usd else int(amount)
print(f"Requesting invoice for {wallet.unit.str(amount)} {wallet.unit}.")
# in case the user wants a specific split, we create a list of amounts # in case the user wants a specific split, we create a list of amounts
optional_split = None optional_split = None
if split: if split:
@@ -305,7 +306,7 @@ async def invoice(ctx: Context, amount: int, id: str, split: int, no_check: bool
elif amount and id: elif amount and id:
await wallet.mint(amount, split=optional_split, id=id) await wallet.mint(amount, split=optional_split, id=id)
print("") print("")
print_balance(ctx) await print_balance(ctx)
return return
@@ -474,7 +475,7 @@ async def send_command(
await send_nostr( await send_nostr(
wallet, amount=amount, pubkey=nostr or nopt, verbose=verbose, yes=yes wallet, amount=amount, pubkey=nostr or nopt, verbose=verbose, yes=yes
) )
print_balance(ctx) await print_balance(ctx)
@cli.command("receive", help="Receive tokens.") @cli.command("receive", help="Receive tokens.")
@@ -508,8 +509,9 @@ async def receive_cli(
mint_url, os.path.join(settings.cashu_dir, wallet.name) mint_url, os.path.join(settings.cashu_dir, wallet.name)
) )
await verify_mint(mint_wallet, mint_url) await verify_mint(mint_wallet, mint_url)
receive_wallet = await receive(wallet, tokenObj)
ctx.obj["WALLET"] = receive_wallet
await receive(wallet, tokenObj)
elif nostr: elif nostr:
await receive_nostr(wallet) await receive_nostr(wallet)
# exit on keypress # exit on keypress
@@ -530,11 +532,12 @@ async def receive_cli(
mint_url, os.path.join(settings.cashu_dir, wallet.name) mint_url, os.path.join(settings.cashu_dir, wallet.name)
) )
await verify_mint(mint_wallet, mint_url) await verify_mint(mint_wallet, mint_url)
await receive(wallet, tokenObj) receive_wallet = await receive(wallet, tokenObj)
ctx.obj["WALLET"] = receive_wallet
else: else:
print("Error: enter token or use either flag --nostr or --all.") print("Error: enter token or use either flag --nostr or --all.")
return return
print_balance(ctx) await print_balance(ctx)
@cli.command("burn", help="Burn spent tokens.") @cli.command("burn", help="Burn spent tokens.")
@@ -586,7 +589,7 @@ async def burn(ctx: Context, token: str, all: bool, force: bool, delete: str):
for i in range(0, len(proofs), settings.proofs_batch_size) for i in range(0, len(proofs), settings.proofs_batch_size)
]: ]:
await wallet.invalidate(_proofs, check_spendable=True) await wallet.invalidate(_proofs, check_spendable=True)
print_balance(ctx) await print_balance(ctx)
@cli.command("pending", help="Show pending tokens.") @cli.command("pending", help="Show pending tokens.")
@@ -865,7 +868,7 @@ async def restore(ctx: Context, to: int, batch: int):
await wallet.restore_wallet_from_mnemonic(mnemonic, to=to, batch=batch) await wallet.restore_wallet_from_mnemonic(mnemonic, to=to, batch=batch)
await wallet.load_proofs() await wallet.load_proofs()
print_balance(ctx) await print_balance(ctx)
@cli.command("selfpay", help="Refresh tokens.") @cli.command("selfpay", help="Refresh tokens.")

View File

@@ -10,8 +10,9 @@ from ...wallet.crud import get_keysets
from ...wallet.wallet import Wallet as Wallet from ...wallet.wallet import Wallet as Wallet
def print_balance(ctx: Context): async def print_balance(ctx: Context):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
await wallet.load_proofs(reload=True, unit=wallet.unit)
print(f"Balance: {wallet.unit.str(wallet.available_balance)}") print(f"Balance: {wallet.unit.str(wallet.available_balance)}")

View File

@@ -35,7 +35,7 @@ async def list_mints(wallet: Wallet):
return mints return mints
async def redeem_TokenV3_multimint(wallet: Wallet, token: TokenV3): async def redeem_TokenV3_multimint(wallet: Wallet, token: TokenV3) -> Wallet:
""" """
Helper function to iterate thruogh a token with multiple mints and redeem them from Helper function to iterate thruogh a token with multiple mints and redeem them from
these mints one keyset at a time. these mints one keyset at a time.
@@ -58,6 +58,9 @@ async def redeem_TokenV3_multimint(wallet: Wallet, token: TokenV3):
_, _ = await mint_wallet.redeem(redeem_proofs) _, _ = await mint_wallet.redeem(redeem_proofs)
print(f"Received {mint_wallet.unit.str(sum_proofs(redeem_proofs))}") print(f"Received {mint_wallet.unit.str(sum_proofs(redeem_proofs))}")
# return the last mint_wallet
return mint_wallet
def serialize_TokenV2_to_TokenV3(tokenv2: TokenV2): def serialize_TokenV2_to_TokenV3(tokenv2: TokenV2):
"""Helper function to receive legacy TokenV2 tokens. """Helper function to receive legacy TokenV2 tokens.
@@ -120,7 +123,7 @@ def deserialize_token_from_string(token: str) -> TokenV3:
async def receive( async def receive(
wallet: Wallet, wallet: Wallet,
tokenObj: TokenV3, tokenObj: TokenV3,
): ) -> Wallet:
logger.debug(f"receive: {tokenObj}") logger.debug(f"receive: {tokenObj}")
proofs = [p for t in tokenObj.token for p in t.proofs] proofs = [p for t in tokenObj.token for p in t.proofs]
@@ -128,7 +131,7 @@ async def receive(
if includes_mint_info: if includes_mint_info:
# redeem tokens with new wallet instances # redeem tokens with new wallet instances
await redeem_TokenV3_multimint( mint_wallet = await redeem_TokenV3_multimint(
wallet, wallet,
tokenObj, tokenObj,
) )
@@ -154,7 +157,7 @@ async def receive(
# reload main wallet so the balance updates # reload main wallet so the balance updates
await wallet.load_proofs(reload=True) await wallet.load_proofs(reload=True)
return wallet.available_balance return mint_wallet
async def send( async def send(

View File

@@ -8,7 +8,7 @@ services:
ports: ports:
- "3338:3338" - "3338:3338"
environment: environment:
- MINT_LIGHTNING_BACKEND=FakeWallet - MINT_BACKEND_BOLT11_SAT=FakeWallet
- MINT_LISTEN_HOST=0.0.0.0 - MINT_LISTEN_HOST=0.0.0.0
- MINT_LISTEN_PORT=3338 - MINT_LISTEN_PORT=3338
- MINT_PRIVATE_KEY=TEST_PRIVATE_KEY - MINT_PRIVATE_KEY=TEST_PRIVATE_KEY

View File

@@ -25,7 +25,7 @@ async def get_random_invoice_data():
wallets_module = importlib.import_module("cashu.lightning") wallets_module = importlib.import_module("cashu.lightning")
wallet_class = getattr(wallets_module, settings.mint_lightning_backend) wallet_class = getattr(wallets_module, settings.mint_backend_bolt11_sat)
WALLET = wallet_class() WALLET = wallet_class()
is_fake: bool = WALLET.__class__.__name__ == "FakeWallet" is_fake: bool = WALLET.__class__.__name__ == "FakeWallet"
is_regtest: bool = not is_fake is_regtest: bool = not is_fake