mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 02:24:20 +01:00
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:
@@ -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
|
||||||
|
|||||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -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
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -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 \
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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."
|
||||||
amount_msat = int(invoice_obj.amount_msat)
|
|
||||||
fees_msat = fee_reserve(amount_msat)
|
if self.unit == Unit.sat:
|
||||||
fees = Amount(unit=Unit.msat, amount=fees_msat)
|
amount_msat = int(invoice_obj.amount_msat)
|
||||||
amount = Amount(unit=Unit.msat, amount=amount_msat)
|
fees_msat = fee_reserve(amount_msat)
|
||||||
|
fees = Amount(unit=Unit.msat, amount=fees_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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,15 +358,17 @@ 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}": {
|
{
|
||||||
"amount": sum_proofs(grouped_proofs),
|
f"{i}": {
|
||||||
"time": reserved_date,
|
"amount": sum_proofs(grouped_proofs),
|
||||||
"ID": key,
|
"time": reserved_date,
|
||||||
"token": token,
|
"ID": key,
|
||||||
"mint": mint,
|
"token": token,
|
||||||
|
"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}": {
|
{
|
||||||
"balance": sum_proofs(wallet.proofs),
|
f"{w}": {
|
||||||
"available": sum_proofs([
|
"balance": sum_proofs(wallet.proofs),
|
||||||
p for p in wallet.proofs if not p.reserved
|
"available": sum_proofs(
|
||||||
]),
|
[p for p in wallet.proofs if not p.reserved]
|
||||||
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return WalletsResponse(wallets=result)
|
return WalletsResponse(wallets=result)
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
|||||||
@@ -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)}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user