refactor: Structure code into different packages

I have put too much code into the same files.

Also, I put everything into the same package: main.

This package is only meant for executables.

Therefore, I have refactored my code to use multiple packages. These packages also guarantee separation of concerns since Golang doesn't allow cyclic imports.
This commit is contained in:
ekzyis 2023-09-10 17:39:34 +02:00
parent b4a8adcb9a
commit 7558655458
43 changed files with 1326 additions and 1047 deletions

View File

@ -1,8 +1,12 @@
.PHONY: build run
SOURCE := $(shell find db env lib lnd pages public server -type f)
build: delphi.market
delphi.market: src/*.go
go build -o delphi.market ./src/
delphi.market: $(SOURCE)
go build -o delphi.market .
run:
go run ./src/
go run .

45
db/db.go Normal file
View File

@ -0,0 +1,45 @@
package db
import (
"database/sql"
"log"
"github.com/joho/godotenv"
_ "github.com/lib/pq"
"github.com/namsral/flag"
)
var (
db *DB
)
type DB struct {
*sql.DB
}
func init() {
if err := godotenv.Load(); err != nil {
log.Fatalf("error loading env vars: %s", err)
}
var dbUrl string
flag.StringVar(&dbUrl, "DATABASE_URL", "", "Database URL")
flag.Parse()
if dbUrl == "" {
log.Fatal("DATABASE_URL not set")
}
db = initDB(dbUrl)
}
func initDB(url string) *DB {
db, err := sql.Open("postgres", url)
if err != nil {
log.Fatal(err)
}
// test connection
_, err = db.Exec("SELECT 1")
if err != nil {
log.Fatal(err)
}
// TODO: run migrations
return &DB{DB: db}
}

46
db/invoice.go Normal file
View File

@ -0,0 +1,46 @@
package db
import "time"
func CreateInvoice(invoice *Invoice) error {
if err := db.QueryRow(""+
"INSERT INTO invoices(pubkey, msats, preimage, hash, bolt11, created_at, expires_at) "+
"VALUES($1, $2, $3, $4, $5, $6, $7) "+
"RETURNING id",
invoice.Pubkey, invoice.Msats, invoice.Preimage, invoice.Hash, invoice.PaymentRequest, invoice.CreatedAt, invoice.ExpiresAt).Scan(&invoice.Id); err != nil {
return err
}
return nil
}
type FetchInvoiceWhere struct {
Id string
Hash string
}
func FetchInvoice(where *FetchInvoiceWhere, invoice *Invoice) error {
var (
query = "SELECT id, pubkey, msats, preimage, hash, bolt11, created_at, expires_at, confirmed_at, held_since FROM invoices "
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.Hash,
&invoice.PaymentRequest, &invoice.CreatedAt, &invoice.ExpiresAt, &invoice.ConfirmedAt, &invoice.HeldSince); err != nil {
return err
}
return nil
}
func ConfirmInvoice(hash string, confirmedAt time.Time, msatsReceived int) error {
if _, err := db.Exec("UPDATE invoices SET confirmed_at = $2, msats_received = $3 WHERE hash = $1", hash, confirmedAt, msatsReceived); err != nil {
return err
}
return nil
}

18
db/lnauth.go Normal file
View File

@ -0,0 +1,18 @@
package db
func CreateLNAuth(lnAuth *LNAuth) error {
err := db.QueryRow(
"INSERT INTO lnauth(k1, lnurl) VALUES($1, $2) RETURNING session_id",
lnAuth.K1, lnAuth.LNURL).Scan(&lnAuth.SessionId)
return err
}
func FetchSessionId(k1 string, sessionId *string) error {
err := db.QueryRow("SELECT session_id FROM lnauth WHERE k1 = $1", k1).Scan(sessionId)
return err
}
func DeleteLNAuth(lnAuth *LNAuth) error {
_, err := db.Exec("DELETE FROM lnauth WHERE k1 = $1", lnAuth.K1)
return err
}

90
db/market.go Normal file
View File

@ -0,0 +1,90 @@
package db
import "database/sql"
func FetchMarket(marketId int, market *Market) error {
if err := db.QueryRow("SELECT id, description FROM markets WHERE id = $1", marketId).Scan(&market.Id, &market.Description); err != nil {
return err
}
return nil
}
func FetchActiveMarkets(markets *[]Market) error {
var (
rows *sql.Rows
market Market
err error
)
if rows, err = db.Query("SELECT id, description, active FROM markets WHERE active = true"); err != nil {
return err
}
defer rows.Close()
for rows.Next() {
rows.Scan(&market.Id, &market.Description, &market.Active)
*markets = append(*markets, market)
}
return nil
}
func FetchShares(marketId int, shares *[]Share) error {
rows, err := db.Query("SELECT id, market_id, description FROM shares WHERE market_id = $1 ORDER BY description DESC", marketId)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var share Share
rows.Scan(&share.Id, &share.MarketId, &share.Description)
*shares = append(*shares, share)
}
return nil
}
type FetchOrdersWhere struct {
MarketId int
Pubkey string
Confirmed bool
}
func FetchOrders(where *FetchOrdersWhere, orders *[]Order) error {
query := "" +
"SELECT o.id, share_id, o.pubkey, o.side, o.quantity, o.price, o.invoice_id, o.created_at, s.description, s.market_id, i.confirmed_at " +
"FROM orders o " +
"JOIN invoices i ON o.invoice_id = i.id " +
"JOIN shares s ON o.share_id = s.id " +
"WHERE "
var args []any
if where.MarketId > 0 {
query += "share_id = ANY(SELECT id FROM shares WHERE market_id = $1) "
args = append(args, where.MarketId)
} else if where.Pubkey != "" {
query += "o.pubkey = $1 "
args = append(args, where.Pubkey)
}
if where.Confirmed {
query += "AND i.confirmed_at IS NOT NULL "
}
query += "AND (i.confirmed_at IS NOT NULL OR i.expires_at > CURRENT_TIMESTAMP) "
query += "ORDER BY price DESC"
rows, err := db.Query(query, args...)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var order Order
rows.Scan(&order.Id, &order.ShareId, &order.Pubkey, &order.Side, &order.Quantity, &order.Price, &order.InvoiceId, &order.CreatedAt, &order.Share.Description, &order.Share.MarketId, &order.Invoice.ConfirmedAt)
*orders = append(*orders, order)
}
return nil
}
func CreateOrder(order *Order) error {
if _, err := db.Exec(""+
"INSERT INTO orders(share_id, pubkey, side, quantity, price, invoice_id) "+
"VALUES ($1, $2, $3, $4, $5, $6)",
order.ShareId, order.Pubkey, order.Side, order.Quantity, order.Price, order.InvoiceId); err != nil {
return err
}
return nil
}

16
db/session.go Normal file
View File

@ -0,0 +1,16 @@
package db
func CreateSession(s *Session) error {
_, err := db.Exec("INSERT INTO sessions(pubkey, session_id) VALUES($1, $2)", s.Pubkey, s.SessionId)
return err
}
func FetchSession(s *Session) error {
err := db.QueryRow("SELECT pubkey FROM sessions WHERE session_id = $1", s.SessionId).Scan(&s.Pubkey)
return err
}
func DeleteSession(s *Session) error {
_, err := db.Exec("DELETE FROM sessions where session_id = $1", s.SessionId)
return err
}

66
db/types.go Normal file
View File

@ -0,0 +1,66 @@
package db
import (
"time"
"gopkg.in/guregu/null.v4"
)
type Serial = int
type UUID = string
type LNAuth struct {
K1 string
LNURL string
CreatdAt time.Time
SessionId string
}
type User struct {
Pubkey string
LastSeen time.Time
}
type Session struct {
Pubkey string
SessionId string
}
type Market struct {
Id Serial
Description string
Active bool
}
type Share struct {
Id UUID
MarketId int
Description string
}
type Invoice struct {
Id UUID
Pubkey string
Msats int64
MsatsReceived int64
Preimage string
Hash string
PaymentRequest string
CreatedAt time.Time
ExpiresAt time.Time
ConfirmedAt null.Time
HeldSince null.Time
}
type Order struct {
Id UUID
CreatedAt time.Time
ShareId string `form:"share_id"`
Share
Pubkey string
Side string `form:"side"`
Quantity int64 `form:"quantity"`
Price int64 `form:"price"`
InvoiceId UUID
Invoice
}

13
db/user.go Normal file
View File

@ -0,0 +1,13 @@
package db
func CreateUser(u *User) error {
_, err := db.Exec(
"INSERT INTO users(pubkey) VALUES ($1) ON CONFLICT(pubkey) DO UPDATE SET last_seen = CURRENT_TIMESTAMP",
u.Pubkey)
return err
}
func UpdateUser(u *User) error {
_, err := db.Exec("UPDATE users SET last_seen = $1 WHERE pubkey = $2", u.LastSeen, u.Pubkey)
return err
}

42
env/env.go vendored Normal file
View File

@ -0,0 +1,42 @@
package env
import (
"fmt"
"log"
"os/exec"
"strings"
"github.com/joho/godotenv"
"github.com/namsral/flag"
)
var (
Port int
PublicURL string
Env string
CommitLongSha string
CommitShortSha string
Version string
)
func init() {
if err := godotenv.Load(); err != nil {
log.Fatalf("error loading env vars: %s", err)
}
flag.StringVar(&PublicURL, "PUBLIC_URL", "delphi.market", "Public URL of website")
flag.IntVar(&Port, "PORT", 4321, "Server port")
flag.StringVar(&Env, "ENV", "development", "Specify for which environment files should be built")
flag.Parse()
CommitLongSha = execCmd("git", "rev-parse", "HEAD")
CommitShortSha = execCmd("git", "rev-parse", "--short", "HEAD")
Version = fmt.Sprintf("v0.0.0+%s", CommitShortSha)
}
func execCmd(name string, args ...string) string {
cmd := exec.Command(name, args...)
stdout, err := cmd.Output()
if err != nil {
log.Fatal(err)
}
return strings.TrimSpace(string(stdout))
}

14
go.mod
View File

@ -1,16 +1,21 @@
module delphi.market
module git.ekzyis.com/ekzyis/delphi.market
go 1.20
replace git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e => gitlab.com/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e
require (
github.com/btcsuite/btcd/btcec/v2 v2.2.2
github.com/btcsuite/btcutil v1.0.2
github.com/joho/godotenv v1.5.1
github.com/labstack/echo/v4 v4.11.1
github.com/lib/pq v1.10.9
github.com/lightninglabs/lndclient v1.0.0
github.com/lightningnetwork/lnd v0.10.0-beta.rc6.0.20200615174244-103c59a4889f
github.com/namsral/flag v1.7.4-pre
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
gopkg.in/guregu/null.v4 v4.0.0
)
require (
@ -55,10 +60,8 @@ require (
github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
github.com/lightninglabs/lndclient v1.0.0 // indirect
github.com/lightninglabs/neutrino v0.11.1-0.20200316235139-bffc52e8f200 // indirect
github.com/lightningnetwork/lightning-onion v1.0.2-0.20200501022730-3c8c8d0b89ea // indirect
github.com/lightningnetwork/lnd v0.10.0-beta.rc6.0.20200615174244-103c59a4889f // indirect
github.com/lightningnetwork/lnd/clock v1.0.1 // indirect
github.com/lightningnetwork/lnd/queue v1.0.4 // indirect
github.com/lightningnetwork/lnd/ticker v1.0.0 // indirect
@ -85,14 +88,13 @@ require (
go.uber.org/zap v1.14.1 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/term v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c // indirect
google.golang.org/grpc v1.24.0 // indirect
gopkg.in/errgo.v1 v1.0.1 // indirect
gopkg.in/guregu/null.v4 v4.0.0 // indirect
gopkg.in/macaroon-bakery.v2 v2.0.1 // indirect
gopkg.in/macaroon.v2 v2.1.0 // indirect
gopkg.in/yaml.v2 v2.2.3 // indirect

35
go.sum
View File

@ -1,5 +1,5 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e/go.mod h1:BWqTsj8PgcPriQJGl7el20J/7TuT1d/hSyFDXMEpoEo=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ=
github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82/go.mod h1:GbuBk21JqF+driLX3XtJYNZjGa45YDoa9IqCTzNSfEc=
@ -24,7 +24,6 @@ github.com/btcsuite/btcd v0.20.1-beta.0.20200515232429-9f0179fd2c46 h1:QyTpiR5nQ
github.com/btcsuite/btcd v0.20.1-beta.0.20200515232429-9f0179fd2c46/go.mod h1:Yktc19YNjh/Iz2//CX0vfRTS4IJKM/RKO5YZ9Fn+Pgo=
github.com/btcsuite/btcd/btcec/v2 v2.2.2 h1:5uxe5YjoCq+JeOpg0gZSNHuFgeogrocBYxvg6w9sAgc=
github.com/btcsuite/btcd/btcec/v2 v2.2.2/go.mod h1:9/CSmJxmuvqzX9Wh2fXMWToLOHhPd11lSPuIupwTkI8=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
@ -51,8 +50,10 @@ github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JG
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8/go.mod h1:tYvUd8KLhm/oXvUeSEs2VlLghFjQt9+ZaF9ghH0JNjc=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/goleveldb v1.0.0 h1:Tvd0BfvqX9o823q1j2UZ/epQo09eJh6dTcRp79ilIN4=
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJGQE=
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
@ -81,6 +82,7 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k=
github.com/frankban/quicktest v1.2.2 h1:xfmOhhoH5fGPgbEAlhLpJH9p0z/0Qizio9osmvn9IUY=
github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@ -96,7 +98,9 @@ github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@ -109,6 +113,7 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
@ -135,13 +140,19 @@ github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c h1:3UvYABOQRhJAApj9MdCN+Ydv841ETSoy6xLzdmmr/9A=
github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA=
github.com/juju/errors v0.0.0-20190806202954-0232dcc7464d h1:hJXjZMxj0SWlMoQkzeZDLi2cmeiWKa7y1B8Rg+qaoEc=
github.com/juju/errors v0.0.0-20190806202954-0232dcc7464d/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 h1:UUHMLvzt/31azWTN/ifGWef4WUqvXk0iRqdhdy/2uzI=
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
github.com/juju/retry v0.0.0-20180821225755-9058e192b216 h1:/eQL7EJQKFHByJe3DeE8Z36yqManj9UY5zppDoQi4FU=
github.com/juju/retry v0.0.0-20180821225755-9058e192b216/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4=
github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2 h1:Pp8RxiF4rSoXP9SED26WCfNB28/dwTDpPXS8XMJR8rc=
github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d h1:irPlN9z5VCe6BTsqVsxheCZH99OFSmqSVyTigW4mEoY=
github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk=
github.com/juju/version v0.0.0-20180108022336-b64dbd566305 h1:lQxPJ1URr2fjsKnJRt/BxiIxjLt9IKGvS+0injMHbag=
github.com/juju/version v0.0.0-20180108022336-b64dbd566305/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@ -151,8 +162,10 @@ github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec/go.mod h1:J+Gs4SYgM6
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4=
github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ=
@ -210,6 +223,7 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -257,12 +271,15 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
gitlab.com/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e/go.mod h1:PIe5G60AUtC0u4HgbjMVEeRR1EUGMMBl6a0gB1tRdDk=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.14.1 h1:nYDKopTbvAPq/NrUVZwT15y2lpROBiLLyoRTbXOYWOo=
go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
@ -277,14 +294,16 @@ golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -320,8 +339,8 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -339,6 +358,7 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -353,6 +373,7 @@ google.golang.org/grpc v1.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s=
google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v1 v1.0.1 h1:oQFRXzZ7CkBGdm1XZm/EbQYaYNNEElNBOd09M6cqNso=
gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo=
@ -365,6 +386,7 @@ gopkg.in/macaroon-bakery.v2 v2.0.1/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETf
gopkg.in/macaroon.v2 v2.0.0/go.mod h1:+I6LnTMkm/uV5ew/0nsulNjL16SK4+C8yDmRUzHR17I=
gopkg.in/macaroon.v2 v2.1.0 h1:HZcsjBCzq9t0eBPMKqTN/uSN6JOm78ZJ2INbqcBQOUI=
gopkg.in/macaroon.v2 v2.1.0/go.mod h1:OUb+TQP/OP0WOerC2Jp/3CwhIKyIa9kQjuc7H24e6/o=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@ -377,6 +399,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=

View File

@ -13,10 +13,9 @@ function restart_server() {
PID=$(pidof delphi.market)
}
function sync() {
function restart() {
restart_server
date +%s.%N > public/hotreload
rsync -avh public/ dev1.delphi.market:/var/www/dev1.delphi --delete
}
function cleanup() {
@ -25,9 +24,9 @@ function cleanup() {
}
trap cleanup EXIT
sync
restart
tail -f server.log &
while inotifywait -r -e modify src/ pages/; do
sync
while inotifywait -r -e modify db/ env/ lib/ lnd/ pages/ public/ server/; do
restart
done

8
lib/max.go Normal file
View File

@ -0,0 +1,8 @@
package lib
func Max(x, y int) int {
if x < y {
return y
}
return x
}

7
lib/merge.go Normal file
View File

@ -0,0 +1,7 @@
package lib
func Merge[T comparable](target *map[T]any, src *map[T]any) {
for k, v := range *src {
(*target)[k] = v
}
}

View File

@ -1,4 +1,4 @@
package main
package lib
import (
"encoding/base64"
@ -6,13 +6,6 @@ import (
"github.com/skip2/go-qrcode"
)
func Max(x, y int) int {
if x < y {
return y
}
return x
}
func ToQR(s string) (string, error) {
png, err := qrcode.Encode(s, qrcode.Medium, 256)
if err != nil {

112
lnd/invoice.go Normal file
View File

@ -0,0 +1,112 @@
package lnd
import (
"context"
"log"
"time"
"git.ekzyis.com/ekzyis/delphi.market/db"
"github.com/lightninglabs/lndclient"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
)
func CreateInvoice(pubkey string, msats int64) (*db.Invoice, error) {
var (
expiry time.Duration = time.Hour
preimage lntypes.Preimage
hash lntypes.Hash
paymentRequest string
lnInvoice *lndclient.Invoice
dbInvoice *db.Invoice
err error
)
if preimage, err = generateNewPreimage(); err != nil {
return nil, err
}
hash = preimage.Hash()
if paymentRequest, err = lnd.Invoices.AddHoldInvoice(context.TODO(), &invoicesrpc.AddInvoiceData{
Hash: &hash,
Value: lnwire.MilliSatoshi(msats),
Expiry: int64(expiry / time.Millisecond),
}); err != nil {
return nil, err
}
if lnInvoice, err = lnd.Client.LookupInvoice(context.TODO(), hash); err != nil {
return nil, err
}
dbInvoice = &db.Invoice{
Pubkey: pubkey,
Msats: msats,
Preimage: preimage.String(),
PaymentRequest: paymentRequest,
Hash: hash.String(),
CreatedAt: lnInvoice.CreationDate,
ExpiresAt: lnInvoice.CreationDate.Add(expiry),
}
if err := db.CreateInvoice(dbInvoice); err != nil {
return nil, err
}
return dbInvoice, nil
}
func CheckInvoice(hash lntypes.Hash) {
var (
pollInterval = 5 * time.Second
invoice db.Invoice
lnInvoice *lndclient.Invoice
preimage lntypes.Preimage
err error
)
if !Enabled {
log.Printf("LND disabled, skipping checking invoice: hash=%s", hash)
return
}
if err = db.FetchInvoice(&db.FetchInvoiceWhere{Hash: hash.String()}, &invoice); err != nil {
log.Println(err)
return
}
handleLoopError := func(err error) {
log.Println(err)
time.Sleep(pollInterval)
}
for {
log.Printf("lookup invoice: hash=%s", hash)
if lnInvoice, err = lnd.Client.LookupInvoice(context.TODO(), hash); err != nil {
handleLoopError(err)
continue
}
if time.Now().After(invoice.ExpiresAt) {
// cancel invoices after expiration if no matching order found yet
if err = lnd.Invoices.CancelInvoice(context.TODO(), hash); err != nil {
handleLoopError(err)
continue
}
log.Printf("invoice expired: hash=%s", hash)
break
}
if lnInvoice.AmountPaid > 0 {
if preimage, err = lntypes.MakePreimageFromStr(invoice.Preimage); 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(pollInterval)
}
}

21
lnd/lib.go Normal file
View File

@ -0,0 +1,21 @@
package lnd
import (
"crypto/rand"
"io"
"github.com/lightningnetwork/lnd/lntypes"
)
func 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
}

49
lnd/lnd.go Normal file
View File

@ -0,0 +1,49 @@
package lnd
import (
"log"
"github.com/joho/godotenv"
"github.com/lightninglabs/lndclient"
"github.com/namsral/flag"
)
var (
lnd *LNDClient
Enabled bool
)
type LNDClient struct {
lndclient.GrpcLndServices
}
func init() {
var (
lndCert string
lndMacaroonDir string
lndHost string
rpcLndServices *lndclient.GrpcLndServices
err error
)
if err = godotenv.Load(); err != nil {
log.Fatalf("error loading env vars: %s", err)
}
flag.StringVar(&lndCert, "LND_CERT", "", "Path to LND TLS certificate")
flag.StringVar(&lndMacaroonDir, "LND_MACAROON_DIR", "", "LND macaroon directory")
flag.StringVar(&lndHost, "LND_HOST", "localhost:10001", "LND gRPC server address")
flag.Parse()
if rpcLndServices, err = lndclient.NewLndServices(&lndclient.LndServicesConfig{
LndAddress: lndHost,
MacaroonDir: lndMacaroonDir,
TLSPath: lndCert,
// TODO: make network configurable
Network: lndclient.NetworkRegtest,
}); err != nil {
log.Println(err)
Enabled = false
return
}
lnd = &LNDClient{GrpcLndServices: *rpcLndServices}
log.Printf("Connected to %s running LND v%s", lndHost, lnd.Version.Version)
Enabled = true
}

1
lnd/types.go Normal file
View File

@ -0,0 +1 @@
package lnd

28
main.go Normal file
View File

@ -0,0 +1,28 @@
package main
import (
"fmt"
"log"
"net/http"
"git.ekzyis.com/ekzyis/delphi.market/env"
"git.ekzyis.com/ekzyis/delphi.market/server"
)
var (
s *server.Server
)
func init() {
log.Printf("Commit: %s", env.CommitShortSha)
log.Printf("Public URL: %s", env.PublicURL)
log.Printf("Environment: %s", env.Env)
s = server.NewServer()
}
func main() {
if err := s.Start(fmt.Sprintf("%s:%d", "127.0.0.1", env.Port)); err != http.ErrServerClosed {
log.Fatal(err)
}
}

View File

@ -51,11 +51,11 @@
<div class="font-mono word-wrap mb-1">{{.lnurl}}</div>
<details class="font-mono mb-1 align-left">
<summary>details</summary>
<div>id: {{.Invoice.Id}}</div>
<div>amount: {{div .Invoice.Msats 1000}} sats</div>
<div>created: {{.Invoice.CreatedAt}}</div>
<div>expiry : {{.Invoice.ExpiresAt}}</div>
<div class="word-wrap">hash: {{.Invoice.PaymentHash}}</div>
<div>id: {{.invoice.Id}}</div>
<div>amount: {{div .invoice.Msats 1000}} sats</div>
<div>created: {{.invoice.CreatedAt}}</div>
<div>expiry : {{.invoice.ExpiresAt}}</div>
<div class="word-wrap">hash: {{.invoice.Hash}}</div>
</details>
</div>
</div>
@ -63,10 +63,10 @@
<script>
const statusElement = document.querySelector("#status")
const label = document.querySelector("#status-label")
const status = "{{.Status}}"
const redirectUrl = "{{.RedirectURL}}"
const status = "{{.status}}"
const redirectUrl = "{{.redirectURL}}"
function poll() {
const invoiceId = "{{.Invoice.Id}}"
const invoiceId = "{{.invoice.Id}}"
const countdown = document.querySelector("#countdown")
const redirect = () => {
clearInterval(interval)

65
server/auth/lnauth.go Normal file
View File

@ -0,0 +1,65 @@
package auth
import (
"crypto/ecdsa"
"crypto/rand"
"encoding/hex"
"fmt"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcutil/bech32"
"git.ekzyis.com/ekzyis/delphi.market/env"
)
type LNAuth struct {
K1 string
LNURL string
}
type LNAuthResponse struct {
K1 string `query:"k1"`
Sig string `query:"sig"`
Key string `query:"key"`
}
func NewLNAuth() (*LNAuth, error) {
k1 := make([]byte, 32)
_, err := rand.Read(k1)
if err != nil {
return nil, fmt.Errorf("rand.Read error: %w", err)
}
k1hex := hex.EncodeToString(k1)
url := []byte(fmt.Sprintf("https://%s/api/login?tag=login&k1=%s&action=login", env.PublicURL, k1hex))
conv, err := bech32.ConvertBits(url, 8, 5, true)
if err != nil {
return nil, fmt.Errorf("bech32.ConvertBits error: %w", err)
}
lnurl, err := bech32.Encode("lnurl", conv)
if err != nil {
return nil, fmt.Errorf("bech32.Encode error: %w", err)
}
return &LNAuth{k1hex, lnurl}, nil
}
func VerifyLNAuth(r *LNAuthResponse) (bool, error) {
var k1Bytes, sigBytes, keyBytes []byte
k1Bytes, err := hex.DecodeString(r.K1)
if err != nil {
return false, fmt.Errorf("k1 decode error: %w", err)
}
sigBytes, err = hex.DecodeString(r.Sig)
if err != nil {
return false, fmt.Errorf("sig decode error: %w", err)
}
keyBytes, err = hex.DecodeString(r.Key)
if err != nil {
return false, fmt.Errorf("key decode error: %w", err)
}
key, err := btcec.ParsePubKey(keyBytes)
if err != nil {
return false, fmt.Errorf("key parse error: %w", err)
}
ecdsaKey := ecdsa.PublicKey{Curve: btcec.S256(), X: key.X(), Y: key.Y()}
return ecdsa.VerifyASN1(&ecdsaKey, k1Bytes, sigBytes), nil
}

45
server/error.go Normal file
View File

@ -0,0 +1,45 @@
package server
import (
"fmt"
"net/http"
"os"
"github.com/labstack/echo/v4"
)
func httpErrorHandler(err error, c echo.Context) {
c.Logger().Error(err)
code := http.StatusInternalServerError
if httpError, ok := err.(*echo.HTTPError); ok {
code = httpError.Code
}
filePath := fmt.Sprintf("public/%d.html", code)
var f *os.File
if f, err = os.Open(filePath); err != nil {
c.Logger().Error(err)
serveError(c, 500)
return
}
if err = c.Stream(code, "text/html", f); err != nil {
c.Logger().Error(err)
serveError(c, 500)
return
}
}
func serveError(c echo.Context, code int) error {
var (
f *os.File
err error
)
if f, err = os.Open(fmt.Sprintf("public/%d.html", code)); err != nil {
c.Logger().Error(err)
return err
}
if err = c.Stream(code, "text/html", f); err != nil {
c.Logger().Error(err)
return err
}
return nil
}

View File

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"git.ekzyis.com/ekzyis/delphi.market/db"
"git.ekzyis.com/ekzyis/delphi.market/lib"
"github.com/labstack/echo/v4"
)
func HandleIndex(envVars map[string]any) echo.HandlerFunc {
return func(c echo.Context) error {
var (
markets []db.Market
err error
data map[string]any
)
if err = db.FetchActiveMarkets(&markets); err != nil {
return err
}
data = map[string]any{
"session": c.Get("session"),
"markets": markets,
}
lib.Merge(&data, &envVars)
return c.Render(http.StatusOK, "index.html", data)
}
}

View File

@ -0,0 +1,78 @@
package handler
import (
"database/sql"
"net/http"
"time"
"git.ekzyis.com/ekzyis/delphi.market/db"
"git.ekzyis.com/ekzyis/delphi.market/lib"
"git.ekzyis.com/ekzyis/delphi.market/lnd"
"github.com/labstack/echo/v4"
"github.com/lightningnetwork/lnd/lntypes"
)
func HandleInvoiceAPI(envVars map[string]any) echo.HandlerFunc {
return func(c echo.Context) error {
var (
invoiceId string
invoice db.Invoice
u db.User
err error
)
invoiceId = c.Param("id")
if err = db.FetchInvoice(&db.FetchInvoiceWhere{Id: invoiceId}, &invoice); err == sql.ErrNoRows {
return echo.NewHTTPError(http.StatusNotFound)
} else if err != nil {
return err
}
if u = c.Get("session").(db.User); invoice.Pubkey != u.Pubkey {
return echo.NewHTTPError(http.StatusUnauthorized)
}
invoice.Preimage = ""
return c.JSON(http.StatusOK, invoice)
}
}
func HandleInvoice(envVars map[string]any) echo.HandlerFunc {
return func(c echo.Context) error {
var (
invoiceId string
invoice db.Invoice
u db.User
hash lntypes.Hash
qr string
status string
err error
)
invoiceId = c.Param("id")
if err = db.FetchInvoice(&db.FetchInvoiceWhere{Id: invoiceId}, &invoice); err == sql.ErrNoRows {
return echo.NewHTTPError(http.StatusNotFound)
} else if err != nil {
return err
}
if u = c.Get("session").(db.User); invoice.Pubkey != u.Pubkey {
return echo.NewHTTPError(http.StatusUnauthorized)
}
if hash, err = lntypes.MakeHashFromStr(invoice.Hash); err != nil {
return err
}
go lnd.CheckInvoice(hash)
if qr, err = lib.ToQR(invoice.PaymentRequest); err != nil {
return err
}
if invoice.ConfirmedAt.Valid {
status = "Paid"
} else if time.Now().After(invoice.ExpiresAt) {
status = "Expired"
}
data := map[string]any{
"session": c.Get("session"),
"invoice": invoice,
"status": status,
"lnurl": invoice.PaymentRequest,
"qr": qr,
}
return c.Render(http.StatusOK, "invoice.html", data)
}
}

View File

@ -0,0 +1,76 @@
package handler
import (
"database/sql"
"net/http"
"time"
"git.ekzyis.com/ekzyis/delphi.market/db"
"git.ekzyis.com/ekzyis/delphi.market/lib"
"git.ekzyis.com/ekzyis/delphi.market/server/auth"
"github.com/labstack/echo/v4"
)
func HandleLogin(envVars map[string]any) echo.HandlerFunc {
return func(c echo.Context) error {
var (
lnAuth *auth.LNAuth
dbLnAuth db.LNAuth
err error
expires time.Time = time.Now().Add(60 * 60 * 24 * 365 * time.Second)
qr string
data map[string]any
)
if lnAuth, err = auth.NewLNAuth(); err != nil {
return err
}
dbLnAuth = db.LNAuth{K1: lnAuth.K1, LNURL: lnAuth.LNURL}
if err = db.CreateLNAuth(&dbLnAuth); err != nil {
return err
}
c.SetCookie(&http.Cookie{Name: "session", HttpOnly: true, Path: "/", Value: dbLnAuth.SessionId, Secure: true, Expires: expires})
if qr, err = lib.ToQR(lnAuth.LNURL); err != nil {
return err
}
data = map[string]any{
"session": c.Get("session"),
"lnurl": lnAuth.LNURL,
"qr": qr,
}
lib.Merge(&data, &envVars)
return c.Render(http.StatusOK, "login.html", data)
}
}
func HandleLoginCallback(envVars map[string]any) echo.HandlerFunc {
return func(c echo.Context) error {
var (
query auth.LNAuthResponse
sessionId string
err error
)
if err := c.Bind(&query); err != nil {
return echo.NewHTTPError(http.StatusBadRequest)
}
if err = db.FetchSessionId(query.K1, &sessionId); err == sql.ErrNoRows {
return echo.NewHTTPError(http.StatusNotFound, map[string]string{"reason": "session not found"})
} else if err != nil {
return err
}
if ok, err := auth.VerifyLNAuth(&query); err != nil {
return err
} else if !ok {
return echo.NewHTTPError(http.StatusBadRequest, map[string]string{"reason": "bad signature"})
}
if err = db.CreateUser(&db.User{Pubkey: query.Key}); err != nil {
return err
}
if err = db.CreateSession(&db.Session{Pubkey: query.Key, SessionId: sessionId}); err != nil {
return err
}
if err = db.DeleteLNAuth(&db.LNAuth{K1: query.K1}); err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]string{"status": "OK"})
}
}

View File

@ -0,0 +1,30 @@
package handler
import (
"net/http"
"time"
"git.ekzyis.com/ekzyis/delphi.market/db"
"github.com/labstack/echo/v4"
)
func HandleLogout(envVars map[string]any) echo.HandlerFunc {
return func(c echo.Context) error {
var (
cookie *http.Cookie
sessionId string
err error
)
if cookie, err = c.Cookie("session"); err != nil {
// cookie not found
return c.Redirect(http.StatusSeeOther, "/")
}
sessionId = cookie.Value
if err = db.DeleteSession(&db.Session{SessionId: sessionId}); err != nil {
return err
}
// tell browser that cookie is expired and thus can be deleted
c.SetCookie(&http.Cookie{Name: "session", HttpOnly: true, Path: "/", Value: sessionId, Secure: true, Expires: time.Now()})
return c.Redirect(http.StatusSeeOther, "/")
}
}

View File

@ -0,0 +1,121 @@
package handler
import (
"database/sql"
"fmt"
"net/http"
"strconv"
"git.ekzyis.com/ekzyis/delphi.market/db"
"git.ekzyis.com/ekzyis/delphi.market/env"
"git.ekzyis.com/ekzyis/delphi.market/lib"
"git.ekzyis.com/ekzyis/delphi.market/lnd"
"github.com/labstack/echo/v4"
"github.com/lightningnetwork/lnd/lntypes"
)
func HandleMarket(envVars map[string]any) echo.HandlerFunc {
return func(c echo.Context) error {
var (
marketId int64
market db.Market
shares []db.Share
orders []db.Order
err error
data map[string]any
)
if marketId, err = strconv.ParseInt(c.Param("id"), 10, 64); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
}
if err = db.FetchMarket(int(marketId), &market); err == sql.ErrNoRows {
return echo.NewHTTPError(http.StatusNotFound, "Not Found")
} else if err != nil {
return err
}
if err = db.FetchShares(market.Id, &shares); err != nil {
return err
}
if err = db.FetchOrders(&db.FetchOrdersWhere{MarketId: market.Id, Confirmed: true}, &orders); err != nil {
return err
}
data = map[string]any{
"session": c.Get("session"),
"Id": market.Id,
"Description": market.Description,
// shares are sorted by description in descending order
// that's how we know that YES must be the first share
"YesShare": shares[0],
"NoShare": shares[1],
"Orders": orders,
}
lib.Merge(&data, &envVars)
return c.Render(http.StatusOK, "market.html", data)
}
}
func HandlePostOrder(envVars map[string]any) echo.HandlerFunc {
return func(c echo.Context) error {
var (
marketId string
u db.User
o db.Order
invoice *db.Invoice
msats int64
data map[string]any
qr string
hash lntypes.Hash
err error
)
marketId = c.Param("id")
// TODO:
// [ ] Step 0: If SELL order, check share balance of user
// [x] Create HODL invoice
// [x] Create (unconfirmed) order
// [ ] Find matching orders
// [ ] Settle invoice when matching order was found,
// else cancel invoice if expired
// parse body
if err := c.Bind(&o); err != nil {
return echo.NewHTTPError(http.StatusBadRequest)
}
u = c.Get("session").(db.User)
o.Pubkey = u.Pubkey
msats = o.Quantity * o.Price * 1000
// TODO: if SELL order, check share balance of user
// Create HODL invoice
if invoice, err = lnd.CreateInvoice(o.Pubkey, msats); err != nil {
return err
}
// Create QR code to pay HODL invoice
if qr, err = lib.ToQR(invoice.PaymentRequest); err != nil {
return err
}
if hash, err = lntypes.MakeHashFromStr(invoice.Hash); err != nil {
return err
}
// Start goroutine to poll status and update invoice in background
go lnd.CheckInvoice(hash)
// Create (unconfirmed) order
o.InvoiceId = invoice.Id
if err := db.CreateOrder(&o); err != nil {
return err
}
// TODO: find matching orders
data = map[string]any{
"session": c.Get("session"),
"lnurl": invoice.PaymentRequest,
"qr": qr,
"invoice": *invoice,
"redirectURL": fmt.Sprintf("https://%s/market/%s", env.PublicURL, marketId),
}
lib.Merge(&data, &envVars)
return c.Render(http.StatusPaymentRequired, "invoice.html", data)
}
}

View File

@ -0,0 +1,29 @@
package handler
import (
"database/sql"
"net/http"
"git.ekzyis.com/ekzyis/delphi.market/db"
"github.com/labstack/echo/v4"
)
func HandleCheckSession(envVars map[string]any) echo.HandlerFunc {
return func(c echo.Context) error {
var (
cookie *http.Cookie
s db.Session
err error
)
if cookie, err = c.Cookie("session"); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, map[string]string{"reason": "cookie required"})
}
s = db.Session{SessionId: cookie.Value}
if err = db.FetchSession(&s); err == sql.ErrNoRows {
return echo.NewHTTPError(http.StatusNotFound, map[string]string{"reason": "session not found"})
} else if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError)
}
return c.JSON(http.StatusOK, map[string]string{"pubkey": s.Pubkey})
}
}

View File

@ -0,0 +1,31 @@
package handler
import (
"net/http"
"git.ekzyis.com/ekzyis/delphi.market/db"
"git.ekzyis.com/ekzyis/delphi.market/lib"
"github.com/labstack/echo/v4"
)
func HandleUser(envVars map[string]any) echo.HandlerFunc {
return func(c echo.Context) error {
var (
u db.User
orders []db.Order
err error
data map[string]any
)
u = c.Get("session").(db.User)
if err = db.FetchOrders(&db.FetchOrdersWhere{Pubkey: u.Pubkey}, &orders); err != nil {
return err
}
data = map[string]any{
"session": c.Get("session"),
"user": u,
"Orders": orders,
}
lib.Merge(&data, &envVars)
return c.Render(http.StatusOK, "user.html", data)
}
}

View File

@ -0,0 +1,19 @@
package middleware
import (
"net/http"
"git.ekzyis.com/ekzyis/delphi.market/lnd"
"github.com/labstack/echo/v4"
)
func LNDGuard(envVars map[string]any) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if lnd.Enabled {
return next(c)
}
return echo.NewHTTPError(http.StatusMethodNotAllowed)
}
}
}

View File

@ -0,0 +1,51 @@
package middleware
import (
"database/sql"
"net/http"
"time"
"git.ekzyis.com/ekzyis/delphi.market/db"
"github.com/labstack/echo/v4"
)
func Session(envVars map[string]any) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
var (
cookie *http.Cookie
err error
s *db.Session
u *db.User
)
if cookie, err = c.Cookie("session"); err != nil {
// cookie not found
return next(c)
}
s = &db.Session{SessionId: cookie.Value}
if err = db.FetchSession(s); err == nil {
// session found
u = &db.User{Pubkey: s.Pubkey, LastSeen: time.Now()}
if err = db.UpdateUser(u); err != nil {
return err
}
c.Set("session", *u)
} else if err != sql.ErrNoRows {
return err
}
return next(c)
}
}
}
func SessionGuard(envVars map[string]any) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
session := c.Get("session")
if session == nil {
return c.Redirect(http.StatusTemporaryRedirect, "/login")
}
return next(c)
}
}
}

44
server/router/router.go Normal file
View File

@ -0,0 +1,44 @@
package router
import (
"github.com/labstack/echo/v4"
"git.ekzyis.com/ekzyis/delphi.market/env"
"git.ekzyis.com/ekzyis/delphi.market/server/router/handler"
"git.ekzyis.com/ekzyis/delphi.market/server/router/middleware"
)
func AddRoutes(e *echo.Echo) {
envVars := map[string]any{
"PUBLIC_URL": env.PublicURL,
"COMMIT_SHORT_SHA": env.CommitShortSha,
"COMMIT_LONG_SHA": env.CommitLongSha,
"VERSION": env.Version,
}
e.Use(middleware.Session(envVars))
e.GET("/", handler.HandleIndex(envVars))
e.GET("/login", handler.HandleLogin(envVars))
e.GET("/api/login", handler.HandleLoginCallback(envVars))
e.GET("/api/session", handler.HandleCheckSession(envVars))
e.POST("/logout", handler.HandleLogout(envVars))
e.GET("/user",
handler.HandleUser(envVars),
middleware.SessionGuard(envVars))
e.GET("/market/:id",
handler.HandleMarket(envVars),
middleware.SessionGuard(envVars))
e.POST("/market/:id/order",
handler.HandlePostOrder(envVars),
middleware.SessionGuard(envVars),
middleware.LNDGuard(envVars))
e.GET("/invoice/:id",
handler.HandleInvoice(envVars),
middleware.SessionGuard(envVars),
middleware.LNDGuard(envVars),
)
e.GET("/api/invoice/:id",
handler.HandleInvoiceAPI(envVars),
middleware.SessionGuard(envVars),
middleware.LNDGuard(envVars),
)
}

59
server/router/template.go Normal file
View File

@ -0,0 +1,59 @@
package router
import (
"html/template"
"io"
"github.com/labstack/echo/v4"
"golang.org/x/exp/constraints"
)
type Template struct {
templates *template.Template
}
type Number interface {
constraints.Integer | constraints.Float
}
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
var (
T *Template
)
func init() {
T = &Template{
templates: template.Must(template.New("").Funcs(template.FuncMap{
"add": add[int64],
"sub": sub[int64],
"div": div[int64],
"substr": substr,
}).ParseGlob("pages/**.html")),
}
}
func add[T Number](arg1 T, arg2 T) T {
return arg1 + arg2
}
func sub[T Number](arg1 T, arg2 T) T {
return arg1 - arg2
}
func div[T Number](arg1 T, arg2 T) T {
return arg1 / arg2
}
func substr(s string, start, length int) string {
if start < 0 || start >= len(s) {
return ""
}
end := start + length
if end > len(s) {
end = len(s)
}
return s[start:end]
}

31
server/server.go Normal file
View File

@ -0,0 +1,31 @@
package server
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"git.ekzyis.com/ekzyis/delphi.market/server/router"
)
type Server struct {
*echo.Echo
}
func NewServer() *Server {
e := echo.New()
e.Static("/", "public")
e.Renderer = router.T
router.AddRoutes(e)
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: "${time_custom} ${method} ${uri} ${status}\n",
CustomTimeFormat: "2006-01-02 15:04:05.00000-0700",
}))
e.HTTPErrorHandler = httpErrorHandler
return &Server{e}
}

View File

@ -1,202 +0,0 @@
package main
import (
"crypto/ecdsa"
"crypto/rand"
"database/sql"
"encoding/hex"
"fmt"
"net/http"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcutil/bech32"
"github.com/labstack/echo/v4"
)
type LnAuth struct {
k1 string
lnurl string
}
type LnAuthResponse struct {
K1 string `query:"k1"`
Sig string `query:"sig"`
Key string `query:"key"`
}
type Session struct {
Pubkey string
}
func lnAuth() (*LnAuth, error) {
k1 := make([]byte, 32)
_, err := rand.Read(k1)
if err != nil {
return nil, fmt.Errorf("rand.Read error: %w", err)
}
k1hex := hex.EncodeToString(k1)
url := []byte(fmt.Sprintf("https://%s/api/login?tag=login&k1=%s&action=login", PUBLIC_URL, k1hex))
conv, err := bech32.ConvertBits(url, 8, 5, true)
if err != nil {
return nil, fmt.Errorf("bech32.ConvertBits error: %w", err)
}
lnurl, err := bech32.Encode("lnurl", conv)
if err != nil {
return nil, fmt.Errorf("bech32.Encode error: %w", err)
}
return &LnAuth{k1hex, lnurl}, nil
}
func lnAuthVerify(r *LnAuthResponse) (bool, error) {
var k1Bytes, sigBytes, keyBytes []byte
k1Bytes, err := hex.DecodeString(r.K1)
if err != nil {
return false, fmt.Errorf("k1 decode error: %w", err)
}
sigBytes, err = hex.DecodeString(r.Sig)
if err != nil {
return false, fmt.Errorf("sig decode error: %w", err)
}
keyBytes, err = hex.DecodeString(r.Key)
if err != nil {
return false, fmt.Errorf("key decode error: %w", err)
}
key, err := btcec.ParsePubKey(keyBytes)
if err != nil {
return false, fmt.Errorf("key parse error: %w", err)
}
ecdsaKey := ecdsa.PublicKey{Curve: btcec.S256(), X: key.X(), Y: key.Y()}
return ecdsa.VerifyASN1(&ecdsaKey, k1Bytes, sigBytes), nil
}
func login(c echo.Context) error {
lnauth, err := lnAuth()
if err != nil {
return err
}
var sessionId string
err = db.QueryRow("INSERT INTO lnauth(k1, lnurl) VALUES($1, $2) RETURNING session_id", lnauth.k1, lnauth.lnurl).Scan(&sessionId)
if err != nil {
return err
}
expires := time.Now().Add(60 * 60 * 24 * 365 * time.Second)
c.SetCookie(&http.Cookie{Name: "session", HttpOnly: true, Path: "/", Value: sessionId, Secure: true, Expires: expires})
qr, err := ToQR(lnauth.lnurl)
if err != nil {
return err
}
return c.Render(http.StatusOK, "login.html", map[string]any{"session": c.Get("session"), "PUBLIC_URL": PUBLIC_URL, "lnurl": lnauth.lnurl, "qr": qr})
}
func verifyLogin(c echo.Context) error {
var query LnAuthResponse
if err := c.Bind(&query); err != nil {
c.Logger().Error(err)
return c.JSON(http.StatusBadRequest, map[string]string{"status": "ERROR", "reason": "bad request"})
}
var sessionId string
err := db.QueryRow("SELECT session_id FROM lnauth WHERE k1 = $1", query.K1).Scan(&sessionId)
if err == sql.ErrNoRows {
return c.JSON(http.StatusBadRequest, map[string]string{"status": "ERROR", "reason": "unknown k1"})
} else if err != nil {
c.Logger().Error(err)
return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"})
}
ok, err := lnAuthVerify(&query)
if err != nil {
c.Logger().Error(err)
return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"})
}
if !ok {
c.Logger().Error("bad signature")
return c.JSON(http.StatusUnauthorized, map[string]string{"status": "ERROR", "reason": "bad signature"})
}
_, err = db.Exec("INSERT INTO users(pubkey) VALUES ($1) ON CONFLICT(pubkey) DO UPDATE SET last_seen = CURRENT_TIMESTAMP", query.Key)
if err != nil {
c.Logger().Error(err)
return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"})
}
_, err = db.Exec("INSERT INTO sessions(pubkey, session_id) VALUES($1, $2)", query.Key, sessionId)
if err != nil {
c.Logger().Error(err)
return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"})
}
_, err = db.Exec("DELETE FROM lnauth WHERE k1 = $1", query.K1)
if err != nil {
c.Logger().Error(err)
return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"})
}
return c.JSON(http.StatusOK, map[string]string{"status": "OK"})
}
func checkSession(c echo.Context) error {
cookie, err := c.Cookie("session")
if err != nil {
c.Logger().Error(err)
return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"})
}
sessionId := cookie.Value
var pubkey string
err = db.QueryRow("SELECT pubkey FROM sessions WHERE session_id = $1", sessionId).Scan(&pubkey)
if err == sql.ErrNoRows {
return c.JSON(http.StatusNotFound, map[string]string{"status": "Not Found", "message": "session not found"})
} else if err != nil {
c.Logger().Error(err)
return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"})
}
return c.JSON(http.StatusOK, map[string]string{"pubkey": pubkey})
}
func sessionHandler(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
cookie, err := c.Cookie("session")
if err != nil {
// cookie not found
return next(c)
}
sessionId := cookie.Value
var pubkey string
err = db.QueryRow("SELECT pubkey FROM sessions WHERE session_id = $1", sessionId).Scan(&pubkey)
if err == nil {
// session found
_, err = db.Exec("UPDATE users SET last_seen = CURRENT_TIMESTAMP WHERE pubkey = $1", pubkey)
if err != nil {
c.Logger().Error(err)
return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"})
}
c.Set("session", Session{pubkey})
} else if err != sql.ErrNoRows {
c.Logger().Error(err)
return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"})
}
return next(c)
}
}
func sessionGuard(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
session := c.Get("session")
if session == nil {
return c.Redirect(http.StatusTemporaryRedirect, "/login")
}
return next(c)
}
}
func logout(c echo.Context) error {
cookie, err := c.Cookie("session")
if err != nil {
// cookie not found
return c.Redirect(http.StatusSeeOther, "/")
}
sessionId := cookie.Value
_, err = db.Exec("DELETE FROM sessions where session_id = $1", sessionId)
if err != nil {
c.Logger().Error(err)
return err
}
// tell browser that cookie is expired and thus can be deleted
c.SetCookie(&http.Cookie{Name: "session", HttpOnly: true, Path: "/", Value: sessionId, Secure: true, Expires: time.Now()})
return c.Redirect(http.StatusSeeOther, "/")
}

188
src/db.go
View File

@ -1,188 +0,0 @@
package main
import (
"database/sql"
"log"
"time"
"github.com/joho/godotenv"
_ "github.com/lib/pq"
"github.com/namsral/flag"
)
var (
DbUrl string
db *DB
)
type DB struct {
*sql.DB
}
func init() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
flag.StringVar(&DbUrl, "DATABASE_URL", "", "Database URL")
flag.Parse()
validateFlags()
db = initDb()
_, err = db.Exec("SELECT 1")
if err != nil {
log.Fatal(err)
}
}
func initDb() *DB {
db, err := sql.Open("postgres", DbUrl)
if err != nil {
log.Fatal(err)
}
return &DB{DB: db}
}
func validateFlags() {
if DbUrl == "" {
log.Fatal("DATABASE_URL not set")
}
}
func (db *DB) FetchMarket(marketId int, market *Market) error {
if err := db.QueryRow("SELECT id, description FROM markets WHERE id = $1", marketId).Scan(&market.Id, &market.Description); err != nil {
return err
}
return nil
}
func (db *DB) FetchShares(marketId int, shares *[]Share) error {
rows, err := db.Query("SELECT id, market_id, description FROM shares WHERE market_id = $1 ORDER BY description DESC", marketId)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var share Share
rows.Scan(&share.Id, &share.MarketId, &share.Description)
*shares = append(*shares, share)
}
return nil
}
type FetchOrdersWhere struct {
MarketId int
Pubkey string
Confirmed bool
}
func (db *DB) FetchOrders(where *FetchOrdersWhere, orders *[]Order) error {
query := "" +
"SELECT o.id, share_id, o.pubkey, o.side, o.quantity, o.price, o.invoice_id, o.created_at, s.description, s.market_id, i.confirmed_at " +
"FROM orders o " +
"JOIN invoices i ON o.invoice_id = i.id " +
"JOIN shares s ON o.share_id = s.id " +
"WHERE "
var args []any
if where.MarketId > 0 {
query += "share_id = ANY(SELECT id FROM shares WHERE market_id = $1) "
args = append(args, where.MarketId)
} else if where.Pubkey != "" {
query += "o.pubkey = $1 "
args = append(args, where.Pubkey)
}
if where.Confirmed {
query += "AND i.confirmed_at IS NOT NULL "
}
query += "AND (i.confirmed_at IS NOT NULL OR i.expires_at > CURRENT_TIMESTAMP) "
query += "ORDER BY price DESC"
rows, err := db.Query(query, args...)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var order Order
rows.Scan(&order.Id, &order.ShareId, &order.Pubkey, &order.Side, &order.Quantity, &order.Price, &order.InvoiceId, &order.CreatedAt, &order.Share.Description, &order.Share.MarketId, &order.Invoice.ConfirmedAt)
*orders = append(*orders, order)
}
return nil
}
func (db *DB) CreateOrder(order *Order) error {
if _, err := db.Exec(""+
"INSERT INTO orders(share_id, pubkey, side, quantity, price, invoice_id) "+
"VALUES ($1, $2, $3, $4, $5, $6)",
order.ShareId, order.Pubkey, order.Side, order.Quantity, order.Price, order.InvoiceId); err != nil {
return err
}
return nil
}
func (db *DB) CreateInvoice(invoice *Invoice) error {
if err := db.QueryRow(""+
"INSERT INTO invoices(pubkey, msats, preimage, hash, bolt11, created_at, expires_at) "+
"VALUES($1, $2, $3, $4, $5, $6, $7) "+
"RETURNING id",
invoice.Pubkey, invoice.Msats, invoice.Preimage, invoice.PaymentHash, invoice.PaymentRequest, invoice.CreatedAt, invoice.ExpiresAt).Scan(&invoice.Id); err != nil {
panic(err)
}
return 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
}
type FetchInvoicesWhere struct {
Expired bool
}
func (db *DB) FetchInvoices(where *FetchInvoicesWhere, invoices *[]Invoice) error {
query := "" +
"SELECT id, msats, msats_received, preimage, hash, bolt11, created_at, expires_at, confirmed_at, held_since " +
"FROM invoices i "
if where.Expired {
query += "WHERE i.expires_at <= CURRENT_TIMESTAMP"
} else {
query += "WHERE i.expires_at > CURRENT_TIMESTAMP"
}
rows, err := db.Query(query)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var inv Invoice
rows.Scan(&inv.Id, &inv.Msats, &inv.ReceivedMsats, &inv.Preimage, &inv.PaymentHash, &inv.PaymentRequest, &inv.CreatedAt, &inv.ExpiresAt, &inv.ConfirmedAt, &inv.HeldSince)
*invoices = append(*invoices, inv)
}
return nil
}
func (db *DB) ConfirmInvoice(hash string, confirmedAt time.Time, msatsReceived int) error {
if _, err := db.Exec("UPDATE invoices SET confirmed_at = $2, msats_received = $3 WHERE hash = $1", hash, confirmedAt, msatsReceived); err != nil {
return err
}
return nil
}
func (db *DB) FetchUser(pubkey string, user *User) error {
return db.QueryRow("SELECT pubkey FROM users WHERE pubkey = $1", pubkey).Scan(&user.Pubkey)
}

View File

@ -1,222 +0,0 @@
package main
import (
"context"
"crypto/rand"
"database/sql"
"io"
"log"
"net/http"
"time"
"github.com/joho/godotenv"
"github.com/labstack/echo/v4"
"github.com/lightninglabs/lndclient"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/namsral/flag"
)
var (
LndCert string
LndMacaroonDir string
LndHost string
lnd *LndClient
lndEnabled bool
)
type LndClient struct {
lndclient.GrpcLndServices
}
func init() {
err := godotenv.Load()
if err != nil {
log.Println("Error loading .env file")
}
flag.StringVar(&LndCert, "LND_CERT", "", "Path to LND TLS certificate")
flag.StringVar(&LndMacaroonDir, "LND_MACAROON_DIR", "", "LND macaroon directory")
flag.StringVar(&LndHost, "LND_HOST", "localhost:10001", "LND gRPC server address")
flag.Parse()
lndEnabled = false
rpcLndServices, err := lndclient.NewLndServices(&lndclient.LndServicesConfig{
LndAddress: LndHost,
MacaroonDir: LndMacaroonDir,
TLSPath: LndCert,
Network: lndclient.NetworkRegtest,
})
if err != nil {
log.Println(err)
return
}
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 {
return func(c echo.Context) error {
if lndEnabled {
return next(c)
}
return serveError(c, 405)
}
}
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) {
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 / time.Millisecond),
})
if err != nil {
return nil, err
}
lnInvoice, err := lnd.Client.LookupInvoice(context.TODO(), hash)
if err != nil {
return nil, err
}
dbInvoice := Invoice{
Session: Session{pubkey},
Msats: msats,
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
}
return &dbInvoice, nil
}
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)
lnInvoice, err := lnd.Client.LookupInvoice(context.TODO(), hash)
if err != nil {
handleLoopError(err)
continue
}
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 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(loopPause)
}
}
func invoice(c echo.Context) error {
invoiceId := c.Param("id")
var invoice Invoice
if err := db.FetchInvoice(&FetchInvoiceWhere{Id: invoiceId}, &invoice); err == sql.ErrNoRows {
return echo.NewHTTPError(http.StatusNotFound, "Not Found")
} else if err != nil {
return err
}
session := c.Get("session").(Session)
if invoice.Pubkey != session.Pubkey {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
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
}
status := ""
if invoice.ConfirmedAt.Valid {
status = "Paid"
} else if time.Now().After(invoice.ExpiresAt) {
status = "Expired"
}
data := map[string]any{
"session": c.Get("session"),
"Invoice": invoice,
"Status": status,
"lnurl": invoice.PaymentRequest,
"qr": qr,
}
return c.Render(http.StatusOK, "invoice.html", data)
}
func invoiceStatus(c echo.Context) error {
invoiceId := c.Param("id")
var invoice Invoice
if err := db.FetchInvoice(&FetchInvoiceWhere{Id: invoiceId}, &invoice); err == sql.ErrNoRows {
return echo.NewHTTPError(http.StatusNotFound, "Not Found")
} else if err != nil {
return err
}
session := c.Get("session").(Session)
if invoice.Pubkey != session.Pubkey {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
invoice.Preimage = ""
return c.JSON(http.StatusOK, invoice)
}

View File

@ -1,155 +0,0 @@
package main
import (
"database/sql"
"fmt"
"math"
"net/http"
"strconv"
"time"
"github.com/labstack/echo/v4"
"github.com/lightningnetwork/lnd/lntypes"
"gopkg.in/guregu/null.v4"
)
type Market struct {
Id int
Description string
Active bool
}
type Share struct {
Id string
MarketId int
Description string
}
type Order struct {
Session
Share
Market
Invoice
Id string
ShareId string `form:"share_id"`
Side string `form:"side"`
Price int `form:"price"`
Quantity int `form:"quantity"`
InvoiceId string
CreatedAt time.Time
}
type Invoice struct {
Session
Id string
Msats int
ReceivedMsats int
Preimage string
PaymentRequest string
PaymentHash string
CreatedAt time.Time
ExpiresAt time.Time
ConfirmedAt null.Time
HeldSince null.Time
}
func costFunction(b float64, q1 float64, q2 float64) float64 {
// reference: http://blog.oddhead.com/2006/10/30/implementing-hansons-market-maker/
return b * math.Log(math.Pow(math.E, q1/b)+math.Pow(math.E, q2/b))
}
// logarithmic market scoring rule (LMSR) market maker from Robin Hanson:
// https://mason.gmu.edu/~rhanson/mktscore.pdf1
func BinaryLMSR(invariant int, funding int, q1 int, q2 int, dq1 int) float64 {
b := float64(funding)
fq1 := float64(q1)
fq2 := float64(q2)
fdq1 := float64(dq1)
return costFunction(b, fq1+fdq1, fq2) - costFunction(b, fq1, fq2)
}
func order(c echo.Context) error {
marketId := c.Param("id")
// (TBD) Step 0: If SELL order, check share balance of user
// (TBD) Step 1: respond with HODL invoice
o := new(Order)
if err := c.Bind(o); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "bad request")
}
session := c.Get("session").(Session)
o.Pubkey = session.Pubkey
amount := o.Quantity * o.Price
invoice, err := lnd.CreateInvoice(session.Pubkey, amount*1000)
if err != nil {
return err
}
o.InvoiceId = invoice.Id
if err := db.CreateOrder(o); err != nil {
return err
}
qr, err := ToQR(invoice.PaymentRequest)
if err != nil {
return err
}
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,
"lnurl": invoice.PaymentRequest,
"qr": qr,
"Invoice": invoice,
"RedirectURL": fmt.Sprintf("https://%s/market/%s", PUBLIC_URL, marketId),
}
return c.Render(http.StatusPaymentRequired, "invoice.html", data)
// Step 2: After payment, confirm order if no matching order was found
// if err := db.CreateOrder(o); err != nil {
// if strings.Contains(err.Error(), "violates check constraint") {
// return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
// }
// return err
// }
// (TBD) Step 3:
// Settle hodl invoice when matching order was found
// else cancel hodl invoice if expired
// ...
// return market(c)
}
func market(c echo.Context) error {
marketId, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
}
var market Market
if err = db.FetchMarket(int(marketId), &market); err == sql.ErrNoRows {
return echo.NewHTTPError(http.StatusNotFound, "Not Found")
} else if err != nil {
return err
}
var shares []Share
if err = db.FetchShares(market.Id, &shares); err != nil {
return err
}
var orders []Order
if err = db.FetchOrders(&FetchOrdersWhere{MarketId: market.Id, Confirmed: true}, &orders); err != nil {
return err
}
data := map[string]any{
"session": c.Get("session"),
"ENV": ENV,
"Id": market.Id,
"Description": market.Description,
// shares are sorted by description in descending order
// that's how we know that YES must be the first share
"YesShare": shares[0],
"NoShare": shares[1],
"Orders": orders,
}
return c.Render(http.StatusOK, "market.html", data)
}

View File

@ -1,108 +0,0 @@
package main
import (
"fmt"
"html/template"
"io"
"net/http"
"os"
"github.com/labstack/echo/v4"
)
type Template struct {
templates *template.Template
}
func add(arg1 int, arg2 int) int {
return arg1 + arg2
}
func sub(arg1 int, arg2 int) int {
return arg1 - arg2
}
func div(arg1 int, arg2 int) int {
return arg1 / arg2
}
func substr(s string, start, length int) string {
if start < 0 || start >= len(s) {
return ""
}
end := start + length
if end > len(s) {
end = len(s)
}
return s[start:end]
}
var (
FuncMap template.FuncMap = template.FuncMap{
"add": add,
"sub": sub,
"div": div,
"substr": substr,
}
)
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
func index(c echo.Context) error {
rows, err := db.Query("SELECT id, description, active FROM markets WHERE active = true")
if err != nil {
return err
}
defer rows.Close()
var markets []Market
for rows.Next() {
var market Market
rows.Scan(&market.Id, &market.Description, &market.Active)
markets = append(markets, market)
}
data := map[string]any{
"session": c.Get("session"),
"ENV": ENV,
"markets": markets,
"VERSION": VERSION,
"COMMIT_LONG_SHA": COMMIT_LONG_SHA}
return c.Render(http.StatusOK, "index.html", data)
}
func serveError(c echo.Context, code int) error {
f, err := os.Open(fmt.Sprintf("public/%d.html", code))
if err != nil {
c.Logger().Error(err)
return err
}
err = c.Stream(code, "text/html", f)
if err != nil {
c.Logger().Error(err)
return err
}
return nil
}
func httpErrorHandler(err error, c echo.Context) {
c.Logger().Error(err)
code := http.StatusInternalServerError
httpError, ok := err.(*echo.HTTPError)
if ok {
code = httpError.Code
}
filePath := fmt.Sprintf("public/%d.html", code)
f, err := os.Open(filePath)
if err != nil {
c.Logger().Error(err)
serveError(c, 500)
return
}
err = c.Stream(code, "text/html", f)
if err != nil {
c.Logger().Error(err)
serveError(c, 500)
return
}
}

View File

@ -1,84 +0,0 @@
package main
import (
"fmt"
"html/template"
"log"
"net/http"
"os/exec"
"strings"
"github.com/joho/godotenv"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/namsral/flag"
)
var (
e *echo.Echo
t *Template
COMMIT_LONG_SHA string
COMMIT_SHORT_SHA string
VERSION string
PORT int
PUBLIC_URL string
ENV string
)
func execCmd(name string, args ...string) string {
cmd := exec.Command(name, args...)
stdout, err := cmd.Output()
if err != nil {
log.Fatal(err)
}
return strings.TrimSpace(string(stdout))
}
func init() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
flag.StringVar(&PUBLIC_URL, "PUBLIC_URL", "delphi.market", "Public URL of website")
flag.IntVar(&PORT, "PORT", 4321, "Server port")
flag.StringVar(&ENV, "ENV", "development", "Specify for which environment files should be built")
flag.Parse()
e = echo.New()
t = &Template{
templates: template.Must(template.New("").Funcs(FuncMap).ParseGlob("pages/**.html")),
}
COMMIT_LONG_SHA = execCmd("git", "rev-parse", "HEAD")
COMMIT_SHORT_SHA = execCmd("git", "rev-parse", "--short", "HEAD")
VERSION = fmt.Sprintf("v0.0.0+%s", COMMIT_SHORT_SHA)
log.Printf("Running commit %s", COMMIT_SHORT_SHA)
log.Printf("Public URL: %s", PUBLIC_URL)
log.Printf("Environment: %s", ENV)
}
func main() {
e.Static("/", "public")
e.Renderer = t
e.GET("/", index)
e.GET("/login", login)
e.GET("/api/login", verifyLogin)
e.GET("/api/session", checkSession)
e.POST("/logout", logout)
e.GET("/user", sessionGuard(user))
e.GET("/market/:id", sessionGuard(market))
e.POST("/market/:id/order", sessionGuard(lndGuard(order)))
e.GET("/invoice/:id", sessionGuard(lndGuard(invoice)))
e.GET("/api/invoice/:id", sessionGuard(lndGuard(invoiceStatus)))
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: "${time_custom} ${method} ${uri} ${status}\n",
CustomTimeFormat: "2006-01-02 15:04:05.00000-0700",
}))
e.Use(sessionHandler)
e.HTTPErrorHandler = httpErrorHandler
if err := RunJobs(); err != nil {
log.Fatal(err)
}
err := e.Start(fmt.Sprintf("%s:%d", "127.0.0.1", PORT))
if err != http.ErrServerClosed {
log.Fatal(err)
}
}

View File

@ -1,29 +0,0 @@
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
type User struct {
Session
}
func user(c echo.Context) error {
session := c.Get("session").(Session)
u := User{}
if err := db.FetchUser(session.Pubkey, &u); err != nil {
return err
}
var orders []Order
if err := db.FetchOrders(&FetchOrdersWhere{Pubkey: session.Pubkey}, &orders); err != nil {
return err
}
data := map[string]any{
"session": c.Get("session"),
"user": u,
"Orders": orders,
}
return c.Render(http.StatusOK, "user.html", data)
}

View File

@ -1,23 +0,0 @@
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 {
hash, err := lntypes.MakeHashFromStr(inv.PaymentHash)
if err != nil {
log.Println(err)
continue
}
go lnd.CheckInvoice(hash)
}
return nil
}