mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-03-17 13:22:56 +01:00
nip61, and related modifications to nip60.
This commit is contained in:
parent
c9f670d700
commit
d0f53b3b7a
2
kinds.go
2
kinds.go
@ -59,6 +59,7 @@ const (
|
|||||||
KindSimpleGroupJoinRequest int = 9021
|
KindSimpleGroupJoinRequest int = 9021
|
||||||
KindSimpleGroupLeaveRequest int = 9022
|
KindSimpleGroupLeaveRequest int = 9022
|
||||||
KindZapGoal int = 9041
|
KindZapGoal int = 9041
|
||||||
|
KindNutZap int = 9321
|
||||||
KindTidalLogin int = 9467
|
KindTidalLogin int = 9467
|
||||||
KindZapRequest int = 9734
|
KindZapRequest int = 9734
|
||||||
KindZap int = 9735
|
KindZap int = 9735
|
||||||
@ -73,6 +74,7 @@ const (
|
|||||||
KindSearchRelayList int = 10007
|
KindSearchRelayList int = 10007
|
||||||
KindSimpleGroupList int = 10009
|
KindSimpleGroupList int = 10009
|
||||||
KindInterestList int = 10015
|
KindInterestList int = 10015
|
||||||
|
KindNutZapInfo int = 10019
|
||||||
KindEmojiList int = 10030
|
KindEmojiList int = 10030
|
||||||
KindDMRelayList int = 10050
|
KindDMRelayList int = 10050
|
||||||
KindUserServerList int = 10063
|
KindUserServerList int = 10063
|
||||||
|
@ -29,18 +29,6 @@ func lightningMeltMint(
|
|||||||
fromKeysets []nut02.Keyset,
|
fromKeysets []nut02.Keyset,
|
||||||
to string,
|
to string,
|
||||||
) (cashu.Proofs, error, lightningSwapStatus) {
|
) (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
|
// now we start the melt-mint process in multiple attempts
|
||||||
invoicePct := uint64(99)
|
invoicePct := uint64(99)
|
||||||
proofsAmount := proofs.Amount()
|
proofsAmount := proofs.Amount()
|
||||||
@ -107,46 +95,73 @@ inspectmeltstatusresponse:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// source mint says it has paid the invoice, now check it against the target mint
|
proofs, err = redeemMinted(ctx, to, mintQuote, amount)
|
||||||
// check if the _mint_ invoice was paid
|
|
||||||
mintQuoteStatusResp, err := client.GetMintQuoteState(ctx, to, mintQuote)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil,
|
||||||
"target mint %s failed to answer to our mint quote checks (%s): %w; a manual fix is needed",
|
fmt.Errorf("failed to redeem minted proofs at %s (after successfully melting at %s): %w", to, from, err),
|
||||||
to, meltQuote, err,
|
manualActionRequired
|
||||||
), 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if it got paid make proceed to get proofs
|
return proofs, nil, nothingCanBeDone
|
||||||
split := cashu.AmountSplit(amount)
|
}
|
||||||
|
|
||||||
|
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)
|
blindedMessages, secrets, rs, err := createBlindedMessages(split, keyset.Id, nil)
|
||||||
if err != 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
|
// 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,
|
Quote: mintQuote,
|
||||||
Outputs: blindedMessages,
|
Outputs: blindedMessages,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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,
|
bm: blindedMessages,
|
||||||
secrets: secrets,
|
secrets: secrets,
|
||||||
rs: rs,
|
rs: rs,
|
||||||
}, mintResponse.Signatures, keysetKeys)
|
}, mintResponse.Signatures, keysetKeys)
|
||||||
if err != nil {
|
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)
|
excludeMints := make([]string, 0, 1)
|
||||||
|
|
||||||
for range 10 {
|
for range 5 {
|
||||||
amount := invoiceAmount*(100+feeReservePct)/100 + feeReserveAbs
|
amount := invoiceAmount*(100+feeReservePct)/100 + feeReserveAbs
|
||||||
var fee uint64
|
var fee uint64
|
||||||
chosen, fee, err = w.getProofsForSending(ctx, amount, ss.specificMint, excludeMints)
|
chosen, fee, err = w.getProofsForSending(ctx, amount, ss.specificMint, excludeMints)
|
||||||
|
@ -11,19 +11,17 @@ import (
|
|||||||
"github.com/nbd-wtf/go-nostr/nip60/client"
|
"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 {
|
if w.wl.PublishUpdate == nil {
|
||||||
return fmt.Errorf("can't do write operations: missing PublishUpdate function")
|
return fmt.Errorf("can't do write operations: missing PublishUpdate function")
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := cashu.DecodeToken(serializedToken)
|
source := "http" + nostr.NormalizeURL(mint)[2:]
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
source := "http" + nostr.NormalizeURL(token.Mint())[2:]
|
|
||||||
lightningSwap := slices.Contains(w.Mints, source)
|
lightningSwap := slices.Contains(w.Mints, source)
|
||||||
proofs := token.Proofs()
|
|
||||||
swapOpts := make([]SwapOption, 0, 1)
|
swapOpts := make([]SwapOption, 0, 1)
|
||||||
|
|
||||||
for i, proof := range proofs {
|
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
|
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 {
|
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{}
|
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)
|
chosen, _, err := w.getProofsForSending(ctx, amount, ss.specificMint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
swapOpts := make([]SwapOption, 0, 2)
|
swapOpts := make([]SwapOption, 0, 2)
|
||||||
|
|
||||||
if ss.p2pk != nil {
|
if ss.p2pk != nil {
|
||||||
if info, err := client.GetMintInfo(ctx, chosen.mint); err != nil || !info.Nuts.Nut11.Supported {
|
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{
|
tags := nut11.P2PKTags{
|
||||||
@ -97,7 +97,7 @@ func (w *Wallet) SendToken(ctx context.Context, amount uint64, opts ...SendOptio
|
|||||||
// get new proofs
|
// get new proofs
|
||||||
proofsToSend, changeProofs, err := w.swapProofs(ctx, chosen.mint, chosen.proofs, amount, swapOpts...)
|
proofsToSend, changeProofs, err := w.swapProofs(ctx, chosen.mint, chosen.proofs, amount, swapOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, chosen.mint, err
|
||||||
}
|
}
|
||||||
|
|
||||||
he := HistoryEntry{
|
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 {
|
if err := w.saveChangeAndDeleteUsedTokens(ctx, chosen.mint, changeProofs, chosen.tokenIndexes, &he); err != nil {
|
||||||
return "", err
|
return nil, chosen.mint, err
|
||||||
}
|
}
|
||||||
|
|
||||||
w.wl.Lock()
|
w.wl.Lock()
|
||||||
@ -119,13 +119,7 @@ func (w *Wallet) SendToken(ctx context.Context, amount uint64, opts ...SendOptio
|
|||||||
}
|
}
|
||||||
w.wl.Unlock()
|
w.wl.Unlock()
|
||||||
|
|
||||||
// serialize token we're sending out
|
return proofsToSend, chosen.mint, nil
|
||||||
token, err := cashu.NewTokenV4(proofsToSend, chosen.mint, cashu.Sat, true)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return token.Serialize()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Wallet) saveChangeAndDeleteUsedTokens(
|
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)
|
return fmt.Errorf("failed to parse token content: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.Mint = "http" + nostr.NormalizeURL(t.Mint)[2:]
|
||||||
|
|
||||||
return nil
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
halfBalance := initialBalance1 / 2
|
halfBalance := initialBalance1 / 2
|
||||||
token, err := w1.SendToken(ctx, halfBalance, WithP2PK(pk2))
|
proofs, mint, err := w1.Send(ctx, halfBalance, WithP2PK(pk2))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// receive token in wallet 2
|
// receive token in wallet 2
|
||||||
err = w2.ReceiveToken(ctx, token)
|
err = w2.Receive(ctx, proofs, mint)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// verify balances
|
// verify balances
|
||||||
@ -100,11 +100,11 @@ func TestWalletTransfer(t *testing.T) {
|
|||||||
pk1, err := kr1.GetPublicKey(ctx)
|
pk1, err := kr1.GetPublicKey(ctx)
|
||||||
require.NoError(t, err)
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// receive token back in wallet 1
|
// receive token back in wallet 1
|
||||||
err = w1.ReceiveToken(ctx, token)
|
err = w1.Receive(ctx, proofs, mint)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// verify final balances match initial
|
// verify final balances match initial
|
||||||
|
61
nip61/info.go
Normal file
61
nip61/info.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package nip61
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/elnosh/gonuts/cashu"
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Info struct {
|
||||||
|
PublicKey string
|
||||||
|
Mints []string
|
||||||
|
Relays []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (zi *Info) toEvent(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) error {
|
||||||
|
evt.CreatedAt = nostr.Now()
|
||||||
|
evt.Kind = 10019
|
||||||
|
|
||||||
|
evt.Tags = make(nostr.Tags, 0, len(zi.Mints)+len(zi.Relays)+1)
|
||||||
|
for _, mint := range zi.Mints {
|
||||||
|
evt.Tags = append(evt.Tags, nostr.Tag{"mint", mint})
|
||||||
|
}
|
||||||
|
for _, url := range zi.Relays {
|
||||||
|
evt.Tags = append(evt.Tags, nostr.Tag{"relay", url})
|
||||||
|
}
|
||||||
|
if zi.PublicKey != "" {
|
||||||
|
evt.Tags = append(evt.Tags, nostr.Tag{"pubkey", zi.PublicKey})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := kr.SignEvent(ctx, evt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (zi *Info) parse(evt *nostr.Event) error {
|
||||||
|
zi.Mints = make([]string, 0)
|
||||||
|
for _, tag := range evt.Tags {
|
||||||
|
if len(tag) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag[0] {
|
||||||
|
case "mint":
|
||||||
|
if len(tag) == 2 || slices.Contains(tag[2:], cashu.Sat.String()) {
|
||||||
|
zi.Mints = append(zi.Mints, "http"+nostr.NormalizeURL(tag[1])[2:])
|
||||||
|
}
|
||||||
|
case "relay":
|
||||||
|
zi.Relays = append(zi.Relays, tag[1])
|
||||||
|
case "pubkey":
|
||||||
|
if nostr.IsValidPublicKey(tag[1]) {
|
||||||
|
zi.PublicKey = tag[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
135
nip61/nip61.go
Normal file
135
nip61/nip61.go
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
package nip61
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"iter"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/elnosh/gonuts/cashu"
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip60"
|
||||||
|
)
|
||||||
|
|
||||||
|
var NutzapsNotAccepted = errors.New("user doesn't accept nutzaps")
|
||||||
|
|
||||||
|
func SendNutzap(
|
||||||
|
ctx context.Context,
|
||||||
|
kr nostr.Keyer,
|
||||||
|
w *nip60.Wallet,
|
||||||
|
pool *nostr.SimplePool,
|
||||||
|
targetUserPublickey string,
|
||||||
|
getUserReadRelays func(context.Context, string, int) []string,
|
||||||
|
relays []string,
|
||||||
|
eventId string, // can be "" if not targeting a specific event
|
||||||
|
amount uint64,
|
||||||
|
message string,
|
||||||
|
) (chan nostr.PublishResult, error) {
|
||||||
|
ie := pool.QuerySingle(ctx, relays, nostr.Filter{Kinds: []int{10019}, Authors: []string{targetUserPublickey}})
|
||||||
|
if ie == nil {
|
||||||
|
return nil, NutzapsNotAccepted
|
||||||
|
}
|
||||||
|
|
||||||
|
info := Info{}
|
||||||
|
if err := info.parse(ie.Event); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(info.Mints) == 0 || info.PublicKey == "" {
|
||||||
|
return nil, NutzapsNotAccepted
|
||||||
|
}
|
||||||
|
|
||||||
|
targetRelays := info.Relays
|
||||||
|
if len(targetRelays) == 0 {
|
||||||
|
targetRelays = getUserReadRelays(ctx, targetUserPublickey, 3)
|
||||||
|
if len(targetRelays) == 0 {
|
||||||
|
return nil, fmt.Errorf("no relays found for sending the nutzap")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nutzap := nostr.Event{
|
||||||
|
CreatedAt: nostr.Now(),
|
||||||
|
Kind: nostr.KindNutZap,
|
||||||
|
Tags: make(nostr.Tags, 0, 8),
|
||||||
|
}
|
||||||
|
|
||||||
|
nutzap.Tags = append(nutzap.Tags, nostr.Tag{"p", targetUserPublickey})
|
||||||
|
nutzap.Tags = append(nutzap.Tags, nostr.Tag{"unit", cashu.Sat.String()})
|
||||||
|
if eventId != "" {
|
||||||
|
nutzap.Tags = append(nutzap.Tags, nostr.Tag{"e", eventId})
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we have enough tokens in any of these mints
|
||||||
|
for mint := range getEligibleTokensWeHave(info.Mints, w.Tokens, amount) {
|
||||||
|
proofs, _, err := w.Send(ctx, amount, nip60.WithP2PK(info.PublicKey), nip60.WithMint(mint))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have succeeded, now we just have to publish the event
|
||||||
|
nutzap.Tags = append(nutzap.Tags, nostr.Tag{"u", mint})
|
||||||
|
for _, proof := range proofs {
|
||||||
|
proofj, _ := json.Marshal(proof)
|
||||||
|
nutzap.Tags = append(nutzap.Tags, nostr.Tag{"proof", string(proofj)})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := kr.SignEvent(ctx, &nutzap); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to sign nutzap event %s: %w", nutzap, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pool.PublishMany(ctx, targetRelays, nutzap), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// we don't have tokens at the desired target mint, so we first have to create some
|
||||||
|
for _, mint := range info.Mints {
|
||||||
|
proofs, err := w.SendExternal(ctx, mint, amount)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "generate mint quote") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to send: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have succeeded, now we just have to publish the event
|
||||||
|
nutzap.Tags = append(nutzap.Tags, nostr.Tag{"u", mint})
|
||||||
|
for _, proof := range proofs {
|
||||||
|
proofj, _ := json.Marshal(proof)
|
||||||
|
nutzap.Tags = append(nutzap.Tags, nostr.Tag{"proof", string(proofj)})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := kr.SignEvent(ctx, &nutzap); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to sign nutzap event %s: %w", nutzap, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pool.PublishMany(ctx, targetRelays, nutzap), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("failed to send, we don't have enough money or all mints are down")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEligibleTokensWeHave(
|
||||||
|
theirMints []string,
|
||||||
|
ourTokens []nip60.Token,
|
||||||
|
targetAmount uint64,
|
||||||
|
) iter.Seq[string] {
|
||||||
|
have := make([]uint64, len(theirMints))
|
||||||
|
|
||||||
|
return func(yield func(string) bool) {
|
||||||
|
for _, token := range ourTokens {
|
||||||
|
if idx := slices.Index(theirMints, token.Mint); idx != -1 {
|
||||||
|
have[idx] += token.Proofs.Amount()
|
||||||
|
|
||||||
|
/* hardcoded estimated maximum fee,
|
||||||
|
unlikely to be more than this */
|
||||||
|
if have[idx] > targetAmount*101/100+2 {
|
||||||
|
if !yield(token.Mint) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user