diff --git a/.env.mint.example b/.env.mint.example index 62674c9..05fd726 100644 --- a/.env.mint.example +++ b/.env.mint.example @@ -18,8 +18,10 @@ MINTING_MAX_AMOUNT=50000 # max melt amount (in sats) MELTING_MAX_AMOUNT=50000 -# Lightning Backend +# Lightning Backend - Lnd, FakeBackend (FOR TESTING ONLY) LIGHTNING_BACKEND="Lnd" + +# LND LND_GRPC_HOST="127.0.0.1:10001" LND_CERT_PATH="/path/to/tls/cert" LND_MACAROON_PATH="/path/to/macaroon" diff --git a/cmd/mint/mint.go b/cmd/mint/mint.go index 0670dc9..38c998d 100644 --- a/cmd/mint/mint.go +++ b/cmd/mint/mint.go @@ -89,47 +89,56 @@ func configFromEnv() (*mint.Config, error) { } mintInfo.Contact = mintContactInfo - // read values for setting up LND - host := os.Getenv("LND_GRPC_HOST") - if host == "" { - return nil, errors.New("LND_GRPC_HOST cannot be empty") - } - certPath := os.Getenv("LND_CERT_PATH") - if certPath == "" { - return nil, errors.New("LND_CERT_PATH cannot be empty") - } - macaroonPath := os.Getenv("LND_MACAROON_PATH") - if macaroonPath == "" { - return nil, errors.New("LND_MACAROON_PATH cannot be empty") - } + var lightningClient lightning.Client + + switch os.Getenv("LIGHTNING_BACKEND") { + case "Lnd": + // read values for setting up LND + host := os.Getenv("LND_GRPC_HOST") + if host == "" { + return nil, errors.New("LND_GRPC_HOST cannot be empty") + } + certPath := os.Getenv("LND_CERT_PATH") + if certPath == "" { + return nil, errors.New("LND_CERT_PATH cannot be empty") + } + macaroonPath := os.Getenv("LND_MACAROON_PATH") + if macaroonPath == "" { + return nil, errors.New("LND_MACAROON_PATH cannot be empty") + } - creds, err := credentials.NewClientTLSFromFile(certPath, "") - if err != nil { - return nil, err - } + creds, err := credentials.NewClientTLSFromFile(certPath, "") + if err != nil { + return nil, err + } - macaroonBytes, err := os.ReadFile(macaroonPath) - if err != nil { - return nil, fmt.Errorf("error reading macaroon: os.ReadFile %v", err) - } + macaroonBytes, err := os.ReadFile(macaroonPath) + if err != nil { + return nil, fmt.Errorf("error reading macaroon: os.ReadFile %v", err) + } - macaroon := &macaroon.Macaroon{} - if err = macaroon.UnmarshalBinary(macaroonBytes); err != nil { - return nil, fmt.Errorf("unable to decode macaroon: %v", err) - } - macarooncreds, err := macaroons.NewMacaroonCredential(macaroon) - if err != nil { - return nil, fmt.Errorf("error setting macaroon creds: %v", err) - } - lndConfig := lightning.LndConfig{ - GRPCHost: host, - Cert: creds, - Macaroon: macarooncreds, - } + macaroon := &macaroon.Macaroon{} + if err = macaroon.UnmarshalBinary(macaroonBytes); err != nil { + return nil, fmt.Errorf("unable to decode macaroon: %v", err) + } + macarooncreds, err := macaroons.NewMacaroonCredential(macaroon) + if err != nil { + return nil, fmt.Errorf("error setting macaroon creds: %v", err) + } + lndConfig := lightning.LndConfig{ + GRPCHost: host, + Cert: creds, + Macaroon: macarooncreds, + } - lndClient, err := lightning.SetupLndClient(lndConfig) - if err != nil { - return nil, fmt.Errorf("error setting LND client: %v", err) + lightningClient, err = lightning.SetupLndClient(lndConfig) + if err != nil { + return nil, fmt.Errorf("error setting LND client: %v", err) + } + case "FakeBackend": + lightningClient = &lightning.FakeBackend{} + default: + return nil, errors.New("invalid lightning backend") } logLevel := mint.Info @@ -145,7 +154,7 @@ func configFromEnv() (*mint.Config, error) { InputFeePpk: inputFeePpk, MintInfo: mintInfo, Limits: mintLimits, - LightningClient: lndClient, + LightningClient: lightningClient, LogLevel: logLevel, }, nil } diff --git a/mint/lightning/fakebackend.go b/mint/lightning/fakebackend.go new file mode 100644 index 0000000..0e75e5e --- /dev/null +++ b/mint/lightning/fakebackend.go @@ -0,0 +1,134 @@ +package lightning + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "slices" + "time" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/zpay32" + decodepay "github.com/nbd-wtf/ln-decodepay" +) + +const ( + FakePreimage = "0000000000000000" +) + +type FakeBackend struct { + invoices []Invoice +} + +func (fb *FakeBackend) ConnectionStatus() error { return nil } + +func (fb *FakeBackend) CreateInvoice(amount uint64) (Invoice, error) { + req, preimage, paymentHash, err := createFakeInvoice(amount) + if err != nil { + return Invoice{}, err + } + + invoice := Invoice{ + PaymentRequest: req, + PaymentHash: paymentHash, + Preimage: preimage, + Settled: true, + Amount: amount, + Expiry: uint64(time.Now().Unix()), + } + fb.invoices = append(fb.invoices, invoice) + + return invoice, nil +} + +func (fb *FakeBackend) InvoiceStatus(hash string) (Invoice, error) { + invoiceIdx := slices.IndexFunc(fb.invoices, func(i Invoice) bool { + return i.PaymentHash == hash + }) + if invoiceIdx == -1 { + return Invoice{}, errors.New("invoice does not exist") + } + + return fb.invoices[invoiceIdx], nil +} + +func (fb *FakeBackend) SendPayment(ctx context.Context, request string, amount uint64) (PaymentStatus, error) { + invoice, err := decodepay.Decodepay(request) + if err != nil { + return PaymentStatus{}, fmt.Errorf("error decoding invoice: %v", err) + } + + outgoingPayment := Invoice{ + PaymentRequest: request, + PaymentHash: invoice.PaymentHash, + Preimage: FakePreimage, + Settled: true, + } + fb.invoices = append(fb.invoices, outgoingPayment) + + return PaymentStatus{ + Preimage: FakePreimage, + PaymentStatus: Succeeded, + }, nil +} + +func (fb *FakeBackend) OutgoingPaymentStatus(ctx context.Context, hash string) (PaymentStatus, error) { + invoiceIdx := slices.IndexFunc(fb.invoices, func(i Invoice) bool { + return i.PaymentHash == hash + }) + if invoiceIdx == -1 { + return PaymentStatus{}, errors.New("payment does not exist") + } + + return PaymentStatus{ + Preimage: fb.invoices[invoiceIdx].Preimage, + PaymentStatus: Succeeded, + }, nil +} + +func (fb *FakeBackend) FeeReserve(amount uint64) uint64 { + return 0 +} + +func createFakeInvoice(amount uint64) (string, string, string, error) { + var random [32]byte + _, err := rand.Read(random[:]) + if err != nil { + return "", "", "", err + } + preimage := hex.EncodeToString(random[:]) + paymentHash := sha256.Sum256(random[:]) + hash := hex.EncodeToString(paymentHash[:]) + + invoice, err := zpay32.NewInvoice( + &chaincfg.SigNetParams, + paymentHash, + time.Now(), + zpay32.Amount(lnwire.MilliSatoshi(amount*1000)), + zpay32.Description("test"), + ) + if err != nil { + return "", "", "", err + } + + invoiceStr, err := invoice.Encode(zpay32.MessageSigner{ + SignCompact: func(msg []byte) ([]byte, error) { + key, err := secp256k1.GeneratePrivateKey() + if err != nil { + return []byte{}, err + } + return ecdsa.SignCompact(key, msg, true), nil + }, + }) + if err != nil { + return "", "", "", err + } + + return invoiceStr, preimage, hash, nil +}