mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-08-03 08:22:12 +02:00
nip61, and related modifications to nip60.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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
36
nip60/send-external.go
Normal 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)
|
||||
}
|
@@ -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(
|
||||
|
@@ -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
25
nip60/utils.go
Normal 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
|
||||
}
|
@@ -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
|
||||
|
Reference in New Issue
Block a user