nip61, and related modifications to nip60.

This commit is contained in:
fiatjaf
2025-02-02 18:16:46 -03:00
parent c9f670d700
commit d0f53b3b7a
11 changed files with 327 additions and 59 deletions

View File

@@ -29,18 +29,6 @@ func lightningMeltMint(
fromKeysets []nut02.Keyset,
to string,
) (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), 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), tryAnotherTargetMint
}
// now we start the melt-mint process in multiple attempts
invoicePct := uint64(99)
proofsAmount := proofs.Amount()
@@ -107,46 +95,73 @@ 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)
proofs, err = redeemMinted(ctx, to, mintQuote, amount)
if err != nil {
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
return nil,
fmt.Errorf("failed to redeem minted proofs at %s (after successfully melting at %s): %w", to, from, err),
manualActionRequired
}
// if it got paid make proceed to get proofs
split := cashu.AmountSplit(amount)
return proofs, nil, nothingCanBeDone
}
func redeemMinted(
ctx context.Context,
mint string,
mintQuote string,
mintAmount uint64,
) (cashu.Proofs, error) {
// 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, mint, mintQuote)
if err != nil {
return nil, fmt.Errorf(
"target failed to answer to our mint quote checks (%s): %w; a manual fix is needed",
mintQuote, err,
)
}
if mintQuoteStatusResp.State != nut04.Paid {
return nil, fmt.Errorf("target says the invoice wasn't paid (mint quote %s)", mintQuote)
}
// since it got paid proceed to get proofs
//
// get active keyset of target mint
keyset, err := client.GetActiveKeyset(ctx, mint)
if err != nil {
return nil, fmt.Errorf("failed to get keyset keys for %s: %w", mint, err)
}
// unblind the signatures from the promises and build the blinded messages
keysetKeys, err := parseKeysetKeys(keyset.Keys)
if err != nil {
return nil, fmt.Errorf("target mint %s sent us an invalid keyset: %w", mint, err)
}
split := cashu.AmountSplit(mintAmount)
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("error creating blinded messages: %w", err)
}
// request mint to sign the blinded messages
mintResponse, err := client.PostMintBolt11(ctx, to, nut04.PostMintBolt11Request{
mintResponse, err := client.PostMintBolt11(ctx, mint, nut04.PostMintBolt11Request{
Quote: mintQuote,
Outputs: blindedMessages,
})
if err != nil {
return nil, fmt.Errorf("mint request to %s failed (%s): %w", to, mintQuote, err), manualActionRequired
return nil, fmt.Errorf("mint request: %w", err)
}
proofs, err = constructProofs(preparedOutputs{
// finally turn those into proofs
proofs, err := constructProofs(preparedOutputs{
bm: blindedMessages,
secrets: secrets,
rs: rs,
}, mintResponse.Signatures, keysetKeys)
if err != nil {
return nil, fmt.Errorf("error constructing proofs: %w", err), manualActionRequired
return nil, fmt.Errorf("error constructing proofs: %w", err)
}
return proofs, nil, nothingCanBeDone
return proofs, nil
}

View File

@@ -38,7 +38,7 @@ func (w *Wallet) PayBolt11(ctx context.Context, invoice string, opts ...SendOpti
excludeMints := make([]string, 0, 1)
for range 10 {
for range 5 {
amount := invoiceAmount*(100+feeReservePct)/100 + feeReserveAbs
var fee uint64
chosen, fee, err = w.getProofsForSending(ctx, amount, ss.specificMint, excludeMints)

View File

@@ -11,19 +11,17 @@ import (
"github.com/nbd-wtf/go-nostr/nip60/client"
)
func (w *Wallet) ReceiveToken(ctx context.Context, serializedToken string) error {
func (w *Wallet) Receive(
ctx context.Context,
proofs cashu.Proofs,
mint string,
) error {
if w.wl.PublishUpdate == nil {
return fmt.Errorf("can't do write operations: missing PublishUpdate function")
}
token, err := cashu.DecodeToken(serializedToken)
if err != nil {
return err
}
source := "http" + nostr.NormalizeURL(token.Mint())[2:]
source := "http" + nostr.NormalizeURL(mint)[2:]
lightningSwap := slices.Contains(w.Mints, source)
proofs := token.Proofs()
swapOpts := make([]SwapOption, 0, 1)
for i, proof := range proofs {

36
nip60/send-external.go Normal file
View File

@@ -0,0 +1,36 @@
package nip60
import (
"context"
"fmt"
"github.com/elnosh/gonuts/cashu"
"github.com/elnosh/gonuts/cashu/nuts/nut04"
"github.com/nbd-wtf/go-nostr/nip60/client"
)
func (w *Wallet) SendExternal(
ctx context.Context,
mint string,
targetAmount uint64,
opts ...SendOption,
) (cashu.Proofs, error) {
if w.wl.PublishUpdate == nil {
return nil, fmt.Errorf("can't do write operations: missing PublishUpdate function")
}
// get the invoice from target mint
mintResp, err := client.PostMintQuoteBolt11(ctx, mint, nut04.PostMintQuoteBolt11Request{
Unit: cashu.Sat.String(),
Amount: targetAmount,
})
if err != nil {
return nil, fmt.Errorf("failed to generate mint quote: %w", err)
}
if _, err := w.PayBolt11(ctx, mintResp.Request, opts...); err != nil {
return nil, err
}
return redeemMinted(ctx, mint, mintResp.Quote, targetAmount)
}

View File

@@ -50,9 +50,9 @@ type chosenTokens struct {
keysets []nut02.Keyset
}
func (w *Wallet) SendToken(ctx context.Context, amount uint64, opts ...SendOption) (string, error) {
func (w *Wallet) Send(ctx context.Context, amount uint64, opts ...SendOption) (cashu.Proofs, string, error) {
if w.wl.PublishUpdate == nil {
return "", fmt.Errorf("can't do write operations: missing PublishUpdate function")
return nil, "", fmt.Errorf("can't do write operations: missing PublishUpdate function")
}
ss := &sendSettings{}
@@ -65,14 +65,14 @@ func (w *Wallet) SendToken(ctx context.Context, amount uint64, opts ...SendOptio
chosen, _, err := w.getProofsForSending(ctx, amount, ss.specificMint, nil)
if err != nil {
return "", err
return nil, "", err
}
swapOpts := make([]SwapOption, 0, 2)
if ss.p2pk != nil {
if info, err := client.GetMintInfo(ctx, chosen.mint); err != nil || !info.Nuts.Nut11.Supported {
return "", fmt.Errorf("mint doesn't support p2pk: %w", err)
return nil, chosen.mint, fmt.Errorf("mint doesn't support p2pk: %w", err)
}
tags := nut11.P2PKTags{
@@ -97,7 +97,7 @@ func (w *Wallet) SendToken(ctx context.Context, amount uint64, opts ...SendOptio
// get new proofs
proofsToSend, changeProofs, err := w.swapProofs(ctx, chosen.mint, chosen.proofs, amount, swapOpts...)
if err != nil {
return "", err
return nil, chosen.mint, err
}
he := HistoryEntry{
@@ -110,7 +110,7 @@ func (w *Wallet) SendToken(ctx context.Context, amount uint64, opts ...SendOptio
}
if err := w.saveChangeAndDeleteUsedTokens(ctx, chosen.mint, changeProofs, chosen.tokenIndexes, &he); err != nil {
return "", err
return nil, chosen.mint, err
}
w.wl.Lock()
@@ -119,13 +119,7 @@ func (w *Wallet) SendToken(ctx context.Context, amount uint64, opts ...SendOptio
}
w.wl.Unlock()
// serialize token we're sending out
token, err := cashu.NewTokenV4(proofsToSend, chosen.mint, cashu.Sat, true)
if err != nil {
return "", err
}
return token.Serialize()
return proofsToSend, chosen.mint, nil
}
func (w *Wallet) saveChangeAndDeleteUsedTokens(

View File

@@ -72,5 +72,7 @@ func (t *Token) parse(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) err
return fmt.Errorf("failed to parse token content: %w", err)
}
t.Mint = "http" + nostr.NormalizeURL(t.Mint)[2:]
return nil
}

25
nip60/utils.go Normal file
View File

@@ -0,0 +1,25 @@
package nip60
import "github.com/elnosh/gonuts/cashu"
func GetProofsAndMint(tokenStr string) (cashu.Proofs, string, error) {
token, err := cashu.DecodeToken(tokenStr)
if err != nil {
return nil, "", err
}
return token.Proofs(), token.Mint(), nil
}
func MakeTokenString(proofs cashu.Proofs, mint string) string {
token, err := cashu.NewTokenV4(proofs, mint, cashu.Sat, true)
if err != nil {
panic(err)
}
tokenStr, err := token.Serialize()
if err != nil {
panic(err)
}
return tokenStr
}

View File

@@ -85,11 +85,11 @@ func TestWalletTransfer(t *testing.T) {
require.NoError(t, err)
halfBalance := initialBalance1 / 2
token, err := w1.SendToken(ctx, halfBalance, WithP2PK(pk2))
proofs, mint, err := w1.Send(ctx, halfBalance, WithP2PK(pk2))
require.NoError(t, err)
// receive token in wallet 2
err = w2.ReceiveToken(ctx, token)
err = w2.Receive(ctx, proofs, mint)
require.NoError(t, err)
// verify balances
@@ -100,11 +100,11 @@ func TestWalletTransfer(t *testing.T) {
pk1, err := kr1.GetPublicKey(ctx)
require.NoError(t, err)
token, err = w2.SendToken(ctx, halfBalance, WithP2PK(pk1))
proofs, mint, err = w2.Send(ctx, halfBalance, WithP2PK(pk1))
require.NoError(t, err)
// receive token back in wallet 1
err = w1.ReceiveToken(ctx, token)
err = w1.Receive(ctx, proofs, mint)
require.NoError(t, err)
// verify final balances match initial