From 87ce57c86265797cb4a0112bda8fe46a53e917eb Mon Sep 17 00:00:00 2001 From: ekzyis Date: Sat, 9 Sep 2023 22:52:50 +0200 Subject: [PATCH] Add binary market page --- go.mod | 9 +- go.sum | 6 +- init.sql | 23 +++++ public/405.html | 34 +++++++ public/index.css | 4 + public/market.css | 43 +++++++++ src/market.go | 32 +++++++ src/router.go | 129 +++++++++++++++++++++++++- src/server.go | 4 +- template/binary_market.html | 174 ++++++++++++++++++++++++++++++++++++ template/index.html | 4 + 11 files changed, 454 insertions(+), 8 deletions(-) create mode 100644 public/405.html create mode 100644 public/market.css create mode 100644 src/market.go create mode 100644 template/binary_market.html diff --git a/go.mod b/go.mod index 73de629..832cbfc 100644 --- a/go.mod +++ b/go.mod @@ -3,21 +3,22 @@ module delphi.market go 1.20 require ( - github.com/btcsuite/btcd/btcec/v2 v2.3.2 + 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/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 ) require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect - github.com/joho/godotenv v1.5.1 // indirect github.com/labstack/gommon v0.4.0 // indirect - github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect - github.com/namsral/flag v1.7.4-pre // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect golang.org/x/crypto v0.11.0 // indirect diff --git a/go.sum b/go.sum index 755bb58..6b5587e 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= -github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= -github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +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/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= github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= @@ -62,6 +62,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY= +golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= diff --git a/init.sql b/init.sql index 391d1be..bfc7df8 100644 --- a/init.sql +++ b/init.sql @@ -12,3 +12,26 @@ CREATE TABLE sessions( pubkey TEXT NOT NULL REFERENCES users(pubkey), session_id VARCHAR(48) ); + +CREATE TABLE markets( + id SERIAL PRIMARY KEY, + description TEXT NOT NULL, + funding BIGINT NOT NULL, + active BOOLEAN DEFAULT true +); +CREATE EXTENSION "uuid-ossp"; +CREATE TABLE contracts( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + market_id INTEGER REFERENCES markets(id), + description TEXT NOT NULL, + quantity DOUBLE PRECISION NOT NULL +); +CREATE TYPE order_side AS ENUM ('BUY', 'SELL'); +CREATE TABLE trades( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + contract_id UUID NOT NULL REFERENCES contracts(id), + pubkey TEXT NOT NULL REFERENCES users(pubkey), + side ORDER_SIDE NOT NULL, + quantity DOUBLE PRECISION NOT NULL, + msats BIGINT NOT NULL +); diff --git a/public/405.html b/public/405.html new file mode 100644 index 0000000..3fdbfae --- /dev/null +++ b/public/405.html @@ -0,0 +1,34 @@ + + + + delphi.market + + + + + + + + + + +
+ +
+
+ + +
+ _  _    ___  ____  
+| || |  / _ \| ___| 
+| || |_| | | |___ \ 
+|__   _| |_| |___) |
+   |_|  \___/|____/ 
+
+
+
Method Not Allowed
+
+ + diff --git a/public/index.css b/public/index.css index 77144c0..86e01ef 100644 --- a/public/index.css +++ b/public/index.css @@ -117,6 +117,10 @@ ul { height: 100vh; } +.w-100p { + width: 100%; +} + .m-auto { margin: auto; } diff --git a/public/market.css b/public/market.css new file mode 100644 index 0000000..eb4ed1d --- /dev/null +++ b/public/market.css @@ -0,0 +1,43 @@ +.order-button { + border: none; + margin: auto; + border-radius: 4px; + font-size: 20px; +} + +.order-form { + display: none; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; +} + +.sx-1 { + margin: 0 1rem; + padding: 0 1rem; +} + +.yes { + background-color: rgba(20,158,97,.24); + color: #35df8d; +} +.yes:hover { + background-color: #35df8d; + color: white; +} +.yes.selected { + background-color: #35df8d; + color: white; +} + +.no { + background-color: rgba(245,57,94,.24); + color: #ff7386; +} +.no:hover { + background-color: #ff7386; + color: white; +} +.no.selected { + background-color: #ff7386; + color: white; +} diff --git a/src/market.go b/src/market.go new file mode 100644 index 0000000..960ed19 --- /dev/null +++ b/src/market.go @@ -0,0 +1,32 @@ +package main + +import ( + "errors" + "math" +) + +// logarithmic market scoring rule (LMSR) market maker from Robin Hanson: +// https://mason.gmu.edu/~rhanson/mktscore.pdf +func BinaryLMSRBuy(invariant int, funding int, tokensAQ float64, tokensBQ float64, sats int) (float64, error) { + k := float64(invariant) + f := float64(funding) + numOutcomes := 2.0 + expA := -tokensAQ / f + expB := -tokensBQ / f + if k != math.Pow(numOutcomes, expA)+math.Pow(numOutcomes, expB) { + // invariant should not already be broken + return -1, errors.New("invariant already broken") + } + // AMM converts order into equal amount of tokens per outcome and then solves equation to fix invariant + // see https://docs.gnosis.io/conditionaltokens/docs/introduction3/#an-example-with-lmsr + newTokensA := tokensAQ + float64(sats) + newTokensB := tokensBQ + float64(sats) + expB = -newTokensB / f + x := newTokensA + f*math.Log(k-math.Pow(numOutcomes, expB))/math.Log(numOutcomes) + expA = -(newTokensA - x) / f + if k != math.Pow(numOutcomes, expA)+math.Pow(numOutcomes, expB) { + // invariant should not be broken + return -1, errors.New("invariant broken") + } + return x, nil +} diff --git a/src/router.go b/src/router.go index 3007c76..12ae9ec 100644 --- a/src/router.go +++ b/src/router.go @@ -12,18 +12,56 @@ import ( "github.com/labstack/echo/v4" "github.com/skip2/go-qrcode" + "golang.org/x/exp/slices" ) type Template struct { templates *template.Template } +type Market struct { + Id int + Description string + Funding int + Active bool +} + +type Contract struct { + Id string + MarketId int + Description string + Quantity float64 +} + +type MarketDataRequest struct { + ContractId string `json:"contract_id"` + OrderSide string `json:"side"` + Sats int `json:"sats,omitempty"` + Quantity float64 `json:"quantity,omitempty"` +} + 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 { - return c.Render(http.StatusOK, "index.html", map[string]any{"session": c.Get("session"), "VERSION": VERSION, "COMMIT_LONG_SHA": COMMIT_LONG_SHA}) + rows, err := db.Query("SELECT id, description, funding, 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.Funding, &market.Active) + markets = append(markets, market) + } + data := map[string]any{ + "session": c.Get("session"), + "markets": markets, + "VERSION": VERSION, + "COMMIT_LONG_SHA": COMMIT_LONG_SHA} + return c.Render(http.StatusOK, "index.html", data) } func login(c echo.Context) error { @@ -131,6 +169,16 @@ func sessionHandler(next echo.HandlerFunc) echo.HandlerFunc { } } +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 { @@ -148,6 +196,85 @@ func logout(c echo.Context) error { return c.Redirect(http.StatusSeeOther, "/") } +func market(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, quantity FROM contracts WHERE market_id = $1 ORDER BY description DESC", marketId) + if err != nil { + return err + } + defer rows.Close() + var contracts []Contract + for rows.Next() { + var contract Contract + rows.Scan(&contract.Id, &contract.MarketId, &contract.Description, &contract.Quantity) + contracts = append(contracts, contract) + } + data := map[string]any{ + "session": c.Get("session"), + "Id": market.Id, + "Description": market.Description, + "Contracts": contracts, + } + return c.Render(http.StatusOK, "binary_market.html", data) +} + +func marketData(c echo.Context) error { + var req MarketDataRequest + err := c.Bind(&req) + if err != nil { + c.Logger().Error(err) + return c.JSON(http.StatusBadRequest, map[string]string{"status": "ERROR", "reason": "bad request"}) + } + marketId := c.Param("id") + var market Market + err = db.QueryRow("SELECT id, description, funding FROM markets WHERE id = $1 AND active = true", marketId).Scan(&market.Id, &market.Description, &market.Funding) + if err == sql.ErrNoRows { + return c.JSON(http.StatusBadRequest, map[string]string{"status": "ERROR", "reason": "market not found"}) + } else if err != nil { + c.Logger().Error(err) + return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"}) + } + rows, err := db.Query("SELECT id, market_id, description, quantity FROM contracts WHERE market_id = $1", marketId) + if err != nil { + return err + } + defer rows.Close() + var contracts []Contract + for rows.Next() { + var contract Contract + rows.Scan(&contract.Id, &contract.MarketId, &contract.Description, &contract.Quantity) + contracts = append(contracts, contract) + } + sats := req.Sats + quantity := req.Quantity + // contract A is always the contract which is bought or sold + contractAIdx := slices.IndexFunc(contracts, func(c Contract) bool { return c.Id == req.ContractId }) + contractBIdx := 0 + if contractAIdx == 0 { + contractBIdx = 1 + } + contractAQ := contracts[contractAIdx].Quantity + contractBQ := contracts[contractBIdx].Quantity + if req.OrderSide == "BUY" && sats > 0 { + quantity, err := BinaryLMSRBuy(1, market.Funding, contractAQ, contractBQ, sats) + if err != nil { + return err + } + return c.JSON(http.StatusOK, map[string]string{"status": "OK", "quantity": fmt.Sprint(quantity)}) + } + if req.OrderSide == "SELL" && quantity > 0 { + // TODO implement BinaryLMSRSell + } + return c.JSON(http.StatusBadRequest, map[string]string{"status": "ERROR", "reason": "bad request"}) +} + func serve500(c echo.Context) { f, err := os.Open("public/500.html") if err != nil { diff --git a/src/server.go b/src/server.go index bdbbad9..0198d53 100644 --- a/src/server.go +++ b/src/server.go @@ -43,7 +43,7 @@ func init() { flag.Parse() e = echo.New() t = &Template{ - templates: template.Must(template.ParseGlob("template/*.html")), + templates: template.Must(template.ParseGlob("template/**.html")), } COMMIT_LONG_SHA = execCmd("git", "rev-parse", "HEAD") COMMIT_SHORT_SHA = execCmd("git", "rev-parse", "--short", "HEAD") @@ -60,6 +60,8 @@ func main() { e.GET("/api/login", verifyLogin) e.GET("/api/session", checkSession) e.POST("/logout", logout) + e.GET("/market/:id", sessionGuard(market)) + e.POST("/api/market/:id/data", sessionGuard(marketData)) e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ Format: "${time_custom} ${method} ${uri} ${status}\n", CustomTimeFormat: "2006-01-02 15:04:05.00000-0700", diff --git a/template/binary_market.html b/template/binary_market.html new file mode 100644 index 0000000..4205f0e --- /dev/null +++ b/template/binary_market.html @@ -0,0 +1,174 @@ + + + + delphi.market + + + + + + + + + + + +
+ +
+
+ + +
+                      _        _   
+ _ __ ___   __ _ _ __| | _____| |_ 
+| '_ ` _ \ / _` | '__| |/ / _ \ __|
+| | | | | | (_| | |  |   <  __/ |_ 
+|_| |_| |_|\__,_|_|  |_|\_\___|\__|
+
+
+
{{.Description}}
+
+ {{ range .Contracts }} {{ if eq .Description "YES" }} + + {{ else }} + + {{ end }} + {{ end }} +
+ {{ range .Contracts }} {{ if eq .Description "YES" }} + + {{ else }} + + {{ end }} + {{ end }} +
+ + + diff --git a/template/index.html b/template/index.html index 55de3e3..831eb20 100644 --- a/template/index.html +++ b/template/index.html @@ -35,6 +35,10 @@
A prediction market using the lightning network [WIP]
+
ACTIVE MARKETS
+ {{ range .markets }} + {{.Description}} + {{ end }}