Skip to content

Commit 6e46d39

Browse files
committed
fix melt and mint list
1 parent 1b18055 commit 6e46d39

File tree

2 files changed

+137
-18
lines changed

2 files changed

+137
-18
lines changed

sixty_nuts/lnurl.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,82 @@ class LNURLError(Exception):
2424
"""LNURL related errors."""
2525

2626

27+
def parse_lightning_invoice_amount(invoice: str, currency: str = "sat") -> int:
28+
"""Parse Lightning invoice (BOLT-11) to extract amount in specified currency units.
29+
30+
Args:
31+
invoice: BOLT-11 Lightning invoice string
32+
currency: Target currency unit ("sat" or "msat")
33+
34+
Returns:
35+
Amount in the specified currency unit
36+
37+
Raises:
38+
LNURLError: If invoice format is invalid or amount cannot be parsed
39+
"""
40+
invoice = invoice.lower().strip()
41+
42+
if not invoice.startswith("ln"):
43+
raise LNURLError("Invalid Lightning invoice format")
44+
45+
# Find the network part (bc, tb, etc.)
46+
network_start = 2
47+
while network_start < len(invoice) and invoice[network_start] not in "0123456789":
48+
network_start += 1
49+
50+
if network_start >= len(invoice):
51+
raise LNURLError("Invalid Lightning invoice format")
52+
53+
# Parse amount and multiplier
54+
amount_str = ""
55+
multiplier = ""
56+
i = network_start
57+
58+
# Extract numeric part
59+
while i < len(invoice) and invoice[i].isdigit():
60+
amount_str += invoice[i]
61+
i += 1
62+
63+
# Extract multiplier if present
64+
if i < len(invoice) and invoice[i] in "munp":
65+
multiplier = invoice[i]
66+
i += 1
67+
68+
# Check if we have the required "1" separator
69+
if i >= len(invoice) or invoice[i] != "1":
70+
raise LNURLError("Invalid Lightning invoice format")
71+
72+
if not amount_str:
73+
raise LNURLError("Lightning invoice amount not specified")
74+
75+
# Convert to base units
76+
try:
77+
amount = int(amount_str)
78+
except ValueError:
79+
raise LNURLError("Invalid Lightning invoice amount")
80+
81+
# Apply multiplier to get millisatoshis
82+
if multiplier == "m": # milli = 10^-3
83+
amount_msat = amount * 100_000_000 # amount is in BTC * 10^-3
84+
elif multiplier == "u": # micro = 10^-6
85+
amount_msat = amount * 100_000 # amount is in BTC * 10^-6
86+
elif multiplier == "n": # nano = 10^-9
87+
amount_msat = amount * 100 # amount is in BTC * 10^-9
88+
elif multiplier == "p": # pico = 10^-12
89+
amount_msat = amount // 10 # amount is in BTC * 10^-12
90+
else:
91+
# No multiplier means the amount is in BTC
92+
amount_msat = amount * 100_000_000_000 # Convert BTC to msat
93+
94+
# Convert to target currency unit
95+
if currency == "msat":
96+
return amount_msat
97+
elif currency == "sat":
98+
return amount_msat // 1000
99+
else:
100+
raise LNURLError(f"Unsupported currency for Lightning: {currency}")
101+
102+
27103
async def decode_lnurl(lnurl: str) -> str:
28104
"""Decode LNURL to get the actual URL.
29105

sixty_nuts/wallet.py

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@
3434
get_pubkey,
3535
nip44_decrypt,
3636
)
37+
from .lnurl import (
38+
get_lnurl_data,
39+
get_lnurl_invoice,
40+
parse_lightning_invoice_amount,
41+
LNURLError,
42+
)
3743
from .types import ProofDict, WalletError
3844
from .events import EventManager
3945

@@ -583,6 +589,7 @@ async def melt(self, invoice: str, *, target_mint: str | None = None) -> None:
583589
584590
Args:
585591
invoice: BOLT-11 Lightning invoice to pay
592+
target_mint: Target mint URL (defaults to primary mint)
586593
587594
Raises:
588595
WalletError: If insufficient balance or payment fails
@@ -593,28 +600,61 @@ async def melt(self, invoice: str, *, target_mint: str | None = None) -> None:
593600
if target_mint is None:
594601
target_mint = self.mint_urls[0]
595602

596-
invoice_amount = 0 # TODO: get_invoice_amount_with_fees(invoice)
603+
try:
604+
invoice_amount = parse_lightning_invoice_amount(invoice, self.currency)
605+
except LNURLError as e:
606+
raise WalletError(f"Invalid Lightning invoice: {e}") from e
597607

608+
# Get current state and check balance
598609
state = await self.fetch_wallet_state(check_proofs=True)
599-
self.raise_if_insufficient_balance(state.balance, invoice_amount)
600610

611+
# Create melt quote to get fees
612+
mint = self._get_mint(target_mint)
613+
melt_quote = await mint.create_melt_quote(unit=self.currency, request=invoice)
614+
fee_reserve = melt_quote.get("fee_reserve", 0)
615+
total_needed = invoice_amount + fee_reserve
616+
617+
self.raise_if_insufficient_balance(state.balance, total_needed)
618+
619+
# Select proofs for the total amount needed (invoice + fees)
601620
selected_proofs, consumed_proofs = await self._select_proofs(
602-
state.proofs, invoice_amount, target_mint
621+
state.proofs, total_needed, target_mint
603622
)
604-
print(selected_proofs)
605-
# TODO: self.mark_pending_proofs(selected_proofs)
606623

607-
# melt proofs and pay invoice
608-
mint = self._get_mint(target_mint)
609-
melt_quote = await mint.create_melt_quote(unit=self.currency, request=invoice)
610-
print(melt_quote)
611-
# TODO: convert selected_proofs to mint format
612-
# melt_resp = await mint.melt(quote=melt_quote["quote"], inputs=selected_proofs)
624+
# Convert selected proofs to mint format
625+
mint_proofs = [self._proofdict_to_mint_proof(p) for p in selected_proofs]
613626

614-
# TODO: check success and undo if failed or retry
627+
# Execute the melt operation
628+
melt_resp = await mint.melt(quote=melt_quote["quote"], inputs=mint_proofs)
615629

616-
# TODO: publish spending history with fee information
617-
pass
630+
# Check if payment was successful
631+
if not melt_resp.get("paid", False):
632+
raise WalletError(
633+
f"Lightning payment failed. State: {melt_resp.get('state', 'unknown')}"
634+
)
635+
636+
# Handle any change returned from the mint
637+
change_proofs: list[ProofDict] = []
638+
if "change" in melt_resp and melt_resp["change"]:
639+
# Convert BlindedSignatures to ProofDict format
640+
# This would require unblinding logic, but for now we'll skip change handling
641+
# In practice, most melts shouldn't have change if amounts are selected properly
642+
pass
643+
644+
# Mark the consumed input proofs as spent
645+
await self._mark_proofs_as_spent(consumed_proofs)
646+
647+
# Store any change proofs
648+
if change_proofs:
649+
await self.store_proofs(change_proofs)
650+
651+
# Publish spending history
652+
event_manager = await self._ensure_event_manager()
653+
await event_manager.publish_spending_history(
654+
direction="out",
655+
amount=invoice_amount, # The actual invoice amount paid
656+
destroyed_token_ids=[], # Will be handled by _mark_proofs_as_spent
657+
)
618658

619659
async def send(
620660
self,
@@ -700,7 +740,6 @@ async def send_to_lnurl(self, lnurl: str, amount: int) -> int:
700740
paid = await wallet.send_to_lnurl("user@getalby.com", 1000)
701741
print(f"Paid {paid} sats")
702742
"""
703-
from .lnurl import get_lnurl_data, get_lnurl_invoice
704743

705744
# Get current balance
706745
state = await self.fetch_wallet_state(check_proofs=True)
@@ -1777,13 +1816,17 @@ async def fetch_wallet_state(self, *, check_proofs: bool = True) -> WalletState:
17771816
decrypted = nip44_decrypt(wallet_event["content"], self._privkey)
17781817
wallet_data = json.loads(decrypted)
17791818

1780-
# Update mint URLs from wallet event
1781-
self.mint_urls = []
1819+
# Update mint URLs from wallet event (only if event contains mint URLs)
1820+
event_mint_urls = []
17821821
for item in wallet_data:
17831822
if item[0] == "mint":
1784-
self.mint_urls.append(item[1])
1823+
event_mint_urls.append(item[1])
17851824
elif item[0] == "privkey":
17861825
self.wallet_privkey = item[1]
1826+
1827+
# Only update mint URLs if the event actually contains some
1828+
if event_mint_urls:
1829+
self.mint_urls = event_mint_urls
17871830
except Exception as e:
17881831
# Skip wallet event if it can't be decrypted
17891832
print(f"Warning: Could not decrypt wallet event: {e}")

0 commit comments

Comments
 (0)