diff --git a/src/auth.go b/src/auth.go index 969acc9..764a36c 100644 --- a/src/auth.go +++ b/src/auth.go @@ -3,11 +3,17 @@ package main import ( "crypto/ecdsa" "crypto/rand" + "database/sql" + "encoding/base64" "encoding/hex" "fmt" + "net/http" + "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcutil/bech32" + "github.com/labstack/echo/v4" + "github.com/skip2/go-qrcode" ) type LnAuth struct { @@ -65,3 +71,135 @@ func lnAuthVerify(r *LnAuthResponse) (bool, error) { 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}) + png, err := qrcode.Encode(lnauth.lnurl, qrcode.Medium, 256) + if err != nil { + return err + } + qr := base64.StdEncoding.EncodeToString([]byte(png)) + 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, "/") +} diff --git a/src/db.go b/src/db.go index 38e2be0..b453e54 100644 --- a/src/db.go +++ b/src/db.go @@ -11,9 +11,13 @@ import ( var ( DbUrl string - db *sql.DB + db *DB ) +type DB struct { + *sql.DB +} + func init() { err := godotenv.Load() if err != nil { @@ -29,12 +33,12 @@ func init() { } } -func initDb() *sql.DB { +func initDb() *DB { db, err := sql.Open("postgres", DbUrl) if err != nil { log.Fatal(err) } - return db + return &DB{DB: db} } func validateFlags() { @@ -42,3 +46,72 @@ func validateFlags() { 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 +} + +func (db *DB) FetchOrderBook(shareId string, orderBook *[]OrderBookEntry) error { + rows, err := db.Query(""+ + "SELECT share_id, side, price, SUM(quantity)"+ + "FROM orders WHERE share_id = $1"+ + "GROUP BY (share_id, side, price)"+ + "ORDER BY share_id DESC, side DESC, price DESC", shareId) + if err != nil { + return err + } + defer rows.Close() + buyOrders := []Order{} + sellOrders := []Order{} + for rows.Next() { + var order Order + rows.Scan(&order.ShareId, &order.Side, &order.Price, &order.Quantity) + if order.Side == "BUY" { + buyOrders = append(buyOrders, Order{Price: order.Price, Quantity: order.Quantity}) + } else { + sellOrders = append(sellOrders, Order{Price: order.Price, Quantity: order.Quantity}) + } + } + buySum := 0 + sellSum := 0 + for i := 0; i < Max(len(buyOrders), len(sellOrders)); i++ { + buyPrice, buyQuantity, sellQuantity, sellPrice := 0, 0, 0, 0 + if i < len(buyOrders) { + buyPrice = buyOrders[i].Price + buyQuantity = buySum + buyOrders[i].Quantity + } + if i < len(sellOrders) { + sellPrice = sellOrders[i].Price + sellQuantity = sellSum + sellOrders[i].Quantity + } + buySum += buyQuantity + sellSum += sellQuantity + *orderBook = append( + *orderBook, + OrderBookEntry{ + BuyQuantity: buyQuantity, + BuyPrice: buyPrice, + SellPrice: sellPrice, + SellQuantity: sellQuantity, + }, + ) + } + return nil +} diff --git a/src/funcs.go b/src/funcs.go deleted file mode 100644 index 3bf4b8c..0000000 --- a/src/funcs.go +++ /dev/null @@ -1,20 +0,0 @@ -package main - -import ( - "html/template" -) - -func add(arg1 int, arg2 int) int { - return arg1 + arg2 -} - -func sub(arg1 int, arg2 int) int { - return arg1 - arg2 -} - -var ( - FuncMap template.FuncMap = template.FuncMap{ - "add": add, - "sub": sub, - } -) diff --git a/src/market.go b/src/market.go index 076ba7d..99461e0 100644 --- a/src/market.go +++ b/src/market.go @@ -1,9 +1,48 @@ package main import ( + "database/sql" + "fmt" "math" + "net/http" + "strconv" + + "github.com/labstack/echo/v4" ) +type Market struct { + Id int + Description string + Funding int + Active bool +} + +type Share struct { + Id string + MarketId int + Description string +} + +type Order struct { + ShareId string + Side string + Price int + Quantity int +} + +type OrderBookEntry struct { + BuyQuantity int + BuyPrice int + SellPrice int + SellQuantity int +} + +type MarketDataRequest struct { + ShareId string `json:"share_id"` + OrderSide string `json:"side"` + Quantity int `json:"quantity"` +} + 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)) @@ -18,3 +57,65 @@ func BinaryLMSR(invariant int, funding int, q1 int, q2 int, dq1 int) float64 { fdq1 := float64(dq1) return costFunction(b, fq1+fdq1, fq2) - costFunction(b, fq1, fq2) } + +func trades(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 + } + data := map[string]any{ + "session": c.Get("session"), + "ENV": ENV, + "Id": market.Id, + "Description": market.Description, + "Shares": shares, + } + return c.Render(http.StatusOK, "bmarket_trade.html", data) +} + +func orders(c echo.Context) error { + marketId, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Bad Request") + } + shareId := c.Param("sid") + + 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 + } + if shareId == "" { + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("/market/%d/%s", market.Id, shares[0].Id)) + } + var orderBook []OrderBookEntry + if err = db.FetchOrderBook(shareId, &orderBook); err != nil { + return err + } + data := map[string]any{ + "session": c.Get("session"), + "ENV": ENV, + "Id": market.Id, + "Description": market.Description, + "ShareId": shareId, + "Shares": shares, + "OrderBook": orderBook, + } + return c.Render(http.StatusOK, "bmarket_order.html", data) +} diff --git a/src/router.go b/src/router.go index dae827a..4fd5e6e 100644 --- a/src/router.go +++ b/src/router.go @@ -1,55 +1,33 @@ package main import ( - "database/sql" - "encoding/base64" "fmt" "html/template" "io" "net/http" "os" - "time" "github.com/labstack/echo/v4" - "github.com/skip2/go-qrcode" ) type Template struct { templates *template.Template } -type Market struct { - Id int - Description string - Funding int - Active bool +func add(arg1 int, arg2 int) int { + return arg1 + arg2 } -type Share struct { - Id string - MarketId int - Description string +func sub(arg1 int, arg2 int) int { + return arg1 - arg2 } -type Order struct { - ShareId string - Side string - Price int - Quantity int -} - -type OrderBookEntry struct { - BuyQuantity int - BuyPrice int - SellPrice int - SellQuantity int -} - -type MarketDataRequest struct { - ShareId string `json:"share_id"` - OrderSide string `json:"side"` - Quantity int `json:"quantity"` -} +var ( + FuncMap template.FuncMap = template.FuncMap{ + "add": add, + "sub": sub, + } +) func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { return t.templates.ExecuteTemplate(w, name, data) @@ -76,249 +54,6 @@ func index(c echo.Context) error { return c.Render(http.StatusOK, "index.html", data) } -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}) - png, err := qrcode.Encode(lnauth.lnurl, qrcode.Medium, 256) - if err != nil { - return err - } - qr := base64.StdEncoding.EncodeToString([]byte(png)) - 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, "/") -} - -func trades(c echo.Context) error { - marketId := c.Param("id") - var market Market - err := db.QueryRow("SELECT id, description FROM markets WHERE id = $1 AND active = true", marketId).Scan(&market.Id, &market.Description) - if err == sql.ErrNoRows { - return echo.NewHTTPError(http.StatusNotFound, "Not Found") - } else if err != nil { - return err - } - 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() - var shares []Share - for rows.Next() { - var share Share - rows.Scan(&share.Id, &share.MarketId, &share.Description) - shares = append(shares, share) - } - data := map[string]any{ - "session": c.Get("session"), - "ENV": ENV, - "Id": market.Id, - "Description": market.Description, - "Shares": shares, - } - return c.Render(http.StatusOK, "bmarket_trade.html", data) -} - -func orders(c echo.Context) error { - marketId := c.Param("id") - shareId := c.Param("sid") - var market Market - err := db.QueryRow("SELECT id, description FROM markets WHERE id = $1 AND active = true", marketId).Scan(&market.Id, &market.Description) - if err == sql.ErrNoRows { - return echo.NewHTTPError(http.StatusNotFound, "Not Found") - } else if err != nil { - return err - } - 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() - var shares []Share - for rows.Next() { - var share Share - rows.Scan(&share.Id, &share.MarketId, &share.Description) - shares = append(shares, share) - } - if shareId == "" { - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("/market/%s/%s", marketId, shares[0].Id)) - } - rows, err = db.Query(""+ - "SELECT share_id, side, price, SUM(quantity)"+ - "FROM orders WHERE share_id = $1"+ - "GROUP BY (share_id, side, price)"+ - "ORDER BY share_id DESC, side DESC, price DESC", shareId) - if err != nil { - return err - } - defer rows.Close() - buyOrders := []Order{} - sellOrders := []Order{} - for rows.Next() { - var order Order - rows.Scan(&order.ShareId, &order.Side, &order.Price, &order.Quantity) - if order.Side == "BUY" { - buyOrders = append(buyOrders, Order{Price: order.Price, Quantity: order.Quantity}) - } else { - sellOrders = append(sellOrders, Order{Price: order.Price, Quantity: order.Quantity}) - } - } - orderBook := []OrderBookEntry{} - buySum := 0 - sellSum := 0 - for i := 0; i < Max(len(buyOrders), len(sellOrders)); i++ { - buyPrice, buyQuantity, sellQuantity, sellPrice := 0, 0, 0, 0 - if i < len(buyOrders) { - buyPrice = buyOrders[i].Price - buyQuantity = buySum + buyOrders[i].Quantity - } - if i < len(sellOrders) { - sellPrice = sellOrders[i].Price - sellQuantity = sellSum + sellOrders[i].Quantity - } - buySum += buyQuantity - sellSum += sellQuantity - orderBook = append( - orderBook, - OrderBookEntry{ - BuyQuantity: buyQuantity, - BuyPrice: buyPrice, - SellPrice: sellPrice, - SellQuantity: sellQuantity, - }, - ) - } - data := map[string]any{ - "session": c.Get("session"), - "ENV": ENV, - "Id": market.Id, - "Description": market.Description, - "ShareId": shareId, - "Shares": shares, - "OrderBook": orderBook, - } - return c.Render(http.StatusOK, "bmarket_order.html", data) -} - func serve500(c echo.Context) { f, err := os.Open("public/500.html") if err != nil {