Use HODL invoices
This commit is contained in:
		
							parent
							
								
									2d2ceec140
								
							
						
					
					
						commit
						057690d5a9
					
				
							
								
								
									
										19
									
								
								src/db.go
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								src/db.go
									
									
									
									
									
								
							@ -129,9 +129,22 @@ func (db *DB) CreateInvoice(invoice *Invoice) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (db *DB) FetchInvoice(invoiceId string, invoice *Invoice) error {
 | 
			
		||||
	if err := db.QueryRow(""+
 | 
			
		||||
		"SELECT id, pubkey, msats, preimage, hash, bolt11, created_at, expires_at, confirmed_at, held_since FROM invoices WHERE id = $1", invoiceId).Scan(&invoice.Id, &invoice.Pubkey, &invoice.Msats, &invoice.Preimage, &invoice.PaymentHash, &invoice.PaymentRequest, &invoice.CreatedAt, &invoice.ExpiresAt, &invoice.ConfirmedAt, &invoice.HeldSince); err != nil {
 | 
			
		||||
type FetchInvoiceWhere struct {
 | 
			
		||||
	Id   string
 | 
			
		||||
	Hash string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (db *DB) FetchInvoice(where *FetchInvoiceWhere, invoice *Invoice) error {
 | 
			
		||||
	query := "SELECT id, pubkey, msats, preimage, hash, bolt11, created_at, expires_at, confirmed_at, held_since FROM invoices "
 | 
			
		||||
	var args []any
 | 
			
		||||
	if where.Id != "" {
 | 
			
		||||
		query += "WHERE id = $1"
 | 
			
		||||
		args = append(args, where.Id)
 | 
			
		||||
	} else if where.Hash != "" {
 | 
			
		||||
		query += "WHERE hash = $1"
 | 
			
		||||
		args = append(args, where.Hash)
 | 
			
		||||
	}
 | 
			
		||||
	if err := db.QueryRow(query, args...).Scan(&invoice.Id, &invoice.Pubkey, &invoice.Msats, &invoice.Preimage, &invoice.PaymentHash, &invoice.PaymentRequest, &invoice.CreatedAt, &invoice.ExpiresAt, &invoice.ConfirmedAt, &invoice.HeldSince); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										124
									
								
								src/lnd.go
									
									
									
									
									
								
							
							
						
						
									
										124
									
								
								src/lnd.go
									
									
									
									
									
								
							@ -2,17 +2,19 @@ package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/rand"
 | 
			
		||||
	"database/sql"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/joho/godotenv"
 | 
			
		||||
	"github.com/labstack/echo/v4"
 | 
			
		||||
	"github.com/lightninglabs/lndclient"
 | 
			
		||||
	"github.com/lightningnetwork/lnd/lnrpc"
 | 
			
		||||
	"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
 | 
			
		||||
	"github.com/lightningnetwork/lnd/lntypes"
 | 
			
		||||
	"github.com/lightningnetwork/lnd/lnwire"
 | 
			
		||||
	"github.com/namsral/flag"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -25,7 +27,7 @@ var (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type LndClient struct {
 | 
			
		||||
	lnrpc.LightningClient
 | 
			
		||||
	lndclient.GrpcLndServices
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
@ -38,20 +40,21 @@ func init() {
 | 
			
		||||
	flag.StringVar(&LndHost, "LND_HOST", "localhost:10001", "LND gRPC server address")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
	lndEnabled = false
 | 
			
		||||
	rpcClient, err := lndclient.NewBasicClient(LndHost, LndCert, LndMacaroonDir, "regtest")
 | 
			
		||||
	rpcLndServices, err := lndclient.NewLndServices(&lndclient.LndServicesConfig{
 | 
			
		||||
		LndAddress:  LndHost,
 | 
			
		||||
		MacaroonDir: LndMacaroonDir,
 | 
			
		||||
		TLSPath:     LndCert,
 | 
			
		||||
		Network:     lndclient.NetworkRegtest,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Println(err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	lnd = &LndClient{LightningClient: rpcClient}
 | 
			
		||||
	if info, err := lnd.GetInfo(context.TODO(), &lnrpc.GetInfoRequest{}); err != nil {
 | 
			
		||||
		log.Printf("LND connection error: %v\n", err)
 | 
			
		||||
		return
 | 
			
		||||
	} else {
 | 
			
		||||
		version := strings.Split(info.Version, " ")[0]
 | 
			
		||||
		log.Printf("Connected to %s running LND v%s", LndHost, version)
 | 
			
		||||
		lndEnabled = true
 | 
			
		||||
	}
 | 
			
		||||
	lnd = &LndClient{GrpcLndServices: *rpcLndServices}
 | 
			
		||||
	ver := lnd.Version
 | 
			
		||||
	log.Printf("Connected to %s running LND v%s", LndHost, ver.Version)
 | 
			
		||||
	lndEnabled = true
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func lndGuard(next echo.HandlerFunc) echo.HandlerFunc {
 | 
			
		||||
@ -63,26 +66,46 @@ func lndGuard(next echo.HandlerFunc) echo.HandlerFunc {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (lnd *LndClient) GenerateNewPreimage() (lntypes.Preimage, error) {
 | 
			
		||||
	randomBytes := make([]byte, 32)
 | 
			
		||||
	_, err := io.ReadFull(rand.Reader, randomBytes)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return lntypes.Preimage{}, err
 | 
			
		||||
	}
 | 
			
		||||
	preimage, err := lntypes.MakePreimage(randomBytes)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return lntypes.Preimage{}, err
 | 
			
		||||
	}
 | 
			
		||||
	return preimage, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (lnd *LndClient) CreateInvoice(pubkey string, msats int) (*Invoice, error) {
 | 
			
		||||
	addInvoiceResponse, err := lnd.AddInvoice(context.TODO(), &lnrpc.Invoice{
 | 
			
		||||
		ValueMsat: int64(msats),
 | 
			
		||||
		Expiry:    3600,
 | 
			
		||||
	expiry := time.Hour
 | 
			
		||||
	preimage, err := lnd.GenerateNewPreimage()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	hash := preimage.Hash()
 | 
			
		||||
	paymentRequest, err := lnd.Invoices.AddHoldInvoice(context.TODO(), &invoicesrpc.AddInvoiceData{
 | 
			
		||||
		Hash:   &hash,
 | 
			
		||||
		Value:  lnwire.MilliSatoshi(msats),
 | 
			
		||||
		Expiry: int64(expiry),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	lnInvoice, err := lnd.LookupInvoice(context.TODO(), &lnrpc.PaymentHash{RHash: addInvoiceResponse.RHash})
 | 
			
		||||
	lnInvoice, err := lnd.Client.LookupInvoice(context.TODO(), hash)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	dbInvoice := Invoice{
 | 
			
		||||
		Session:        Session{pubkey},
 | 
			
		||||
		Msats:          msats,
 | 
			
		||||
		Preimage:       hex.EncodeToString(lnInvoice.RPreimage),
 | 
			
		||||
		PaymentRequest: lnInvoice.PaymentRequest,
 | 
			
		||||
		PaymentHash:    hex.EncodeToString(lnInvoice.RHash),
 | 
			
		||||
		CreatedAt:      time.Unix(lnInvoice.CreationDate, 0),
 | 
			
		||||
		ExpiresAt:      time.Unix(lnInvoice.CreationDate+lnInvoice.Expiry, 0),
 | 
			
		||||
		Preimage:       preimage.String(),
 | 
			
		||||
		PaymentRequest: paymentRequest,
 | 
			
		||||
		PaymentHash:    hash.String(),
 | 
			
		||||
		CreatedAt:      lnInvoice.CreationDate,
 | 
			
		||||
		ExpiresAt:      lnInvoice.CreationDate.Add(expiry),
 | 
			
		||||
	}
 | 
			
		||||
	if err := db.CreateInvoice(&dbInvoice); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
@ -90,40 +113,65 @@ func (lnd *LndClient) CreateInvoice(pubkey string, msats int) (*Invoice, error)
 | 
			
		||||
	return &dbInvoice, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (lnd *LndClient) CheckInvoice(hash string) {
 | 
			
		||||
func (lnd *LndClient) CheckInvoice(hash lntypes.Hash) {
 | 
			
		||||
	if !lndEnabled {
 | 
			
		||||
		log.Printf("LND disabled, skipping checking invoice: hash=%s", hash)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var invoice Invoice
 | 
			
		||||
	if err := db.FetchInvoice(&FetchInvoiceWhere{Hash: hash.String()}, &invoice); err != nil {
 | 
			
		||||
		log.Println(err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	loopPause := 5 * time.Second
 | 
			
		||||
	handleLoopError := func(err error) {
 | 
			
		||||
		log.Println(err)
 | 
			
		||||
		time.Sleep(loopPause)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		log.Printf("lookup invoice: hash=%s", hash)
 | 
			
		||||
		invoice, err := lnd.LookupInvoice(context.TODO(), &lnrpc.PaymentHash{RHashStr: hash})
 | 
			
		||||
		lnInvoice, err := lnd.Client.LookupInvoice(context.TODO(), hash)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			time.Sleep(5 * time.Second)
 | 
			
		||||
			handleLoopError(err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if time.Now().After(time.Unix(invoice.CreationDate+invoice.Expiry, 0)) {
 | 
			
		||||
		if time.Now().After(invoice.ExpiresAt) {
 | 
			
		||||
			if err := lnd.Invoices.CancelInvoice(context.TODO(), hash); err != nil {
 | 
			
		||||
				handleLoopError(err)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			log.Printf("invoice expired: hash=%s", hash)
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		if invoice.SettleDate != 0 && invoice.AmtPaidMsat > 0 {
 | 
			
		||||
			if err := db.ConfirmInvoice(hash, time.Unix(invoice.SettleDate, 0), int(invoice.AmtPaidMsat)); err != nil {
 | 
			
		||||
				log.Println(err)
 | 
			
		||||
				time.Sleep(5 * time.Second)
 | 
			
		||||
		if lnInvoice.AmountPaid > 0 {
 | 
			
		||||
			preimage, err := lntypes.MakePreimageFromStr(invoice.Preimage)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				handleLoopError(err)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			// TODO settle invoice after matching order was found
 | 
			
		||||
			if err := lnd.Invoices.SettleInvoice(context.TODO(), preimage); err != nil {
 | 
			
		||||
				handleLoopError(err)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			if err := db.ConfirmInvoice(hash.String(), time.Now(), int(lnInvoice.AmountPaid)); err != nil {
 | 
			
		||||
				handleLoopError(err)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			log.Printf("invoice confirmed: hash=%s", hash)
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		time.Sleep(5 * time.Second)
 | 
			
		||||
		time.Sleep(loopPause)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func invoice(c echo.Context) error {
 | 
			
		||||
	invoiceId := c.Param("id")
 | 
			
		||||
	var invoice Invoice
 | 
			
		||||
	if err := db.FetchInvoice(invoiceId, &invoice); err == sql.ErrNoRows {
 | 
			
		||||
	if err := db.FetchInvoice(&FetchInvoiceWhere{Id: invoiceId}, &invoice); err == sql.ErrNoRows {
 | 
			
		||||
		return echo.NewHTTPError(http.StatusNotFound, "Not Found")
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
@ -132,7 +180,11 @@ func invoice(c echo.Context) error {
 | 
			
		||||
	if invoice.Pubkey != session.Pubkey {
 | 
			
		||||
		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
 | 
			
		||||
	}
 | 
			
		||||
	go lnd.CheckInvoice(invoice.PaymentHash)
 | 
			
		||||
	hash, err := lntypes.MakeHashFromStr(invoice.PaymentHash)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	go lnd.CheckInvoice(hash)
 | 
			
		||||
	qr, err := ToQR(invoice.PaymentRequest)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
@ -156,7 +208,7 @@ func invoice(c echo.Context) error {
 | 
			
		||||
func invoiceStatus(c echo.Context) error {
 | 
			
		||||
	invoiceId := c.Param("id")
 | 
			
		||||
	var invoice Invoice
 | 
			
		||||
	if err := db.FetchInvoice(invoiceId, &invoice); err == sql.ErrNoRows {
 | 
			
		||||
	if err := db.FetchInvoice(&FetchInvoiceWhere{Id: invoiceId}, &invoice); err == sql.ErrNoRows {
 | 
			
		||||
		return echo.NewHTTPError(http.StatusNotFound, "Not Found")
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/labstack/echo/v4"
 | 
			
		||||
	"github.com/lightningnetwork/lnd/lntypes"
 | 
			
		||||
	"gopkg.in/guregu/null.v4"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -90,7 +91,11 @@ func order(c echo.Context) error {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	go lnd.CheckInvoice(invoice.PaymentHash)
 | 
			
		||||
	hash, err := lntypes.MakeHashFromStr(invoice.PaymentHash)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	go lnd.CheckInvoice(hash)
 | 
			
		||||
	data := map[string]any{
 | 
			
		||||
		"session":     c.Get("session"),
 | 
			
		||||
		"ENV":         ENV,
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,23 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"log"
 | 
			
		||||
 | 
			
		||||
	"github.com/lightningnetwork/lnd/lntypes"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func RunJobs() error {
 | 
			
		||||
	var invoices []Invoice
 | 
			
		||||
	if err := db.FetchInvoices(&FetchInvoicesWhere{Expired: false}, &invoices); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	for _, inv := range invoices {
 | 
			
		||||
		go lnd.CheckInvoice(inv.PaymentHash)
 | 
			
		||||
		hash, err := lntypes.MakeHashFromStr(inv.PaymentHash)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		go lnd.CheckInvoice(hash)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user