mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-03-17 21:32:56 +01:00
nip60: wallet.PayBolt11()
This commit is contained in:
parent
26597452c5
commit
7c04b497ec
@ -42,9 +42,9 @@ func lightningMeltMint(
|
||||
}
|
||||
|
||||
// now we start the melt-mint process in multiple attempts
|
||||
invoicePct := 0.99
|
||||
invoicePct := uint64(99)
|
||||
proofsAmount := proofs.Amount()
|
||||
amount := float64(proofsAmount) * invoicePct
|
||||
amount := proofsAmount * invoicePct / 100
|
||||
fee := uint64(calculateFee(proofs, fromKeysets))
|
||||
var meltQuote string
|
||||
var mintQuote string
|
||||
@ -65,14 +65,14 @@ func lightningMeltMint(
|
||||
Unit: cashu.Sat.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error requesting melt quote from %s: %w", from, err), nothingCanBeDone
|
||||
return nil, fmt.Errorf("error requesting melt quote from %s: %w", from, err), storeTokenFromSourceMint
|
||||
}
|
||||
|
||||
// if amount in proofs is less than amount asked from mint in melt request,
|
||||
// lower the amount for mint request (because of lighting fees?)
|
||||
// lower the amount for mint request (because of lighting fees)
|
||||
if meltResp.Amount+meltResp.FeeReserve+fee > proofsAmount {
|
||||
invoicePct -= 0.01
|
||||
amount *= invoicePct
|
||||
invoicePct--
|
||||
amount = proofsAmount * invoicePct / 100
|
||||
} else {
|
||||
meltQuote = meltResp.Quote
|
||||
mintQuote = mintResp.Quote
|
||||
@ -124,7 +124,7 @@ inspectmeltstatusresponse:
|
||||
}
|
||||
|
||||
// if it got paid make proceed to get proofs
|
||||
split := []uint64{1, 2, 3, 4}
|
||||
split := cashu.AmountSplit(amount)
|
||||
blindedMessages, secrets, rs, err := createBlindedMessages(split, keyset.Id, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating blinded messages: %v", err), manualActionRequired
|
||||
|
96
nip60/pay.go
Normal file
96
nip60/pay.go
Normal file
@ -0,0 +1,96 @@
|
||||
package nip60
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/elnosh/gonuts/cashu"
|
||||
"github.com/elnosh/gonuts/cashu/nuts/nut05"
|
||||
"github.com/nbd-wtf/go-nostr/nip60/client"
|
||||
)
|
||||
|
||||
func (w *Wallet) PayBolt11(ctx context.Context, invoice string, opts ...SendOption) (string, error) {
|
||||
ss := &sendSettings{}
|
||||
for _, opt := range opts {
|
||||
opt(ss)
|
||||
}
|
||||
|
||||
invoiceAmount, err := getSatoshisAmountFromBolt11(invoice)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
w.tokensMu.Lock()
|
||||
defer w.tokensMu.Unlock()
|
||||
|
||||
var chosen chosenTokens
|
||||
var meltQuote string
|
||||
var meltAmount uint64
|
||||
|
||||
invoicePct := uint64(99)
|
||||
for range 10 {
|
||||
amount := invoiceAmount * invoicePct / 100
|
||||
var fee uint64
|
||||
chosen, fee, err = w.getProofsForSending(ctx, amount, ss.specificMint)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// request _melt_ quote (ask the mint how much will it cost to pay a bolt11 invoice)
|
||||
meltResp, err := client.PostMeltQuoteBolt11(ctx, chosen.mint, nut05.PostMeltQuoteBolt11Request{
|
||||
Request: invoice,
|
||||
Unit: cashu.Sat.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error requesting melt quote from %s: %w", chosen.mint, err)
|
||||
}
|
||||
|
||||
// if amount in proofs is not sufficient to pay for the melt request,
|
||||
// increase the amount and get proofs again (because of lighting fees)
|
||||
if meltResp.Amount+meltResp.FeeReserve+fee > chosen.proofs.Amount() {
|
||||
invoicePct--
|
||||
} else {
|
||||
meltQuote = meltResp.Quote
|
||||
meltAmount = meltResp.Amount
|
||||
goto meltworked
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("stop trying to do the melt because the mint part is too expensive")
|
||||
|
||||
meltworked:
|
||||
// swap our proofs so we get the exact amount for paying the invoice
|
||||
principal, change, err := w.SwapProofs(ctx, chosen.mint, chosen.proofs, meltAmount)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to swap at %s into the exact melt amount: %w", chosen.mint, err)
|
||||
}
|
||||
|
||||
if err := w.saveChangeAndDeleteUsedTokens(ctx, chosen.mint, change, chosen.tokenIndexes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// request from mint to _melt_ into paying the invoice
|
||||
delay := 200 * time.Millisecond
|
||||
// this request will block until the invoice is paid or it fails
|
||||
// (but the API also says it can return "pending" so we handle both)
|
||||
meltStatus, err := client.PostMeltBolt11(ctx, chosen.mint, nut05.PostMeltBolt11Request{
|
||||
Quote: meltQuote,
|
||||
Inputs: principal,
|
||||
})
|
||||
inspectmeltstatusresponse:
|
||||
if err != nil || meltStatus.State == nut05.Unpaid {
|
||||
return "", fmt.Errorf("error melting token: %w", err)
|
||||
} else if meltStatus.State == nut05.Unknown {
|
||||
return "", fmt.Errorf("we don't know what happened with the melt at %s: %v", chosen.mint, meltStatus)
|
||||
} else if meltStatus.State == nut05.Pending {
|
||||
for {
|
||||
time.Sleep(delay)
|
||||
delay *= 2
|
||||
meltStatus, err = client.GetMeltQuoteState(ctx, chosen.mint, meltStatus.Quote)
|
||||
goto inspectmeltstatusresponse
|
||||
}
|
||||
}
|
||||
|
||||
return meltStatus.Preimage, nil
|
||||
}
|
138
nip60/send.go
138
nip60/send.go
@ -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")
|
||||
}
|
||||
|
@ -37,18 +37,42 @@ func LoadStash(
|
||||
kr nostr.Keyer,
|
||||
pool *nostr.SimplePool,
|
||||
relays []string,
|
||||
) *WalletStash {
|
||||
return loadStashFromPool(ctx, kr, pool, relays, false)
|
||||
}
|
||||
|
||||
func LoadStashWithHistory(
|
||||
ctx context.Context,
|
||||
kr nostr.Keyer,
|
||||
pool *nostr.SimplePool,
|
||||
relays []string,
|
||||
) *WalletStash {
|
||||
return loadStashFromPool(ctx, kr, pool, relays, true)
|
||||
}
|
||||
|
||||
func loadStashFromPool(
|
||||
ctx context.Context,
|
||||
kr nostr.Keyer,
|
||||
pool *nostr.SimplePool,
|
||||
relays []string,
|
||||
withHistory bool,
|
||||
) *WalletStash {
|
||||
pk, err := kr.GetPublicKey(ctx)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
kinds := []int{37375, 7375}
|
||||
if withHistory {
|
||||
kinds = append(kinds, 7375)
|
||||
}
|
||||
|
||||
eoseChan := make(chan struct{})
|
||||
events := pool.SubManyNotifyEOSE(
|
||||
ctx,
|
||||
relays,
|
||||
nostr.Filters{
|
||||
{Kinds: []int{37375, 7375, 7376}, Authors: []string{pk}},
|
||||
{Kinds: kinds, Authors: []string{pk}},
|
||||
{Kinds: []int{5}, Tags: nostr.TagMap{"k": []string{"7375"}}, Authors: []string{pk}},
|
||||
},
|
||||
eoseChan,
|
||||
|
Loading…
x
Reference in New Issue
Block a user