nip60: wallet.PayBolt11()

This commit is contained in:
fiatjaf
2025-01-29 14:54:55 -03:00
parent 26597452c5
commit 7c04b497ec
4 changed files with 207 additions and 67 deletions

View File

@@ -42,6 +42,14 @@ func WithMint(url string) SendOption {
}
}
type chosenTokens struct {
mint string
tokens []Token
tokenIndexes []int
proofs cashu.Proofs
keysets []nut02.Keyset
}
func (w *Wallet) SendToken(ctx context.Context, amount uint64, opts ...SendOption) (string, error) {
ss := &sendSettings{}
for _, opt := range opts {
@@ -51,56 +59,15 @@ func (w *Wallet) SendToken(ctx context.Context, amount uint64, opts ...SendOptio
w.tokensMu.Lock()
defer w.tokensMu.Unlock()
type part struct {
mint string
tokens []Token
tokenIndexes []int
proofs cashu.Proofs
keysets []nut02.Keyset
chosen, _, err := w.getProofsForSending(ctx, amount, ss.specificMint)
if err != nil {
return "", err
}
var target part
byMint := make(map[string]part)
for t, token := range w.Tokens {
if ss.specificMint != "" && token.Mint != ss.specificMint {
continue
}
part, ok := byMint[token.Mint]
if !ok {
keysets, err := client.GetAllKeysets(ctx, token.Mint)
if err != nil {
return "", fmt.Errorf("failed to get %s keysets: %w", token.Mint, err)
}
part.keysets = keysets
part.tokens = make([]Token, 0, 3)
part.tokenIndexes = make([]int, 0, 3)
part.proofs = make(cashu.Proofs, 0, 7)
part.mint = token.Mint
}
part.tokens = append(part.tokens, token)
part.tokenIndexes = append(part.tokenIndexes, t)
part.proofs = append(part.proofs, token.Proofs...)
if part.proofs.Amount() >= amount {
// maybe we found it here
fee := calculateFee(part.proofs, part.keysets)
if part.proofs.Amount() >= (amount + fee) {
// yes, we did
target = part
goto found
}
}
}
// if we got here it's because we didn't get enough proofs from the same mint
return "", fmt.Errorf("not enough proofs found from the same mint")
found:
swapOpts := make([]SwapOption, 0, 2)
if ss.p2pk != nil {
if info, err := client.GetMintInfo(ctx, target.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)
}
@@ -124,24 +91,47 @@ found:
}
// get new proofs
proofsToSend, changeProofs, err := w.SwapProofs(ctx, target.mint, target.proofs, amount, swapOpts...)
proofsToSend, changeProofs, err := w.SwapProofs(ctx, chosen.mint, chosen.proofs, amount, swapOpts...)
if err != nil {
return "", err
}
if err := w.saveChangeAndDeleteUsedTokens(ctx, chosen.mint, changeProofs, chosen.tokenIndexes); err != nil {
return "", err
}
// serialize token we're sending out
token, err := cashu.NewTokenV4(proofsToSend, chosen.mint, cashu.Sat, true)
if err != nil {
return "", err
}
wevt := nostr.Event{}
w.toEvent(ctx, w.wl.kr, &wevt)
w.wl.Changes <- wevt
return token.Serialize()
}
func (w *Wallet) saveChangeAndDeleteUsedTokens(
ctx context.Context,
mintURL string,
changeProofs cashu.Proofs,
usedTokenIndexes []int,
) error {
// delete spent tokens and save our change
updatedTokens := make([]Token, 0, len(w.Tokens))
changeToken := Token{
mintedAt: nostr.Now(),
Mint: target.mint,
Mint: mintURL,
Proofs: changeProofs,
Deleted: make([]string, 0, len(target.tokenIndexes)),
Deleted: make([]string, 0, len(usedTokenIndexes)),
event: &nostr.Event{},
}
for i, token := range w.Tokens {
if slices.Contains(target.tokenIndexes, i) {
if slices.Contains(usedTokenIndexes, i) {
if token.event != nil {
token.Deleted = append(token.Deleted, token.event.ID)
@@ -160,21 +150,51 @@ found:
if len(changeToken.Proofs) > 0 {
if err := changeToken.toEvent(ctx, w.wl.kr, w.Identifier, changeToken.event); err != nil {
return "", fmt.Errorf("failed to make change token: %w", err)
return fmt.Errorf("failed to make change token: %w", err)
}
w.wl.Changes <- *changeToken.event
w.Tokens = append(updatedTokens, changeToken)
}
// serialize token we're sending out
token, err := cashu.NewTokenV4(proofsToSend, target.mint, cashu.Sat, true)
if err != nil {
return "", err
return nil
}
func (w *Wallet) getProofsForSending(
ctx context.Context,
amount uint64,
specificMint string,
) (chosenTokens, uint64, error) {
byMint := make(map[string]chosenTokens)
for t, token := range w.Tokens {
if specificMint != "" && token.Mint != specificMint {
continue
}
part, ok := byMint[token.Mint]
if !ok {
keysets, err := client.GetAllKeysets(ctx, token.Mint)
if err != nil {
return chosenTokens{}, 0, fmt.Errorf("failed to get %s keysets: %w", token.Mint, err)
}
part.keysets = keysets
part.tokens = make([]Token, 0, 3)
part.tokenIndexes = make([]int, 0, 3)
part.proofs = make(cashu.Proofs, 0, 7)
}
part.tokens = append(part.tokens, token)
part.tokenIndexes = append(part.tokenIndexes, t)
part.proofs = append(part.proofs, token.Proofs...)
if part.proofs.Amount() >= amount {
// maybe we found it here
fee := calculateFee(part.proofs, part.keysets)
if part.proofs.Amount() >= (amount + fee) {
// yes, we did
return part, fee, nil
}
}
}
wevt := nostr.Event{}
w.toEvent(ctx, w.wl.kr, &wevt)
w.wl.Changes <- wevt
return token.Serialize()
// if we got here it's because we didn't get enough proofs from the same mint
return chosenTokens{}, 0, fmt.Errorf("not enough proofs found from the same mint")
}