nip60: slight improvement to lightning melt-mint flow.

This commit is contained in:
fiatjaf 2025-01-28 23:28:13 -03:00
parent 2244740f61
commit 794561a3b3
4 changed files with 89 additions and 63 deletions

View File

@ -12,6 +12,15 @@ import (
"github.com/nbd-wtf/go-nostr/nip60/client"
)
type lightningSwapStatus int
const (
nothingCanBeDone = iota
tryAnotherTargetMint
storeTokenFromSourceMint
manualActionRequired
)
// lightningMeltMint does the lightning dance of moving funds between mints
func lightningMeltMint(
ctx context.Context,
@ -19,17 +28,17 @@ func lightningMeltMint(
from string,
fromKeysets []nut02.Keyset,
to string,
) (newProofs cashu.Proofs, err error, canTryWithAnotherTargetMint bool, manualActionRequired bool) {
) (cashu.Proofs, error, lightningSwapStatus) {
// get active keyset of target mint
keyset, err := client.GetActiveKeyset(ctx, to)
if err != nil {
return nil, fmt.Errorf("failed to get keyset keys for %s: %w", to, err), true, false
return nil, fmt.Errorf("failed to get keyset keys for %s: %w", to, err), tryAnotherTargetMint
}
// unblind the signatures from the promises and build the proofs
keysetKeys, err := parseKeysetKeys(keyset.Keys)
if err != nil {
return nil, fmt.Errorf("target mint %s sent us an invalid keyset: %w", to, err), true, false
return nil, fmt.Errorf("target mint %s sent us an invalid keyset: %w", to, err), tryAnotherTargetMint
}
// now we start the melt-mint process in multiple attempts
@ -46,7 +55,7 @@ func lightningMeltMint(
Unit: cashu.Sat.String(),
})
if err != nil {
return nil, fmt.Errorf("error requesting mint quote from %s: %w", to, err), true, false
return nil, fmt.Errorf("error requesting mint quote from %s: %w", to, err), tryAnotherTargetMint
}
// request _melt_ quote from the 'from' mint
@ -56,7 +65,7 @@ func lightningMeltMint(
Unit: cashu.Sat.String(),
})
if err != nil {
return nil, fmt.Errorf("error requesting melt quote from %s: %w", from, err), false, false
return nil, fmt.Errorf("error requesting melt quote from %s: %w", from, err), nothingCanBeDone
}
// if amount in proofs is less than amount asked from mint in melt request,
@ -71,64 +80,69 @@ func lightningMeltMint(
}
}
return nil, fmt.Errorf("stop trying to do the melt because the mint part is too expensive"), true, false
return nil, fmt.Errorf("stop trying to do the melt because the mint part is too expensive"), tryAnotherTargetMint
meltworked:
// request from mint to pay invoice from the mint quote request
_, err = client.PostMeltBolt11(ctx, from, nut05.PostMeltBolt11Request{
// request from mint to _melt_ into paying the invoice
delay := 200 * time.Millisecond
// this request will block until the invoice is paid or it fails
// (but the API also says it can return "pending" so we handle both)
meltStatus, err := client.PostMeltBolt11(ctx, from, nut05.PostMeltBolt11Request{
Quote: meltQuote,
Inputs: proofs,
})
inspectmeltstatusresponse:
if err != nil || meltStatus.State == nut05.Unpaid {
return nil, fmt.Errorf("error melting token: %w", err), storeTokenFromSourceMint
} else if meltStatus.State == nut05.Unknown {
return nil,
fmt.Errorf("we don't know what happened with the melt at %s: %v", from, meltStatus),
manualActionRequired
} else if meltStatus.State == nut05.Pending {
for {
time.Sleep(delay)
delay *= 2
meltStatus, err = client.GetMeltQuoteState(ctx, from, meltStatus.Quote)
goto inspectmeltstatusresponse
}
}
// source mint says it has paid the invoice, now check it against the target mint
// check if the _mint_ invoice was paid
mintQuoteStatusResp, err := client.GetMintQuoteState(ctx, to, mintQuote)
if err != nil {
return nil, fmt.Errorf("error melting token: %v", err), false, true
return nil, fmt.Errorf(
"target mint %s failed to answer to our mint quote checks (%s): %w; a manual fix is needed",
to, meltQuote, err,
), manualActionRequired
}
if mintQuoteStatusResp.State != nut04.Paid {
return nil, fmt.Errorf(
"target mint %s says the invoice wasn't paid although the source mint %s said it did, %s -> %s",
to, from, meltQuote, mintQuote,
), manualActionRequired
}
sleepTime := time.Millisecond * 200
failures := 0
for range 12 {
sleepTime *= 2
time.Sleep(sleepTime)
// check if the _mint_ invoice was paid
mintQuoteStatusResp, err := client.GetMintQuoteState(ctx, to, mintQuote)
if err != nil {
failures++
if failures > 10 {
return nil, fmt.Errorf(
"target mint %s failed to answer to our mint quote checks (%s): %w; a manual fix is needed",
to, meltQuote, err,
), false, true
}
}
// if it wasn't paid try again
if mintQuoteStatusResp.State != nut04.Paid {
continue
}
// if it got paid make proceed to get proofs
split := []uint64{1, 2, 3, 4}
blindedMessages, secrets, rs, err := createBlindedMessages(split, keyset.Id, nil)
if err != nil {
return nil, fmt.Errorf("error creating blinded messages: %v", err), false, true
}
// request mint to sign the blinded messages
mintResponse, err := client.PostMintBolt11(ctx, to, nut04.PostMintBolt11Request{
Quote: mintQuote,
Outputs: blindedMessages,
})
if err != nil {
return nil, fmt.Errorf("mint request to %s failed (%s): %w", to, mintQuote, err), false, true
}
proofs, err := constructProofs(mintResponse.Signatures, blindedMessages, secrets, rs, keysetKeys)
if err != nil {
return nil, fmt.Errorf("error constructing proofs: %w", err), false, true
}
return proofs, nil, false, false
// if it got paid make proceed to get proofs
split := []uint64{1, 2, 3, 4}
blindedMessages, secrets, rs, err := createBlindedMessages(split, keyset.Id, nil)
if err != nil {
return nil, fmt.Errorf("error creating blinded messages: %v", err), manualActionRequired
}
return nil, fmt.Errorf("we gave up waiting for the invoice at %s to be paid: %s", to, meltQuote), false, true
// request mint to sign the blinded messages
mintResponse, err := client.PostMintBolt11(ctx, to, nut04.PostMintBolt11Request{
Quote: mintQuote,
Outputs: blindedMessages,
})
if err != nil {
return nil, fmt.Errorf("mint request to %s failed (%s): %w", to, mintQuote, err), manualActionRequired
}
proofs, err = constructProofs(mintResponse.Signatures, blindedMessages, secrets, rs, keysetKeys)
if err != nil {
return nil, fmt.Errorf("error constructing proofs: %w", err), manualActionRequired
}
return proofs, nil, nothingCanBeDone
}

View File

@ -59,7 +59,7 @@ func (w *Wallet) ReceiveToken(ctx context.Context, serializedToken string) error
// and telling the current mint to pay it
if lightningSwap {
for _, targetMint := range w.Mints {
swappedProofs, err, tryAnother, needsManualAction := lightningMeltMint(
swappedProofs, err, status := lightningMeltMint(
ctx,
newProofs,
source,
@ -67,12 +67,15 @@ func (w *Wallet) ReceiveToken(ctx context.Context, serializedToken string) error
targetMint,
)
if err != nil {
if tryAnother {
if status == tryAnotherTargetMint {
continue
}
if needsManualAction {
if status == manualActionRequired {
return fmt.Errorf("failed to swap (needs manual action): %w", err)
}
if status == nothingCanBeDone {
return fmt.Errorf("failed to swap (nothing can be done, we probably lost the money): %w", err)
}
// if we get here that means we still have our proofs from the untrusted mint, so save those
goto saveproofs

View File

@ -158,12 +158,13 @@ found:
updatedTokens = append(updatedTokens, token)
}
if err := changeToken.toEvent(ctx, w.wl.kr, w.Identifier, changeToken.event); err != nil {
return "", fmt.Errorf("failed to make change token: %w", err)
if len(changeToken.Proofs) > 0 {
if err := changeToken.toEvent(ctx, w.wl.kr, w.Identifier, changeToken.event); err != nil {
return "", fmt.Errorf("failed to make change token: %w", err)
}
w.wl.Changes <- *changeToken.event
w.Tokens = append(updatedTokens, changeToken)
}
w.wl.Changes <- *changeToken.event
w.Tokens = append(updatedTokens, changeToken)
// serialize token we're sending out
token, err := cashu.NewTokenV4(proofsToSend, target.mint, cashu.Sat, true)

View File

@ -18,6 +18,14 @@ type Token struct {
event *nostr.Event
}
func (t Token) ID() string {
if t.event != nil {
return t.event.ID
}
return "<not-published>"
}
func (t Token) toEvent(ctx context.Context, kr nostr.Keyer, walletId string, evt *nostr.Event) error {
pk, err := kr.GetPublicKey(ctx)
if err != nil {