diff --git a/cmd/nutw/nutw.go b/cmd/nutw/nutw.go index e21a325..4aefe20 100644 --- a/cmd/nutw/nutw.go +++ b/cmd/nutw/nutw.go @@ -266,7 +266,7 @@ func requestMint(amountStr string) error { return errors.New("invalid amount") } - mintResponse, err := nutw.RequestMint(amount) + mintResponse, err := nutw.RequestMint(amount, nutw.CurrentMint()) if err != nil { return err } diff --git a/testutils/utils.go b/testutils/utils.go index f76d4be..39b91a6 100644 --- a/testutils/utils.go +++ b/testutils/utils.go @@ -165,7 +165,7 @@ func CreateTestWallet(walletpath, defaultMint string) (*wallet.Wallet, error) { } func FundCashuWallet(ctx context.Context, wallet *wallet.Wallet, lnd *btcdocker.Lnd, amount uint64) error { - mintRes, err := wallet.RequestMint(amount) + mintRes, err := wallet.RequestMint(amount, wallet.CurrentMint()) if err != nil { return fmt.Errorf("error requesting mint: %v", err) } diff --git a/wallet/examples/wallet.go b/wallet/examples/wallet.go index 98e80bd..80c79e8 100644 --- a/wallet/examples/wallet.go +++ b/wallet/examples/wallet.go @@ -20,7 +20,7 @@ func main() { wallet, err := wallet.LoadWallet(config) // Mint tokens - mintQuote, err := wallet.RequestMint(42) + mintQuote, err := wallet.RequestMint(42, wallet.CurrentMint()) // Check quote state quoteState, err := wallet.MintQuoteState(mintQuote.Quote) @@ -34,7 +34,7 @@ func main() { includeFees := true includeDLEQProof := false proofsToSend, err := wallet.Send(21, mint, includeFees) - token, err := cashu.NewTokenV4(proofsToSend, mint, "sat", includeDLEQProof) + token, err := cashu.NewTokenV4(proofsToSend, mint, cashu.Sat, includeDLEQProof) fmt.Println(token.Serialize()) // Receive diff --git a/wallet/wallet.go b/wallet/wallet.go index f58acd9..794084c 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -122,7 +122,7 @@ func LoadWallet(config Config) (*Wallet, error) { // if mint is new, add it walletMint, ok := wallet.mints[mintURL] if !ok { - mint, err := wallet.addMint(mintURL) + mint, err := wallet.AddMint(mintURL) if err != nil { return nil, fmt.Errorf("error adding new mint: %v", err) } @@ -198,8 +198,8 @@ func mintInfo(mintURL string) (*walletMint, error) { return &walletMint{mintURL, activeKeysets, inactiveKeysets}, nil } -// addMint adds the mint to the list of mints trusted by the wallet -func (w *Wallet) addMint(mint string) (*walletMint, error) { +// AddMint adds the mint to the list of mints trusted by the wallet +func (w *Wallet) AddMint(mint string) (*walletMint, error) { url, err := url.Parse(mint) if err != nil { return nil, fmt.Errorf("invalid mint url: %v", err) @@ -334,9 +334,14 @@ func amount(proofs []storage.DBProof) uint64 { // RequestMint requests a mint quote to the wallet's current mint // for the specified amount -func (w *Wallet) RequestMint(amount uint64) (*nut04.PostMintQuoteBolt11Response, error) { +func (w *Wallet) RequestMint(amount uint64, mint string) (*nut04.PostMintQuoteBolt11Response, error) { + selectedMint, ok := w.mints[mint] + if !ok { + return nil, ErrMintNotExist + } + mintRequest := nut04.PostMintQuoteBolt11Request{Amount: amount, Unit: cashu.Sat.String()} - mintResponse, err := PostMintQuoteBolt11(w.currentMint.mintURL, mintRequest) + mintResponse, err := PostMintQuoteBolt11(selectedMint.mintURL, mintRequest) if err != nil { return nil, err } @@ -350,6 +355,7 @@ func (w *Wallet) RequestMint(amount uint64) (*nut04.PostMintQuoteBolt11Response, TransactionType: storage.Mint, QuoteAmount: amount, Id: mintResponse.Quote, + Mint: selectedMint.mintURL, PaymentRequest: mintResponse.Request, PaymentHash: bolt11.PaymentHash, CreatedAt: int64(bolt11.CreatedAt), @@ -366,7 +372,16 @@ func (w *Wallet) RequestMint(amount uint64) (*nut04.PostMintQuoteBolt11Response, } func (w *Wallet) MintQuoteState(quoteId string) (*nut04.PostMintQuoteBolt11Response, error) { - return GetMintQuoteState(w.currentMint.mintURL, quoteId) + invoice := w.db.GetInvoiceByQuoteId(quoteId) + if invoice == nil { + return nil, ErrQuoteNotFound + } + + mint := invoice.Mint + if len(invoice.Mint) == 0 { + mint = w.currentMint.mintURL + } + return GetMintQuoteState(mint, quoteId) } // MintTokens will check whether if the mint quote has been paid. @@ -375,7 +390,16 @@ func (w *Wallet) MintQuoteState(quoteId string) (*nut04.PostMintQuoteBolt11Respo // If successful, it will unblind the signatures to generate proofs // and store the proofs in the db. func (w *Wallet) MintTokens(quoteId string) (uint64, error) { - mintQuote, err := w.MintQuoteState(quoteId) + invoice := w.db.GetInvoiceByQuoteId(quoteId) + if invoice == nil { + return 0, ErrQuoteNotFound + } + mint := invoice.Mint + if len(invoice.Mint) == 0 { + mint = w.currentMint.mintURL + } + + mintQuote, err := GetMintQuoteState(mint, quoteId) if err != nil { return 0, err } @@ -387,22 +411,14 @@ func (w *Wallet) MintTokens(quoteId string) (uint64, error) { return 0, errors.New("invoice not paid") } - invoice, err := w.GetInvoiceByPaymentRequest(mintQuote.Request) - if err != nil { - return 0, err - } - if invoice == nil { - return 0, errors.New("invoice not found") - } - - activeKeyset, err := w.getActiveSatKeyset(w.currentMint.mintURL) + activeKeyset, err := w.getActiveSatKeyset(mint) if err != nil { return 0, fmt.Errorf("error getting active sat keyset: %v", err) } // get counter for keyset counter := w.counterForKeyset(activeKeyset.Id) - split := w.splitWalletTarget(invoice.QuoteAmount, w.currentMint.mintURL) + split := w.splitWalletTarget(invoice.QuoteAmount, mint) blindedMessages, secrets, rs, err := w.createBlindedMessages(split, activeKeyset.Id, &counter) if err != nil { return 0, fmt.Errorf("error creating blinded messages: %v", err) @@ -410,7 +426,7 @@ func (w *Wallet) MintTokens(quoteId string) (uint64, error) { // request mint to sign the blinded messages postMintRequest := nut04.PostMintBolt11Request{Quote: quoteId, Outputs: blindedMessages} - mintResponse, err := PostMintBolt11(w.currentMint.mintURL, postMintRequest) + mintResponse, err := PostMintBolt11(mint, postMintRequest) if err != nil { return 0, err } @@ -592,7 +608,7 @@ func (w *Wallet) Receive(token cashu.Token, swapToTrusted bool) (uint64, error) // only add mint if not previously trusted _, ok := w.mints[tokenMint] if !ok { - _, err := w.addMint(tokenMint) + _, err := w.AddMint(tokenMint) if err != nil { return 0, err } @@ -654,7 +670,7 @@ func (w *Wallet) ReceiveHTLC(token cashu.Token, preimage string) (uint64, error) // only add mint if not previously trusted _, ok := w.mints[tokenMint] if !ok { - _, err := w.addMint(tokenMint) + _, err := w.AddMint(tokenMint) if err != nil { return 0, err } @@ -796,57 +812,12 @@ func (w *Wallet) swapToTrusted(proofs cashu.Proofs, mintFromProofs string) (uint proofsToSwap = newProofs } - var mintResponse *nut04.PostMintQuoteBolt11Response - var meltQuoteResponse *nut05.PostMeltQuoteBolt11Response - invoicePct := 0.99 - proofsAmount := proofsToSwap.Amount() - amount := float64(proofsAmount) * invoicePct - fees := uint64(feesForProofs(proofsToSwap, mint)) - for { - // request a mint quote from the configured default mint - // this will generate an invoice from the trusted mint - mintAmountRequest := uint64(amount) - fees - mintResponse, err = w.RequestMint(mintAmountRequest) - if err != nil { - return 0, fmt.Errorf("error requesting mint: %v", err) - } - - // request melt quote from untrusted mint which will - // request mint to pay invoice generated from trusted mint in previous mint request - meltRequest := nut05.PostMeltQuoteBolt11Request{Request: mintResponse.Request, Unit: cashu.Sat.String()} - meltQuoteResponse, err = PostMeltQuoteBolt11(mintFromProofs, meltRequest) - if err != nil { - return 0, fmt.Errorf("error with melt request: %v", err) - } - - // if amount in proofs is less than amount asked from mint in melt request, - // lower the amount for mint request - if meltQuoteResponse.Amount+meltQuoteResponse.FeeReserve+fees > proofsAmount { - invoicePct -= 0.01 - amount *= invoicePct - } else { - break - } - } - - // request untrusted mint to pay invoice generated from trusted mint - meltBolt11Request := nut05.PostMeltBolt11Request{Quote: meltQuoteResponse.Quote, Inputs: proofsToSwap} - meltBolt11Response, err := PostMeltBolt11(mintFromProofs, meltBolt11Request) + amountSwapped, err := w.swapProofs(proofsToSwap, mint, w.currentMint) if err != nil { - return 0, fmt.Errorf("error melting token: %v", err) + return 0, err } - // if melt request was successful and untrusted mint paid the invoice, - // make mint request to trusted mint to get valid proofs - if meltBolt11Response.Paid { - mintedAmount, err := w.MintTokens(mintResponse.Quote) - if err != nil { - return 0, fmt.Errorf("error minting tokens: %v", err) - } - return mintedAmount, nil - } else { - return 0, errors.New("mint could not pay lightning invoice") - } + return amountSwapped, nil } func (w *Wallet) CheckMeltQuoteState(quoteId string) (*nut05.PostMeltQuoteBolt11Response, error) { @@ -1057,6 +1028,89 @@ func (w *Wallet) Melt(invoice, mintURL string) (*nut05.PostMeltQuoteBolt11Respon return meltBolt11Response, err } +// MintSwap will swap the amount from to the specified mint +func (w *Wallet) MintSwap(amount uint64, from, to string) (uint64, error) { + // check both mints are in list of trusted mints + fromMint, fromOk := w.mints[from] + toMint, toOk := w.mints[to] + if !fromOk || !toOk { + return 0, ErrMintNotExist + } + + balanceByMints := w.GetBalanceByMints() + if balanceByMints[from] < amount { + return 0, ErrInsufficientMintBalance + } + + proofsToSwap, err := w.getProofsForAmount(amount, &fromMint, true) + if err != nil { + return 0, err + } + + amountSwapped, err := w.swapProofs(proofsToSwap, &fromMint, &toMint) + if err != nil { + return 0, err + } + + return amountSwapped, nil +} + +// swapProofs will swap the proofs in the from mint to specified mint +func (w *Wallet) swapProofs(proofs cashu.Proofs, from, to *walletMint) (uint64, error) { + var mintResponse *nut04.PostMintQuoteBolt11Response + var meltQuoteResponse *nut05.PostMeltQuoteBolt11Response + invoicePct := 0.99 + proofsAmount := proofs.Amount() + amount := float64(proofsAmount) * invoicePct + fees := uint64(feesForProofs(proofs, from)) + for { + // request mint quote to the 'to' mint + // this will generate an invoice + mintAmountRequest := uint64(amount) - fees + var err error + mintResponse, err = w.RequestMint(mintAmountRequest, to.mintURL) + if err != nil { + return 0, fmt.Errorf("error requesting mint quote: %v", err) + } + + // request melt quote from the 'from' mint + // this melt will pay the invoice generated from the previous mint quote request + meltRequest := nut05.PostMeltQuoteBolt11Request{Request: mintResponse.Request, Unit: cashu.Sat.String()} + meltQuoteResponse, err = PostMeltQuoteBolt11(from.mintURL, meltRequest) + if err != nil { + return 0, fmt.Errorf("error with melt request: %v", err) + } + + // if amount in proofs is less than amount asked from mint in melt request, + // lower the amount for mint request + if meltQuoteResponse.Amount+meltQuoteResponse.FeeReserve+fees > proofsAmount { + invoicePct -= 0.01 + amount *= invoicePct + } else { + break + } + } + + // request from mint to pay invoice from the mint quote request + meltBolt11Request := nut05.PostMeltBolt11Request{Quote: meltQuoteResponse.Quote, Inputs: proofs} + meltBolt11Response, err := PostMeltBolt11(from.mintURL, meltBolt11Request) + if err != nil { + return 0, fmt.Errorf("error melting token: %v", err) + } + + // if melt request was successful and invoice got paid, + // make mint request to get valid proofs + if meltBolt11Response.Paid { + mintedAmount, err := w.MintTokens(mintResponse.Quote) + if err != nil { + return 0, fmt.Errorf("error minting tokens: %v", err) + } + return mintedAmount, nil + } else { + return 0, errors.New("mint could not pay lightning invoice") + } +} + func (w *Wallet) getProofsFromMint(mintURL string) cashu.Proofs { proofs := w.getInactiveProofsByMint(mintURL) proofs = append(proofs, w.getActiveProofsByMint(mintURL)...) diff --git a/wallet/wallet_integration_test.go b/wallet/wallet_integration_test.go index 7208ff3..b5d0b57 100644 --- a/wallet/wallet_integration_test.go +++ b/wallet/wallet_integration_test.go @@ -131,7 +131,7 @@ func TestMintTokens(t *testing.T) { var mintAmount uint64 = 30000 // check no err - mintRes, err := testWallet.RequestMint(mintAmount) + mintRes, err := testWallet.RequestMint(mintAmount, testWallet.CurrentMint()) if err != nil { t.Fatalf("error requesting mint: %v", err) } @@ -456,6 +456,68 @@ func TestMelt(t *testing.T) { } } +func TestMintSwap(t *testing.T) { + mint2URL := "http://127.0.0.1:8081" + testMintPath := filepath.Join(".", "testmint2") + testMint, err := testutils.CreateTestMintServer(lnd2, "8081", testMintPath, dbMigrationPath, 0) + if err != nil { + t.Fatal(err) + } + defer func() { + os.RemoveAll(testMintPath) + }() + go func() { + t.Fatal(testMint.Start()) + }() + + mintURL := "http://127.0.0.1:3338" + testWalletPath := filepath.Join(".", "/testmintswapwallet") + testWallet, err := testutils.CreateTestWallet(testWalletPath, mintURL) + if err != nil { + t.Fatal(err) + } + defer func() { + os.RemoveAll(testWalletPath) + }() + + var amountToSwap uint64 = 1000 + _, err = testWallet.MintSwap(amountToSwap, testWallet.CurrentMint(), mint2URL) + if !errors.Is(err, wallet.ErrMintNotExist) { + t.Fatalf("expected error '%v' but got error '%v'", wallet.ErrMintNotExist, err) + } + + _, err = testWallet.AddMint(mint2URL) + if err != nil { + t.Fatalf("unexpected error adding mint to wallet: %v", err) + } + + _, err = testWallet.MintSwap(amountToSwap, testWallet.CurrentMint(), mint2URL) + if !errors.Is(err, wallet.ErrInsufficientMintBalance) { + t.Fatalf("expected error '%v' but got error '%v'", wallet.ErrInsufficientMintBalance, err) + } + + var fundAmount uint64 = 21000 + if err := testutils.FundCashuWallet(ctx, testWallet, lnd2, fundAmount); err != nil { + t.Fatalf("error funding wallet: %v", err) + } + amountSwapped, err := testWallet.MintSwap(amountToSwap, testWallet.CurrentMint(), mint2URL) + if err != nil { + t.Fatalf("unexpected error doing mint swap: %v", err) + } + + balanceByMints := testWallet.GetBalanceByMints() + mint1Balance := balanceByMints[testWallet.CurrentMint()] + expectedBalance := fundAmount - amountToSwap + if mint1Balance != expectedBalance { + t.Fatalf("expected balance '%v' but got '%v'", expectedBalance, mint1Balance) + } + + mint2Balance := balanceByMints[mint2URL] + if mint2Balance != amountSwapped { + t.Fatalf("expected balance '%v' but got '%v'", amountSwapped, mint2Balance) + } +} + // check balance is correct after certain operations func TestWalletBalance(t *testing.T) { mintURL := "http://127.0.0.1:3338" @@ -470,7 +532,7 @@ func TestWalletBalance(t *testing.T) { // test balance after mint request var mintAmount uint64 = 20000 - mintRequest, err := balanceTestWallet.RequestMint(mintAmount) + mintRequest, err := balanceTestWallet.RequestMint(mintAmount, balanceTestWallet.CurrentMint()) if err != nil { t.Fatalf("unexpected error in mint request: %v", err) } @@ -780,7 +842,7 @@ func testWalletRestore( mintURL := testWallet.CurrentMint() var mintAmount uint64 = 20000 - mintRequest, err := testWallet.RequestMint(mintAmount) + mintRequest, err := testWallet.RequestMint(mintAmount, testWallet.CurrentMint()) if err != nil { t.Fatalf("unexpected error in mint request: %v", err) } @@ -874,7 +936,7 @@ func TestHTLC(t *testing.T) { }() if err := testutils.FundCashuWallet(ctx, testWallet, lnd2, 30000); err != nil { - t.Fatalf("error funding wallet") + t.Fatalf("error funding wallet: %v", err) } preimage := "aaaaaa" @@ -971,7 +1033,7 @@ func testP2PK( testWallet2 *wallet.Wallet, fakeBackend bool, ) { - mintRequest, err := testWallet.RequestMint(20000) + mintRequest, err := testWallet.RequestMint(20000, testWallet.CurrentMint()) if err != nil { t.Fatalf("unexpected error in mint request: %v", err) } @@ -1060,7 +1122,7 @@ func testDLEQ(t *testing.T, testWallet *wallet.Wallet, fakeBackend bool) { t.Fatalf("unexpected error getting keysets: %v", err) } - mintRes, err := testWallet.RequestMint(10000) + mintRes, err := testWallet.RequestMint(10000, testWallet.CurrentMint()) if err != nil { t.Fatalf("unexpected error requesting mint: %v", err) } @@ -1118,7 +1180,7 @@ func TestNutshell(t *testing.T) { os.RemoveAll(testWalletPath) }() - mintRes, err := testWallet.RequestMint(10000) + mintRes, err := testWallet.RequestMint(10000, testWallet.CurrentMint()) if err != nil { t.Fatalf("unexpected error requesting mint: %v", err) } @@ -1163,7 +1225,7 @@ func TestOverpaidFeesChange(t *testing.T) { os.RemoveAll(testWalletPath) }() - mintRes, err := testWallet.RequestMint(10000) + mintRes, err := testWallet.RequestMint(10000, testWallet.CurrentMint()) if err != nil { t.Fatalf("unexpected error requesting mint: %v", err) } @@ -1199,7 +1261,7 @@ func TestOverpaidFeesChange(t *testing.T) { // do extra ops after melting to check counter for blinded messages // was incremented correctly - mintRes, err = testWallet.RequestMint(5000) + mintRes, err = testWallet.RequestMint(5000, testWallet.CurrentMint()) if err != nil { t.Fatalf("unexpected error requesting mint: %v", err) }