Skip to content

Commit

Permalink
wallet and cli commands for pending proofs and quotes
Browse files Browse the repository at this point in the history
  • Loading branch information
elnosh committed Oct 10, 2024
1 parent c82756b commit 440390b
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 3 deletions.
58 changes: 58 additions & 0 deletions cmd/nutw/nutw.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ func main() {
sendCmd,
receiveCmd,
payCmd,
quotesCmd,
p2pkLockCmd,
mnemonicCmd,
restoreCmd,
Expand Down Expand Up @@ -144,6 +145,11 @@ func getBalance(ctx *cli.Context) error {
}

fmt.Printf("\nTotal balance: %v sats\n", totalBalance)

pendingBalance := nutw.PendingBalance()
if pendingBalance > 0 {
fmt.Printf("Pending balance: %v sats\n", pendingBalance)
}
return nil
}

Expand Down Expand Up @@ -416,6 +422,58 @@ func pay(ctx *cli.Context) error {
return nil
}

const (
checkFlag = "check"
)

var quotesCmd = &cli.Command{
Name: "quotes",
Usage: "list and check status of pending melt quotes",
Before: setupWallet,
Flags: []cli.Flag{
&cli.StringFlag{
Name: checkFlag,
Usage: "check state of quote",
},
},
Action: quotes,
}

func quotes(ctx *cli.Context) error {
pendingQuotes := nutw.GetPendingMeltQuotes()

if ctx.IsSet(checkFlag) {
quote := ctx.String(checkFlag)

quoteResponse, err := nutw.CheckMeltQuoteState(quote)
if err != nil {
printErr(err)
}

switch quoteResponse.State {
case nut05.Paid:
fmt.Printf("Invoice for quote '%v' was paid. Preimage: %v\n", quote, quoteResponse.Preimage)
case nut05.Pending:
fmt.Println("payment is still pending")
case nut05.Unpaid:
fmt.Println("quote was not paid")
}

return nil
}

if len(pendingQuotes) > 0 {
fmt.Println("Pending quotes: ")
for _, quote := range pendingQuotes {
fmt.Printf("ID: %v\n", quote)
}
} else {
fmt.Println("no pending quotes")
}

return nil
}

var p2pkLockCmd = &cli.Command{
Name: "p2pk-lock",
Usage: "Retrieves a public key to which ecash can be locked",
Expand Down
4 changes: 4 additions & 0 deletions mint/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package mint

import (
"time"

"github.com/elnosh/gonuts/cashu/nuts/nut06"
"github.com/elnosh/gonuts/mint/lightning"
)
Expand All @@ -23,6 +25,8 @@ type Config struct {
Limits MintLimits
LightningClient lightning.Client
LogLevel LogLevel
// NOTE: using this value for testing
MeltTimeout *time.Duration
}

type MintInfo struct {
Expand Down
10 changes: 8 additions & 2 deletions mint/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (
type MintServer struct {
httpServer *http.Server
mint *Mint
// NOTE: using this value for testing
meltTimeout *time.Duration
}

func (ms *MintServer) Start() error {
Expand All @@ -46,7 +48,7 @@ func SetupMintServer(config Config) (*MintServer, error) {
return nil, err
}

mintServer := &MintServer{mint: mint}
mintServer := &MintServer{mint: mint, meltTimeout: config.MeltTimeout}
err = mintServer.setupHttpServer(config.Port)
if err != nil {
return nil, err
Expand Down Expand Up @@ -449,7 +451,11 @@ func (ms *MintServer) meltTokens(rw http.ResponseWriter, req *http.Request) {
return
}

ctx, cancel := context.WithTimeout(context.Background(), time.Minute*1)
timeout := time.Minute * 1
if ms.meltTimeout != nil {
timeout = *ms.meltTimeout
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

meltQuote, err := ms.mint.MeltTokens(ctx, method, meltTokensRequest.Quote, meltTokensRequest.Inputs)
Expand Down
2 changes: 2 additions & 0 deletions testutils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ func mintConfig(
return nil, fmt.Errorf("error setting LND client: %v", err)
}

timeout := time.Second * 2
mintConfig := &mint.Config{
DerivationPathIdx: 0,
Port: port,
Expand All @@ -247,6 +248,7 @@ func mintConfig(
Limits: limits,
LightningClient: lndClient,
LogLevel: mint.Disable,
MeltTimeout: &timeout,
}

return mintConfig, nil
Expand Down
34 changes: 33 additions & 1 deletion wallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,18 @@ func (w *Wallet) GetBalanceByMints() map[string]uint64 {
return mintsBalances
}

func (w *Wallet) PendingBalance() uint64 {
return Amount(w.db.GetPendingProofs())
}

func Amount(proofs []storage.DBProof) uint64 {
var totalAmount uint64 = 0
for _, proof := range proofs {
totalAmount += proof.Amount
}
return totalAmount
}

// RequestMint requests a mint quote to the wallet's current mint
// for the specified amount
func (w *Wallet) RequestMint(amount uint64) (*nut04.PostMintQuoteBolt11Response, error) {
Expand Down Expand Up @@ -756,7 +768,6 @@ func (w *Wallet) CheckMeltQuoteState(quoteId string) (*nut05.PostMeltQuoteBolt11
return nil, fmt.Errorf("error storing proofs: %v", err)
}
}

}
}

Expand Down Expand Up @@ -1805,6 +1816,27 @@ func Restore(walletPath, mnemonic string, mintsToRestore []string) (cashu.Proofs
return proofsRestored, nil
}

func (w *Wallet) GetPendingProofs() []storage.DBProof {
return w.db.GetPendingProofs()
}

// GetPendingMeltQuotes return a list of pending quote ids
func (w *Wallet) GetPendingMeltQuotes() []string {
pendingProofs := w.db.GetPendingProofs()
pendingProofsMap := make(map[string][]storage.DBProof)
for _, proof := range pendingProofs {
pendingProofsMap[proof.MeltQuoteId] = append(pendingProofsMap[proof.MeltQuoteId], proof)
}

pendingQuotes := make([]string, len(pendingProofsMap))
i := 0
for quote, _ := range pendingProofsMap {
pendingQuotes[i] = quote
i++
}
return pendingQuotes
}

func (w *Wallet) GetInvoiceByPaymentRequest(pr string) (*storage.Invoice, error) {
bolt11, err := decodepay.Decodepay(pr)
if err != nil {
Expand Down
158 changes: 158 additions & 0 deletions wallet/wallet_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package wallet_test

import (
"context"
"crypto/sha256"
"errors"
"flag"
"log"
Expand All @@ -19,6 +20,7 @@ import (
"github.com/elnosh/gonuts/testutils"
"github.com/elnosh/gonuts/wallet"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
)

var (
Expand Down Expand Up @@ -600,6 +602,162 @@ func TestWalletBalanceFees(t *testing.T) {
}
}

func TestPendingProofs(t *testing.T) {
mintURL := "http://127.0.0.1:3338"
testWalletPath := filepath.Join(".", "/testpendingwallet")
testWallet, err := testutils.CreateTestWallet(testWalletPath, mintURL)
if err != nil {
t.Fatal(err)
}
defer func() {
os.RemoveAll(testWalletPath)
}()

var fundingBalance uint64 = 15000
if err := testutils.FundCashuWallet(ctx, testWallet, lnd2, fundingBalance); err != nil {
t.Fatalf("error funding wallet: %v", err)
}

// use hodl invoice to cause melt to get stuck in pending
preimage, _ := testutils.GenerateRandomBytes()
hash := sha256.Sum256(preimage)
hodlInvoice := invoicesrpc.AddHoldInvoiceRequest{Hash: hash[:], Value: 2100}
addHodlInvoiceRes, err := lnd2.InvoicesClient.AddHoldInvoice(ctx, &hodlInvoice)
if err != nil {
t.Fatalf("error creating hodl invoice: %v", err)
}

meltQuote, err := testWallet.Melt(addHodlInvoiceRes.PaymentRequest, testWallet.CurrentMint())
if err != nil {
t.Fatalf("unexpected error in melt: %v", err)
}
if meltQuote.State != nut05.Pending {
t.Fatalf("expected quote state of '%s' but got '%s' instead", nut05.Pending, meltQuote.State)
}

// check amount of pending proofs is same as quote amount
pendingProofsAmount := wallet.Amount(testWallet.GetPendingProofs())
expectedAmount := meltQuote.Amount + meltQuote.FeeReserve
if pendingProofsAmount != expectedAmount {
t.Fatalf("expected amount of pending proofs of '%v' but got '%v' instead",
expectedAmount, pendingProofsAmount)
}
pendingBalance := testWallet.PendingBalance()
expectedPendingBalance := meltQuote.Amount + meltQuote.FeeReserve
if pendingBalance != expectedPendingBalance {
t.Fatalf("expected pending balance of '%v' but got '%v' instead",
expectedPendingBalance, pendingBalance)
}

// there should be 1 pending quote
pendingMeltQuotes := testWallet.GetPendingMeltQuotes()
if len(pendingMeltQuotes) != 1 {
t.Fatalf("expected '%v' pending quote but got '%v' instead", 1, len(pendingMeltQuotes))
}
if pendingMeltQuotes[0] != meltQuote.Quote {
t.Fatalf("expected pending quote with id '%v' but got '%v' instead",
meltQuote.Quote, pendingMeltQuotes[0])
}

// settle hodl invoice and test that there are no pending proofs now
settleHodlInvoice := invoicesrpc.SettleInvoiceMsg{Preimage: preimage}
_, err = lnd2.InvoicesClient.SettleInvoice(ctx, &settleHodlInvoice)
if err != nil {
t.Fatalf("error settling hodl invoice: %v", err)
}

meltQuoteStateResponse, err := testWallet.CheckMeltQuoteState(meltQuote.Quote)
if err != nil {
t.Fatalf("unexpected error checking melt quote state: %v", err)
}
if meltQuoteStateResponse.State != nut05.Paid {
t.Fatalf("expected quote state of '%s' but got '%s' instead",
nut05.Paid, meltQuoteStateResponse.State)
}

// check no pending proofs or pending balance after settling and checking melt quote state
pendingProofsAmount = wallet.Amount(testWallet.GetPendingProofs())
if pendingProofsAmount != 0 {
t.Fatalf("expected no pending proofs amount but got '%v' instead", pendingProofsAmount)
}
pendingBalance = testWallet.PendingBalance()
if pendingBalance != 0 {
t.Fatalf("expected no pending balance but got '%v' instead", pendingBalance)
}

// check no pending melt quotes
pendingMeltQuotes = testWallet.GetPendingMeltQuotes()
if len(pendingMeltQuotes) != 0 {
t.Fatalf("expected no pending quotes but got '%v' instead", len(pendingMeltQuotes))
}

// test hodl invoice to cause melt to get stuck in pending and then cancel it
preimage, _ = testutils.GenerateRandomBytes()
hash = sha256.Sum256(preimage)
hodlInvoice = invoicesrpc.AddHoldInvoiceRequest{Hash: hash[:], Value: 2100}
addHodlInvoiceRes, err = lnd2.InvoicesClient.AddHoldInvoice(ctx, &hodlInvoice)
if err != nil {
t.Fatalf("error creating hodl invoice: %v", err)
}

meltQuote, err = testWallet.Melt(addHodlInvoiceRes.PaymentRequest, testWallet.CurrentMint())
if err != nil {
t.Fatalf("unexpected error in melt: %v", err)
}
if meltQuote.State != nut05.Pending {
t.Fatalf("expected quote state of '%s' but got '%s' instead", nut05.Pending, meltQuote.State)
}
pendingProofsAmount = wallet.Amount(testWallet.GetPendingProofs())
expectedAmount = meltQuote.Amount + meltQuote.FeeReserve
if pendingProofsAmount != expectedAmount {
t.Fatalf("expected amount of pending proofs of '%v' but got '%v' instead",
expectedAmount, pendingProofsAmount)
}
pendingMeltQuotes = testWallet.GetPendingMeltQuotes()
if len(pendingMeltQuotes) != 1 {
t.Fatalf("expected '%v' pending quote but got '%v' instead", 1, len(pendingMeltQuotes))
}

cancelInvoice := invoicesrpc.CancelInvoiceMsg{PaymentHash: hash[:]}
_, err = lnd2.InvoicesClient.CancelInvoice(ctx, &cancelInvoice)
if err != nil {
t.Fatalf("error canceling hodl invoice: %v", err)
}

meltQuoteStateResponse, err = testWallet.CheckMeltQuoteState(meltQuote.Quote)
if err != nil {
t.Fatalf("unexpected error checking melt quote state: %v", err)
}
if meltQuoteStateResponse.State != nut05.Unpaid {
t.Fatalf("expected quote state of '%s' but got '%s' instead",
nut05.Unpaid, meltQuoteStateResponse.State)
}

// check no pending proofs or pending balance after canceling and checking melt quote state
pendingProofsAmount = wallet.Amount(testWallet.GetPendingProofs())
if pendingProofsAmount != 0 {
t.Fatalf("expected no pending proofs amount but got '%v' instead", pendingProofsAmount)
}
pendingBalance = testWallet.PendingBalance()
if pendingBalance != 0 {
t.Fatalf("expected no pending balance but got '%v' instead", pendingBalance)
}
// check no pending melt quotes
pendingMeltQuotes = testWallet.GetPendingMeltQuotes()
if len(pendingMeltQuotes) != 0 {
t.Fatalf("expected no pending quotes but got '%v' instead", len(pendingMeltQuotes))
}

// check proofs that were pending were added back to wallet balance
// so wallet balance at this point should be fundingWalletAmount - firstSuccessfulMeltAmount
walletBalance := testWallet.GetBalance()
expectedWalletBalance := fundingBalance - meltQuote.Amount - meltQuote.FeeReserve
if walletBalance != expectedWalletBalance {
t.Fatalf("expected wallet balance of '%v' but got '%v' instead",
expectedWalletBalance, walletBalance)
}
}

func TestWalletRestore(t *testing.T) {
mintURL := "http://127.0.0.1:3338"

Expand Down

0 comments on commit 440390b

Please sign in to comment.