mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-11-15 16:50:16 +01:00
nip60: slight improvement to lightning melt-mint flow.
This commit is contained in:
@@ -12,6 +12,15 @@ import (
|
|||||||
"github.com/nbd-wtf/go-nostr/nip60/client"
|
"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
|
// lightningMeltMint does the lightning dance of moving funds between mints
|
||||||
func lightningMeltMint(
|
func lightningMeltMint(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
@@ -19,17 +28,17 @@ func lightningMeltMint(
|
|||||||
from string,
|
from string,
|
||||||
fromKeysets []nut02.Keyset,
|
fromKeysets []nut02.Keyset,
|
||||||
to string,
|
to string,
|
||||||
) (newProofs cashu.Proofs, err error, canTryWithAnotherTargetMint bool, manualActionRequired bool) {
|
) (cashu.Proofs, error, lightningSwapStatus) {
|
||||||
// get active keyset of target mint
|
// get active keyset of target mint
|
||||||
keyset, err := client.GetActiveKeyset(ctx, to)
|
keyset, err := client.GetActiveKeyset(ctx, to)
|
||||||
if err != nil {
|
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
|
// unblind the signatures from the promises and build the proofs
|
||||||
keysetKeys, err := parseKeysetKeys(keyset.Keys)
|
keysetKeys, err := parseKeysetKeys(keyset.Keys)
|
||||||
if err != nil {
|
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
|
// now we start the melt-mint process in multiple attempts
|
||||||
@@ -46,7 +55,7 @@ func lightningMeltMint(
|
|||||||
Unit: cashu.Sat.String(),
|
Unit: cashu.Sat.String(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
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
|
// request _melt_ quote from the 'from' mint
|
||||||
@@ -56,7 +65,7 @@ func lightningMeltMint(
|
|||||||
Unit: cashu.Sat.String(),
|
Unit: cashu.Sat.String(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
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,
|
// 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:
|
meltworked:
|
||||||
// request from mint to pay invoice from the mint quote request
|
// request from mint to _melt_ into paying the invoice
|
||||||
_, err = client.PostMeltBolt11(ctx, from, nut05.PostMeltBolt11Request{
|
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,
|
Quote: meltQuote,
|
||||||
Inputs: proofs,
|
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 {
|
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
|
// if it got paid make proceed to get proofs
|
||||||
failures := 0
|
split := []uint64{1, 2, 3, 4}
|
||||||
for range 12 {
|
blindedMessages, secrets, rs, err := createBlindedMessages(split, keyset.Id, nil)
|
||||||
sleepTime *= 2
|
if err != nil {
|
||||||
time.Sleep(sleepTime)
|
return nil, fmt.Errorf("error creating blinded messages: %v", err), manualActionRequired
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ func (w *Wallet) ReceiveToken(ctx context.Context, serializedToken string) error
|
|||||||
// and telling the current mint to pay it
|
// and telling the current mint to pay it
|
||||||
if lightningSwap {
|
if lightningSwap {
|
||||||
for _, targetMint := range w.Mints {
|
for _, targetMint := range w.Mints {
|
||||||
swappedProofs, err, tryAnother, needsManualAction := lightningMeltMint(
|
swappedProofs, err, status := lightningMeltMint(
|
||||||
ctx,
|
ctx,
|
||||||
newProofs,
|
newProofs,
|
||||||
source,
|
source,
|
||||||
@@ -67,12 +67,15 @@ func (w *Wallet) ReceiveToken(ctx context.Context, serializedToken string) error
|
|||||||
targetMint,
|
targetMint,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if tryAnother {
|
if status == tryAnotherTargetMint {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if needsManualAction {
|
if status == manualActionRequired {
|
||||||
return fmt.Errorf("failed to swap (needs manual action): %w", err)
|
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
|
// if we get here that means we still have our proofs from the untrusted mint, so save those
|
||||||
goto saveproofs
|
goto saveproofs
|
||||||
|
|||||||
@@ -158,12 +158,13 @@ found:
|
|||||||
updatedTokens = append(updatedTokens, token)
|
updatedTokens = append(updatedTokens, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := changeToken.toEvent(ctx, w.wl.kr, w.Identifier, changeToken.event); err != nil {
|
if len(changeToken.Proofs) > 0 {
|
||||||
return "", fmt.Errorf("failed to make change token: %w", err)
|
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
|
// serialize token we're sending out
|
||||||
token, err := cashu.NewTokenV4(proofsToSend, target.mint, cashu.Sat, true)
|
token, err := cashu.NewTokenV4(proofsToSend, target.mint, cashu.Sat, true)
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ type Token struct {
|
|||||||
event *nostr.Event
|
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 {
|
func (t Token) toEvent(ctx context.Context, kr nostr.Keyer, walletId string, evt *nostr.Event) error {
|
||||||
pk, err := kr.GetPublicKey(ctx)
|
pk, err := kr.GetPublicKey(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user