Refactor router code

This commit is contained in:
ekzyis 2023-09-09 22:52:51 +02:00
parent 04ce96069b
commit c2a8968120
5 changed files with 325 additions and 298 deletions

View File

@ -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, "/")
}

View File

@ -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
}

View File

@ -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,
}
)

View File

@ -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)
}

View File

@ -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 {