mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-11-15 16:50:16 +01:00
nip60: wallet.PayBolt11()
This commit is contained in:
@@ -42,9 +42,9 @@ func lightningMeltMint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// now we start the melt-mint process in multiple attempts
|
// now we start the melt-mint process in multiple attempts
|
||||||
invoicePct := 0.99
|
invoicePct := uint64(99)
|
||||||
proofsAmount := proofs.Amount()
|
proofsAmount := proofs.Amount()
|
||||||
amount := float64(proofsAmount) * invoicePct
|
amount := proofsAmount * invoicePct / 100
|
||||||
fee := uint64(calculateFee(proofs, fromKeysets))
|
fee := uint64(calculateFee(proofs, fromKeysets))
|
||||||
var meltQuote string
|
var meltQuote string
|
||||||
var mintQuote string
|
var mintQuote string
|
||||||
@@ -65,14 +65,14 @@ func lightningMeltMint(
|
|||||||
Unit: cashu.Sat.String(),
|
Unit: cashu.Sat.String(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
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,
|
// 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 {
|
if meltResp.Amount+meltResp.FeeReserve+fee > proofsAmount {
|
||||||
invoicePct -= 0.01
|
invoicePct--
|
||||||
amount *= invoicePct
|
amount = proofsAmount * invoicePct / 100
|
||||||
} else {
|
} else {
|
||||||
meltQuote = meltResp.Quote
|
meltQuote = meltResp.Quote
|
||||||
mintQuote = mintResp.Quote
|
mintQuote = mintResp.Quote
|
||||||
@@ -124,7 +124,7 @@ inspectmeltstatusresponse:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if it got paid make proceed to get proofs
|
// 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)
|
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: %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) {
|
func (w *Wallet) SendToken(ctx context.Context, amount uint64, opts ...SendOption) (string, error) {
|
||||||
ss := &sendSettings{}
|
ss := &sendSettings{}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
@@ -51,56 +59,15 @@ func (w *Wallet) SendToken(ctx context.Context, amount uint64, opts ...SendOptio
|
|||||||
w.tokensMu.Lock()
|
w.tokensMu.Lock()
|
||||||
defer w.tokensMu.Unlock()
|
defer w.tokensMu.Unlock()
|
||||||
|
|
||||||
type part struct {
|
chosen, _, err := w.getProofsForSending(ctx, amount, ss.specificMint)
|
||||||
mint string
|
if err != nil {
|
||||||
tokens []Token
|
return "", err
|
||||||
tokenIndexes []int
|
|
||||||
proofs cashu.Proofs
|
|
||||||
keysets []nut02.Keyset
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
swapOpts := make([]SwapOption, 0, 2)
|
||||||
|
|
||||||
if ss.p2pk != nil {
|
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)
|
return "", fmt.Errorf("mint doesn't support p2pk: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,24 +91,47 @@ found:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get new proofs
|
// 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 {
|
if err != nil {
|
||||||
return "", err
|
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
|
// delete spent tokens and save our change
|
||||||
updatedTokens := make([]Token, 0, len(w.Tokens))
|
updatedTokens := make([]Token, 0, len(w.Tokens))
|
||||||
|
|
||||||
changeToken := Token{
|
changeToken := Token{
|
||||||
mintedAt: nostr.Now(),
|
mintedAt: nostr.Now(),
|
||||||
Mint: target.mint,
|
Mint: mintURL,
|
||||||
Proofs: changeProofs,
|
Proofs: changeProofs,
|
||||||
Deleted: make([]string, 0, len(target.tokenIndexes)),
|
Deleted: make([]string, 0, len(usedTokenIndexes)),
|
||||||
event: &nostr.Event{},
|
event: &nostr.Event{},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, token := range w.Tokens {
|
for i, token := range w.Tokens {
|
||||||
if slices.Contains(target.tokenIndexes, i) {
|
if slices.Contains(usedTokenIndexes, i) {
|
||||||
if token.event != nil {
|
if token.event != nil {
|
||||||
token.Deleted = append(token.Deleted, token.event.ID)
|
token.Deleted = append(token.Deleted, token.event.ID)
|
||||||
|
|
||||||
@@ -160,21 +150,51 @@ found:
|
|||||||
|
|
||||||
if len(changeToken.Proofs) > 0 {
|
if len(changeToken.Proofs) > 0 {
|
||||||
if err := changeToken.toEvent(ctx, w.wl.kr, w.Identifier, changeToken.event); err != nil {
|
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.wl.Changes <- *changeToken.event
|
||||||
w.Tokens = append(updatedTokens, changeToken)
|
w.Tokens = append(updatedTokens, changeToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
// serialize token we're sending out
|
return nil
|
||||||
token, err := cashu.NewTokenV4(proofsToSend, target.mint, cashu.Sat, true)
|
}
|
||||||
if err != nil {
|
|
||||||
return "", err
|
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{}
|
// if we got here it's because we didn't get enough proofs from the same mint
|
||||||
w.toEvent(ctx, w.wl.kr, &wevt)
|
return chosenTokens{}, 0, fmt.Errorf("not enough proofs found from the same mint")
|
||||||
w.wl.Changes <- wevt
|
|
||||||
|
|
||||||
return token.Serialize()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,18 +37,42 @@ func LoadStash(
|
|||||||
kr nostr.Keyer,
|
kr nostr.Keyer,
|
||||||
pool *nostr.SimplePool,
|
pool *nostr.SimplePool,
|
||||||
relays []string,
|
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 {
|
) *WalletStash {
|
||||||
pk, err := kr.GetPublicKey(ctx)
|
pk, err := kr.GetPublicKey(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kinds := []int{37375, 7375}
|
||||||
|
if withHistory {
|
||||||
|
kinds = append(kinds, 7375)
|
||||||
|
}
|
||||||
|
|
||||||
eoseChan := make(chan struct{})
|
eoseChan := make(chan struct{})
|
||||||
events := pool.SubManyNotifyEOSE(
|
events := pool.SubManyNotifyEOSE(
|
||||||
ctx,
|
ctx,
|
||||||
relays,
|
relays,
|
||||||
nostr.Filters{
|
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}},
|
{Kinds: []int{5}, Tags: nostr.TagMap{"k": []string{"7375"}}, Authors: []string{pk}},
|
||||||
},
|
},
|
||||||
eoseChan,
|
eoseChan,
|
||||||
|
|||||||
Reference in New Issue
Block a user