From ae3248ce8decbfd2baaa63039961033876a3fa53 Mon Sep 17 00:00:00 2001 From: 0ceanSlim Date: Thu, 20 Feb 2025 12:41:36 -0500 Subject: [PATCH 1/2] mint: add CLN support --- .env.mint.example | 7 +- .github/workflows/ci.yml | 9 +- cmd/mint/mint.go | 34 ++- mint/lightning/cln.go | 426 ++++++++++++++++++++++++++++++++++ mint/lightning/lightning.go | 15 +- mint/lightning/lnd.go | 6 - mint/mint.go | 2 +- mint/mint_integration_test.go | 87 +++++-- testutils/utils.go | 34 +-- 9 files changed, 579 insertions(+), 41 deletions(-) create mode 100644 mint/lightning/cln.go diff --git a/.env.mint.example b/.env.mint.example index 2a50948..e7dcee4 100644 --- a/.env.mint.example +++ b/.env.mint.example @@ -20,7 +20,7 @@ MINTING_MAX_AMOUNT=50000 # max melt amount (in sats) MELTING_MAX_AMOUNT=50000 -# Lightning Backend - Lnd, FakeBackend (FOR TESTING ONLY) +# Lightning Backend - Lnd, CLN, FakeBackend (FOR TESTING ONLY) LIGHTNING_BACKEND="Lnd" # LND @@ -28,6 +28,11 @@ LND_GRPC_HOST="127.0.0.1:10001" LND_CERT_PATH="/path/to/tls/cert" LND_MACAROON_PATH="/path/to/macaroon" +# CLN +CLN_REST_URL="http://127.0.0.1:3030" +CLN_CERT_PATH="/path/to/cert" +CLN_REST_RUNE_PATH="/path/to/rune" + # enable MPP/NUT-15 (disabled by default) # ENABLE_MPP=TRUE diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60633a6..5006daa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,11 @@ jobs: with: go-version: 1.23.7 - - name: Integration Tests + - name: Mint Integration Tests LND run: go test -v --tags=integration ./mint - - run: go test -v --tags=integration ./wallet \ No newline at end of file + + - name: Mint Integration Tests CLN + run: go test -v --tags=integration ./mint -args -backend=CLN + + - name: Wallet Integration Tests + run: go test -v --tags=integration ./wallet \ No newline at end of file diff --git a/cmd/mint/mint.go b/cmd/mint/mint.go index 20fd886..fbbf64e 100644 --- a/cmd/mint/mint.go +++ b/cmd/mint/mint.go @@ -126,9 +126,8 @@ func configFromEnv() (*mint.Config, error) { } var lightningClient lightning.Client - switch os.Getenv("LIGHTNING_BACKEND") { - case "Lnd": - // read values for setting up LND + switch strings.ToUpper(os.Getenv("LIGHTNING_BACKEND")) { + case "LND": host := os.Getenv("LND_GRPC_HOST") if host == "" { return nil, errors.New("LND_GRPC_HOST cannot be empty") @@ -170,8 +169,35 @@ func configFromEnv() (*mint.Config, error) { if err != nil { return nil, fmt.Errorf("error setting LND client: %v", err) } - case "FakeBackend": + + case "CLN": + restURL := os.Getenv("CLN_REST_URL") + if restURL == "" { + return nil, errors.New("CLN_REST_URL cannot be empty") + } + runePath := os.Getenv("CLN_REST_RUNE_PATH") + if runePath == "" { + return nil, errors.New("CLN_REST_RUNE_PATH cannot be empty") + } + + rune, err := os.ReadFile(runePath) + if err != nil { + return nil, fmt.Errorf("error reading macaroon: os.ReadFile %v", err) + } + + clnConfig := lightning.CLNConfig{ + RestURL: restURL, + Rune: string(rune), + } + + lightningClient, err = lightning.SetupCLNClient(clnConfig) + if err != nil { + return nil, fmt.Errorf("error setting up CLN client: %v", err) + } + + case "FAKEBACKEND": lightningClient = &lightning.FakeBackend{} + default: return nil, errors.New("invalid lightning backend") } diff --git a/mint/lightning/cln.go b/mint/lightning/cln.go new file mode 100644 index 0000000..a09ff51 --- /dev/null +++ b/mint/lightning/cln.go @@ -0,0 +1,426 @@ +package lightning + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "math/rand/v2" + "net/http" + "time" +) + +type CLNConfig struct { + RestURL string + Rune string +} + +type CLNClient struct { + config CLNConfig + client *http.Client +} + +type ErrorResponse struct { + Code int `json:"code"` + Message string `json:"message,omitempty"` +} + +func SetupCLNClient(config CLNConfig) (*CLNClient, error) { + return &CLNClient{ + config: config, + client: &http.Client{Timeout: 30 * time.Second}, + }, nil +} + +func (cln *CLNClient) Post(ctx context.Context, url string, body interface{}) (*http.Response, error) { + var jsonData []byte + if body != nil { + var err error + jsonData, err = json.Marshal(body) + if err != nil { + return nil, err + } + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, err + } + + req.Header.Set("Rune", cln.config.Rune) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + + return cln.client.Do(req) +} + +func (cln *CLNClient) ConnectionStatus() error { + resp, err := cln.Post(context.Background(), cln.config.RestURL+"/v1/getinfo", nil) + if err != nil { + return err + } + defer resp.Body.Close() + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + return fmt.Errorf("could not get connection status from CLN: %s", bodyBytes) + } + + return nil +} + +func (cln *CLNClient) CreateInvoice(amount uint64) (Invoice, error) { + r := rand.New(rand.NewPCG(uint64(time.Now().UnixMicro()), uint64(time.Now().UnixMilli()))) + + body := map[string]interface{}{ + "amount_msat": amount * 1000, + "label": time.Now().Unix() + int64(r.Int()), + "description": "Cashu Lightning Invoice", + "expiry": InvoiceExpiryTime, + } + + resp, err := cln.Post(context.Background(), cln.config.RestURL+"/v1/invoice", body) + if err != nil { + return Invoice{}, err + } + defer resp.Body.Close() + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return Invoice{}, err + } + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + var errRes ErrorResponse + if err := json.Unmarshal(bodyBytes, &errRes); err != nil { + return Invoice{}, err + } + return Invoice{}, errors.New(errRes.Message) + } + + var response struct { + Bolt11 string `json:"bolt11"` + PaymentHash string `json:"payment_hash"` + } + if err := json.Unmarshal(bodyBytes, &response); err != nil { + return Invoice{}, err + } + + return Invoice{ + PaymentRequest: response.Bolt11, + PaymentHash: response.PaymentHash, + Amount: amount, + Expiry: InvoiceExpiryTime, + }, nil +} + +func (cln *CLNClient) InvoiceStatus(hash string) (Invoice, error) { + body := map[string]string{"payment_hash": hash} + + resp, err := cln.Post(context.Background(), cln.config.RestURL+"/v1/listinvoices", body) + if err != nil { + return Invoice{}, err + } + defer resp.Body.Close() + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return Invoice{}, err + } + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + var errRes ErrorResponse + if err := json.Unmarshal(bodyBytes, &errRes); err != nil { + return Invoice{}, err + } + return Invoice{}, errors.New(errRes.Message) + } + + var response struct { + Invoices []struct { + Bolt11 string `json:"bolt11"` + PaymentHash string `json:"payment_hash"` + Preimage string `json:"payment_preimage"` + AmountMsat uint64 `json:"amount_msat"` + Status string `json:"status"` + ExpiresAt int64 `json:"expires_at"` + } `json:"invoices"` + } + if err := json.Unmarshal(bodyBytes, &response); err != nil { + return Invoice{}, err + } + if len(response.Invoices) == 0 { + return Invoice{}, fmt.Errorf("invoice not found") + } + + invoice := response.Invoices[0] + invoiceSettled := invoice.Status == "paid" + + return Invoice{ + PaymentRequest: invoice.Bolt11, + PaymentHash: invoice.PaymentHash, + Preimage: invoice.Preimage, + Settled: invoiceSettled, + Amount: invoice.AmountMsat / 1000, + Expiry: uint64(invoice.ExpiresAt), + }, nil +} + +func (cln *CLNClient) SendPayment(ctx context.Context, request string, maxFee uint64) (PaymentStatus, error) { + body := map[string]interface{}{ + "bolt11": request, + "maxfee": maxFee * 1000, + } + + resp, err := cln.Post(ctx, cln.config.RestURL+"/v1/pay", body) + if err != nil { + return PaymentStatus{PaymentStatus: Pending}, err + } + defer resp.Body.Close() + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return PaymentStatus{PaymentStatus: Pending}, err + } + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + var errRes ErrorResponse + if err := json.Unmarshal(bodyBytes, &errRes); err != nil { + return PaymentStatus{PaymentStatus: Pending}, err + } + return PaymentStatus{PaymentStatus: Failed}, errors.New(errRes.Message) + } + + var response struct { + Preimage string `json:"payment_preimage"` + Status string `json:"status"` + } + if err := json.Unmarshal(bodyBytes, &response); err != nil { + return PaymentStatus{PaymentStatus: Pending}, err + } + + status := Pending + switch response.Status { + case "complete": + status = Succeeded + case "pending": + status = Pending + case "failed": + status = Failed + } + + return PaymentStatus{ + Preimage: response.Preimage, + PaymentStatus: status, + }, nil +} + +func (cln *CLNClient) PayPartialAmount( + ctx context.Context, + request string, + amountMsat uint64, + maxFee uint64, +) (PaymentStatus, error) { + body := map[string]interface{}{ + "bolt11": request, + "partial_msat": amountMsat, + "maxfee": maxFee * 1000, + "retry_for": 30, + } + + resp, err := cln.Post(ctx, cln.config.RestURL+"/v1/pay", body) + if err != nil { + return PaymentStatus{}, err + } + defer resp.Body.Close() + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return PaymentStatus{PaymentStatus: Pending}, err + } + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + var errRes ErrorResponse + if err := json.Unmarshal(bodyBytes, &errRes); err != nil { + return PaymentStatus{PaymentStatus: Pending}, err + } + return PaymentStatus{PaymentStatus: Failed}, errors.New(errRes.Message) + } + + var response struct { + Preimage string `json:"payment_preimage"` + Status string `json:"status"` + } + if err := json.Unmarshal(bodyBytes, &response); err != nil { + return PaymentStatus{}, fmt.Errorf("failed to parse response: %w", err) + } + + status := Pending + switch response.Status { + case "complete": + status = Succeeded + case "pending": + status = Pending + case "failed": + status = Failed + } + + return PaymentStatus{ + Preimage: response.Preimage, + PaymentStatus: status, + }, nil +} + +func (cln *CLNClient) OutgoingPaymentStatus(ctx context.Context, paymentHash string) (PaymentStatus, error) { + body := map[string]string{"payment_hash": paymentHash} + resp, err := cln.Post(ctx, cln.config.RestURL+"/v1/listpays", body) + if err != nil { + return PaymentStatus{}, err + } + defer resp.Body.Close() + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return PaymentStatus{PaymentStatus: Pending}, err + } + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + var errRes ErrorResponse + if err := json.Unmarshal(bodyBytes, &errRes); err != nil { + return PaymentStatus{PaymentStatus: Pending}, err + } + return PaymentStatus{PaymentStatus: Failed}, errors.New(errRes.Message) + } + + var listPaysResponse struct { + Pays []struct { + PaymentHash string `json:"payment_hash"` + Status string `json:"status"` + PaymentPreimage string `json:"preimage,omitempty"` + } `json:"pays"` + } + if err := json.Unmarshal(bodyBytes, &listPaysResponse); err != nil { + return PaymentStatus{PaymentStatus: Pending}, err + } + if len(listPaysResponse.Pays) == 0 { + return PaymentStatus{PaymentStatus: Failed}, OutgoingPaymentNotFound + } + + payment := listPaysResponse.Pays[0] + switch payment.Status { + case "complete": + return PaymentStatus{PaymentStatus: Succeeded, Preimage: payment.PaymentPreimage}, nil + case "failed": + return PaymentStatus{PaymentStatus: Failed}, nil + default: + return PaymentStatus{PaymentStatus: Pending}, nil + } +} + +func (cln *CLNClient) FeeReserve(amount uint64) uint64 { + return uint64(math.Ceil(float64(amount) * FeePercent)) +} + +func (cln *CLNClient) SubscribeInvoice(ctx context.Context, paymentHash string) (InvoiceSubscriptionClient, error) { + body := map[string]string{"payment_hash": paymentHash} + + resp, err := cln.Post(context.Background(), cln.config.RestURL+"/v1/listinvoices", body) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + var errRes ErrorResponse + if err := json.Unmarshal(bodyBytes, &errRes); err != nil { + return nil, err + } + return nil, errors.New(errRes.Message) + } + + var response struct { + Invoices []struct { + Label string `json:"label"` + } `json:"invoices"` + } + if err := json.Unmarshal(bodyBytes, &response); err != nil { + return nil, err + } + if len(response.Invoices) == 0 { + return nil, fmt.Errorf("invoice not found") + } + + sub := &CLNInvoiceSub{ + client: &CLNClient{config: cln.config, client: &http.Client{}}, + ctx: ctx, + paymentHash: paymentHash, + invoiceLabel: response.Invoices[0].Label, + } + return sub, nil +} + +type CLNInvoiceSub struct { + client *CLNClient + ctx context.Context + paymentHash string + invoiceLabel string +} + +func (clnSub *CLNInvoiceSub) Recv() (Invoice, error) { + body := map[string]string{"label": clnSub.invoiceLabel} + + // NOTE: this call blocks untils the invoice is either paid or expired + resp, err := clnSub.client.Post(clnSub.ctx, clnSub.client.config.RestURL+"/v1/waitinvoice", body) + if err != nil { + return Invoice{}, err + } + defer resp.Body.Close() + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return Invoice{}, err + } + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + var errRes ErrorResponse + if err := json.Unmarshal(bodyBytes, &errRes); err != nil { + return Invoice{}, err + } + return Invoice{}, errors.New(errRes.Message) + } + + var response struct { + Status string `json:"status"` + PaymentHash string `json:"payment_hash"` + Preimage string `json:"payment_preimage"` + AmountMsat uint64 `json:"amount_msat"` + } + if err := json.Unmarshal(bodyBytes, &response); err != nil { + return Invoice{}, err + } + + inv := Invoice{ + PaymentHash: response.PaymentHash, + Settled: false, + Amount: response.AmountMsat / 1000, + } + + if response.Status == "paid" { + inv.Settled = true + inv.Preimage = response.Preimage + } + + return inv, nil +} diff --git a/mint/lightning/lightning.go b/mint/lightning/lightning.go index 4ae2377..d5908f1 100644 --- a/mint/lightning/lightning.go +++ b/mint/lightning/lightning.go @@ -1,6 +1,9 @@ package lightning -import "context" +import ( + "context" + "errors" +) // Client interface to interact with a Lightning backend type Client interface { @@ -14,6 +17,16 @@ type Client interface { SubscribeInvoice(ctx context.Context, paymentHash string) (InvoiceSubscriptionClient, error) } +const ( + // 1 hour + InvoiceExpiryTime = 3600 + FeePercent float64 = 0.01 +) + +var ( + OutgoingPaymentNotFound = errors.New("outgoing payment not found") +) + type Invoice struct { PaymentRequest string PaymentHash string diff --git a/mint/lightning/lnd.go b/mint/lightning/lnd.go index 7a6b1a7..82ec3ee 100644 --- a/mint/lightning/lnd.go +++ b/mint/lightning/lnd.go @@ -16,12 +16,6 @@ import ( "google.golang.org/grpc/credentials" ) -const ( - // 1 hour - InvoiceExpiryTime = 3600 - FeePercent float64 = 0.01 -) - type LndConfig struct { GRPCHost string Cert credentials.TransportCredentials diff --git a/mint/mint.go b/mint/mint.go index ead44a0..494bb16 100644 --- a/mint/mint.go +++ b/mint/mint.go @@ -896,7 +896,7 @@ func (m *Mint) MeltTokens(ctx context.Context, meltTokensRequest nut05.PostMeltB // if got failed from SendPayment // do additional check by calling to get outgoing payment status paymentStatus, err := m.lightningClient.OutgoingPaymentStatus(ctx, meltQuote.PaymentHash) - if status.Code(err) == codes.NotFound { + if errors.Is(err, lightning.OutgoingPaymentNotFound) || status.Code(err) == codes.NotFound { m.logInfof("no outgoing payment found with hash: %v. Removing pending proofs and marking quote '%v' as unpaid", meltQuote.PaymentHash, meltQuote.Id) diff --git a/mint/mint_integration_test.go b/mint/mint_integration_test.go index eb913df..6649775 100644 --- a/mint/mint_integration_test.go +++ b/mint/mint_integration_test.go @@ -119,23 +119,40 @@ func testMain(m *testing.M) (int, error) { lnd3.Terminate(ctx) }() case "CLN": - // NOTE: Putting as placeholder for now. Tests here will fail. - // Would still need to add some setup when CLN support is added. cln1, err := cln.NewCLN(ctx, bitcoind) if err != nil { return 1, err } - cln2, err := cln.NewCLN(ctx, bitcoind) if err != nil { return 1, err } + cln3, err := cln.NewCLN(ctx, bitcoind) + if err != nil { + return 1, err + } + + lightningClient1, err = testutils.CLNClient(cln1) + if err != nil { + return 1, err + } + lightningClient2, err = testutils.CLNClient(cln2) + if err != nil { + return 1, err + } + lightningClient3, err = testutils.CLNClient(cln3) + if err != nil { + return 1, err + } + node1 = testutils.NewCLNBackend(cln1) node2 = testutils.NewCLNBackend(cln2) + node3 = testutils.NewCLNBackend(cln3) defer func() { cln1.Terminate(ctx) cln2.Terminate(ctx) + cln3.Terminate(ctx) }() default: @@ -537,7 +554,6 @@ func TestRequestMeltQuote(t *testing.T) { // trying to create another melt quote with same invoice should throw error _, err = testMint.RequestMeltQuote(meltQuoteRequest) if !errors.Is(err, cashu.MeltQuoteForRequestExists) { - //if !errors.Is(err, cashu.PaymentMethodNotSupportedErr) { t.Fatalf("expected error '%v' but got '%v' instead", cashu.MeltQuoteForRequestExists, err) } } @@ -793,8 +809,14 @@ func TestMelt(t *testing.T) { if err != nil { t.Fatalf("got unexpected error in melt: %v", err) } - if len(melt.Preimage) == 0 { - t.Fatal("melt returned empty preimage") + if melt.State != nut05.Paid { + t.Fatalf("expected melt quote with state of '%s' but got '%s' instead", nut05.Paid, meltResponse.State) + } + // NOTE: for internal quotes CLN will not return the preimage so only check it for LND + if *backend == "LND" { + if len(melt.Preimage) == 0 { + t.Fatal("melt returned empty preimage") + } } // now mint should work because quote was settled internally @@ -818,6 +840,16 @@ func TestMPPMelt(t *testing.T) { if err != nil { t.Fatal(err) } + case "CLN": + cln4, err := cln.NewCLN(ctx, bitcoind) + if err != nil { + t.Fatal(err) + } + defer cln4.Terminate(ctx) + lightningClient4, err = testutils.CLNClient(cln4) + if err != nil { + t.Fatal(err) + } } if err := testutils.FundNode(ctx, bitcoind, node2); err != nil { @@ -1054,17 +1086,40 @@ func TestMPPMelt(t *testing.T) { } func TestPendingProofs(t *testing.T) { + // CLN does not support hodl invoices unless with a plugin so creating LND container + // regardless and using this node for the hodl invoice that is used to test + // pending payments + lnd, err := lnd.NewLnd(ctx, bitcoind) + if err != nil { + t.Fatal(err) + } + defer lnd.Terminate(ctx) + node4 := &testutils.LndBackend{Lnd: lnd} + if err := testutils.OpenChannel(ctx, bitcoind, node1, node4, 300000); err != nil { + t.Fatal(err) + } + // use hodl invoice to cause payment to stuck and put quote and proofs in state of pending preimageBytes, _ := testutils.GenerateRandomBytes() preimage := hex.EncodeToString(preimageBytes) hashBytes := sha256.Sum256(preimageBytes) hash := hex.EncodeToString(hashBytes[:]) - hodlInvoice, err := node2.CreateHodlInvoice(2100, hash) - if err != nil { - t.Fatalf("error creating hodl invoice: %v", err) + + var paymentRequest string + if *backend == "CLN" { + hodlInvoice, err := node4.CreateHodlInvoice(2100, hash) + if err != nil { + t.Fatalf("error creating hodl invoice: %v", err) + } + paymentRequest = hodlInvoice.PaymentRequest + } else { + hodlInvoice, err := node2.CreateHodlInvoice(2100, hash) + if err != nil { + t.Fatalf("error creating hodl invoice: %v", err) + } + paymentRequest = hodlInvoice.PaymentRequest } - paymentRequest := hodlInvoice.PaymentRequest meltQuoteRequest := nut05.PostMeltQuoteBolt11Request{Request: paymentRequest, Unit: cashu.Sat.String()} meltQuote, err := testMint.RequestMeltQuote(meltQuoteRequest) @@ -1130,11 +1185,17 @@ func TestPendingProofs(t *testing.T) { t.Fatalf("expected error '%v' but got '%v' instead", cashu.ProofPendingErr, err) } - if err := node2.SettleHodlInvoice(preimage, "", nil); err != nil { - t.Fatalf("error settling hodl invoice: %v", err) + if *backend == "CLN" { + if err := node4.SettleHodlInvoice(preimage); err != nil { + t.Fatalf("error settling hodl invoice: %v", err) + } + } else { + if err := node2.SettleHodlInvoice(preimage); err != nil { + t.Fatalf("error settling hodl invoice: %v", err) + } } - meltQuote, err = testMint.GetMeltQuoteState(ctx, melt.Id) + meltQuote, err = testMint.GetMeltQuoteState(context.Background(), melt.Id) if err != nil { t.Fatalf("unexpected error getting melt quote state: %v", err) } diff --git a/testutils/utils.go b/testutils/utils.go index 131b783..f5687fb 100644 --- a/testutils/utils.go +++ b/testutils/utils.go @@ -60,10 +60,7 @@ type LightningBackend interface { CreateInvoice(amount uint64) (*Invoice, error) LookupInvoice(hash string) (*Invoice, error) CreateHodlInvoice(amount uint64, hash string) (*Invoice, error) - // CLN does not support HODL invoices (unless using a plugin) - // passing invoice and payer as a hack. Payer will pay the invoice - // just like a regular invoice. - SettleHodlInvoice(preimage string, invoice string, payer *CLNBackend) error + SettleHodlInvoice(preimage string) error } type Peer struct { @@ -209,8 +206,7 @@ func (lndContainer *LndBackend) CreateHodlInvoice(amount uint64, hash string) (* }, nil } -// NOTE: invoice and payer are not used. Those are an ugly hack for CLN -func (lndContainer *LndBackend) SettleHodlInvoice(preimage string, invoice string, payer *CLNBackend) error { +func (lndContainer *LndBackend) SettleHodlInvoice(preimage string) error { preimageBytes, err := hex.DecodeString(preimage) if err != nil { return err @@ -455,9 +451,10 @@ func (clnContainer *CLNBackend) PayInvoice(invoice string) error { } func (clnContainer *CLNBackend) CreateInvoice(amount uint64) (*Invoice, error) { + r := mathrand.New(mathrand.NewPCG(uint64(time.Now().UnixMicro()), uint64(time.Now().UnixMilli()))) body := map[string]any{ "amount_msat": amount * 1000, - "label": time.Now().Unix(), + "label": time.Now().Unix() + int64(r.Int()), "description": "test", } @@ -489,16 +486,13 @@ func (clnContainer *CLNBackend) CreateInvoice(amount uint64) (*Invoice, error) { }, nil } +// NOTE: CLN does not support HODL invoices (unless using a plugin). These will not be used +// in the tests. Rather will do it through LND. func (clnContainer *CLNBackend) CreateHodlInvoice(amount uint64, hash string) (*Invoice, error) { return clnContainer.CreateInvoice(amount) } -func (clnContainer *CLNBackend) SettleHodlInvoice(preimage string, invoice string, payer *CLNBackend) error { - if err := payer.PayInvoice(invoice); err != nil { - return err - } - return nil -} +func (clnContainer *CLNBackend) SettleHodlInvoice(preimage string) error { return nil } func (clnContainer *CLNBackend) LookupInvoice(hash string) (*Invoice, error) { body := map[string]string{ @@ -710,6 +704,20 @@ func LndClient(lnd *lnd.Lnd) (*lightning.LndClient, error) { return lndClient, nil } +func CLNClient(clnNode *cln.CLN) (*lightning.CLNClient, error) { + clnConfig := lightning.CLNConfig{ + RestURL: fmt.Sprintf("http://%s:%s", clnNode.Host, clnNode.RestPort), + Rune: clnNode.Rune, + } + + clnClient, err := lightning.SetupCLNClient(clnConfig) + if err != nil { + return nil, fmt.Errorf("error setting CLN client: %v", err) + } + + return clnClient, nil +} + func CreateTestMint( backend lightning.Client, dbpath string, From 00afbc51b253d51bc0043aaacbef8a0c25aa6d56 Mon Sep 17 00:00:00 2001 From: elnosh Date: Wed, 26 Mar 2025 15:40:44 -0500 Subject: [PATCH 2/2] breakup integration tests jobs --- .github/workflows/ci.yml | 20 +++++++++++++++----- mint/mint_integration_test.go | 5 +++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5006daa..2d3ba26 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,8 +32,11 @@ jobs: - name: Fuzz Uint64 subtraction run: go test -v -fuzz=FuzzUnderflowSubUint64 -fuzztime=60s ./cashu - integration-tests: + mint-integration-tests: runs-on: ubuntu-latest + strategy: + matrix: + backend: [LND, CLN] steps: - uses: actions/checkout@v4 @@ -42,11 +45,18 @@ jobs: with: go-version: 1.23.7 - - name: Mint Integration Tests LND - run: go test -v --tags=integration ./mint + - name: Mint Integration Tests + run: go test -v --tags=integration ./mint -args -backend ${{ matrix.backend }} - - name: Mint Integration Tests CLN - run: go test -v --tags=integration ./mint -args -backend=CLN + wallet-integration-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.23.7 - name: Wallet Integration Tests run: go test -v --tags=integration ./wallet \ No newline at end of file diff --git a/mint/mint_integration_test.go b/mint/mint_integration_test.go index 6649775..0da60f3 100644 --- a/mint/mint_integration_test.go +++ b/mint/mint_integration_test.go @@ -1195,6 +1195,11 @@ func TestPendingProofs(t *testing.T) { } } + time.Sleep(time.Second * 2) + + meltContext, cancel = context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + meltQuote, err = testMint.GetMeltQuoteState(context.Background(), melt.Id) if err != nil { t.Fatalf("unexpected error getting melt quote state: %v", err)