164 lines
4.3 KiB
Go
164 lines
4.3 KiB
Go
package nip61
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"iter"
|
|
"slices"
|
|
"strings"
|
|
|
|
"fiatjaf.com/nostr"
|
|
"fiatjaf.com/nostr/nip60"
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
)
|
|
|
|
var NutzapsNotAccepted = errors.New("user doesn't accept nutzaps")
|
|
|
|
type NutzapOptions struct {
|
|
// Optionally specify the event we'll reference in the nutzap,
|
|
// if not specified we'll just send money to the receiver
|
|
EventID nostr.ID
|
|
|
|
// string message to include in the nutzap
|
|
Message string
|
|
|
|
// We'll send the nutzap to these relays besides any relay found in the kind:10019
|
|
SendToRelays []string
|
|
|
|
// Send specifically from this mint
|
|
SpecificSourceMint string
|
|
}
|
|
|
|
func SendNutzap(
|
|
ctx context.Context,
|
|
kr nostr.Keyer,
|
|
w *nip60.Wallet,
|
|
pool *nostr.Pool,
|
|
amount uint64,
|
|
targetUser nostr.PubKey,
|
|
targetUserRelays []string,
|
|
opts NutzapOptions,
|
|
) (chan nostr.PublishResult, error) {
|
|
ie := pool.QuerySingle(ctx, targetUserRelays, nostr.Filter{
|
|
Kinds: []nostr.Kind{10019},
|
|
Authors: []nostr.PubKey{targetUser},
|
|
},
|
|
nostr.SubscriptionOptions{Label: "pre-nutzap"})
|
|
if ie == nil {
|
|
return nil, NutzapsNotAccepted
|
|
}
|
|
|
|
info := Info{}
|
|
if err := info.ParseEvent(ie.Event); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(info.Mints) == 0 || info.PublicKey == nostr.ZeroPK {
|
|
return nil, NutzapsNotAccepted
|
|
}
|
|
|
|
targetRelays := nostr.AppendUnique(info.Relays, opts.SendToRelays...)
|
|
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", targetUser.Hex()})
|
|
if opts.EventID != nostr.ZeroID {
|
|
nutzap.Tags = append(nutzap.Tags, nostr.Tag{"e", opts.EventID.Hex()})
|
|
}
|
|
|
|
p2pk, err := btcec.ParsePubKey(append([]byte{2}, info.PublicKey[:]...))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid p2pk target '%s': %w", info.PublicKey.Hex(), err)
|
|
}
|
|
|
|
// check if we have enough tokens in any of these mints
|
|
for mint := range getEligibleTokensWeHave(info.Mints, w.Tokens, amount) {
|
|
if opts.SpecificSourceMint != "" && opts.SpecificSourceMint != mint {
|
|
continue
|
|
}
|
|
|
|
proofs, _, err := w.SendInternal(ctx, amount, nip60.SendOptions{
|
|
P2PK: p2pk,
|
|
SpecificSourceMint: 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, nip60.SendOptions{
|
|
P2PK: p2pk,
|
|
SpecificSourceMint: opts.SpecificSourceMint,
|
|
})
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|