Add binary market page
This commit is contained in:
parent
fbab0b4975
commit
87ce57c862
9
go.mod
9
go.mod
|
@ -3,21 +3,22 @@ module delphi.market
|
||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
require (
|
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/btcsuite/btcutil v1.0.2
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/labstack/echo/v4 v4.11.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
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
|
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible // 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/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-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // 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/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
golang.org/x/crypto v0.11.0 // indirect
|
golang.org/x/crypto v0.11.0 // indirect
|
||||||
|
|
6
go.sum
6
go.sum
|
@ -1,7 +1,7 @@
|
||||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
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 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.2.2 h1:5uxe5YjoCq+JeOpg0gZSNHuFgeogrocBYxvg6w9sAgc=
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
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/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 v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||||
github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts=
|
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.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
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-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.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||||
|
|
23
init.sql
23
init.sql
|
@ -12,3 +12,26 @@ CREATE TABLE sessions(
|
||||||
pubkey TEXT NOT NULL REFERENCES users(pubkey),
|
pubkey TEXT NOT NULL REFERENCES users(pubkey),
|
||||||
session_id VARCHAR(48)
|
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
|
||||||
|
);
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>delphi.market</title>
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
|
<link rel="stylesheet" href="/index.css" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#091833" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="flex flex-row text-center justify-center pt-1">
|
||||||
|
<nav>
|
||||||
|
<a href="/">home</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<div class="container flex flex-column text-center">
|
||||||
|
<code>
|
||||||
|
<strong>
|
||||||
|
<pre>
|
||||||
|
_ _ ___ ____
|
||||||
|
| || | / _ \| ___|
|
||||||
|
| || |_| | | |___ \
|
||||||
|
|__ _| |_| |___) |
|
||||||
|
|_| \___/|____/ </pre>
|
||||||
|
</strong>
|
||||||
|
</code>
|
||||||
|
<div class="font-mono mb-1">Method Not Allowed</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -117,6 +117,10 @@ ul {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-100p {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.m-auto {
|
.m-auto {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
129
src/router.go
129
src/router.go
|
@ -12,18 +12,56 @@ import (
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/skip2/go-qrcode"
|
"github.com/skip2/go-qrcode"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Template struct {
|
type Template struct {
|
||||||
templates *template.Template
|
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 {
|
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
|
||||||
return t.templates.ExecuteTemplate(w, name, data)
|
return t.templates.ExecuteTemplate(w, name, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func index(c echo.Context) error {
|
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 {
|
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 {
|
func logout(c echo.Context) error {
|
||||||
cookie, err := c.Cookie("session")
|
cookie, err := c.Cookie("session")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -148,6 +196,85 @@ func logout(c echo.Context) error {
|
||||||
return c.Redirect(http.StatusSeeOther, "/")
|
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) {
|
func serve500(c echo.Context) {
|
||||||
f, err := os.Open("public/500.html")
|
f, err := os.Open("public/500.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -43,7 +43,7 @@ func init() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
e = echo.New()
|
e = echo.New()
|
||||||
t = &Template{
|
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_LONG_SHA = execCmd("git", "rev-parse", "HEAD")
|
||||||
COMMIT_SHORT_SHA = execCmd("git", "rev-parse", "--short", "HEAD")
|
COMMIT_SHORT_SHA = execCmd("git", "rev-parse", "--short", "HEAD")
|
||||||
|
@ -60,6 +60,8 @@ func main() {
|
||||||
e.GET("/api/login", verifyLogin)
|
e.GET("/api/login", verifyLogin)
|
||||||
e.GET("/api/session", checkSession)
|
e.GET("/api/session", checkSession)
|
||||||
e.POST("/logout", logout)
|
e.POST("/logout", logout)
|
||||||
|
e.GET("/market/:id", sessionGuard(market))
|
||||||
|
e.POST("/api/market/:id/data", sessionGuard(marketData))
|
||||||
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
||||||
Format: "${time_custom} ${method} ${uri} ${status}\n",
|
Format: "${time_custom} ${method} ${uri} ${status}\n",
|
||||||
CustomTimeFormat: "2006-01-02 15:04:05.00000-0700",
|
CustomTimeFormat: "2006-01-02 15:04:05.00000-0700",
|
||||||
|
|
|
@ -0,0 +1,174 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>delphi.market</title>
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
|
<link rel="stylesheet" href="/index.css" />
|
||||||
|
<link rel="stylesheet" href="/market.css" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#091833" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="flex flex-row text-center justify-center pt-1">
|
||||||
|
<nav>
|
||||||
|
<a href="/">home</a>
|
||||||
|
{{ if .session }}
|
||||||
|
<form action='/logout' method='post'>
|
||||||
|
<button type='submit'>logout</button>
|
||||||
|
</form>
|
||||||
|
{{ else }} <a href="/login">login</a> {{ end }}
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<div class="container flex flex-column text-center">
|
||||||
|
<code>
|
||||||
|
<strong>
|
||||||
|
<pre>
|
||||||
|
_ _
|
||||||
|
_ __ ___ __ _ _ __| | _____| |_
|
||||||
|
| '_ ` _ \ / _` | '__| |/ / _ \ __|
|
||||||
|
| | | | | | (_| | | | < __/ |_
|
||||||
|
|_| |_| |_|\__,_|_| |_|\_\___|\__|</pre>
|
||||||
|
</strong>
|
||||||
|
</code>
|
||||||
|
<div class="font-mono mb-1">{{.Description}}</div>
|
||||||
|
<div class="flex flex-row justify-center mb-1">
|
||||||
|
{{ range .Contracts }} {{ if eq .Description "YES" }}
|
||||||
|
<button id="yes-order" class="order-button sx-1 yes">YES</button>
|
||||||
|
{{ else }}
|
||||||
|
<button id="no-order" class="order-button sx-1 no">NO</button>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ range .Contracts }} {{ if eq .Description "YES" }}
|
||||||
|
<form id="yes-form" class="order-form" hidden action="/api/market/{{$.Id}}/order" method="post">
|
||||||
|
<button id="yes-buy" type="button" class="order-button yes w-100p selected">BUY</button>
|
||||||
|
<button id="yes-sell" type="button" class="order-button no w-100p">SELL</button>
|
||||||
|
<input hidden name="contract_id" value="{{.Id}}" />
|
||||||
|
<input id="yes-side" hidden name="side" value="BUY" />
|
||||||
|
<input id="yes-quantity" type="number" name="quantity" placeholder="quantity" disabled />
|
||||||
|
<input id="yes-sats" type="number" name="sats" placeholder="sats"/>
|
||||||
|
<label id="yes-submit-label">BUY YES contracts</label>
|
||||||
|
<button type="submit">SUBMIT</button>
|
||||||
|
</form>
|
||||||
|
{{ else }}
|
||||||
|
<form id="no-form" class="order-form" hidden action="/api/market/{{$.Id}}/order" method="post">
|
||||||
|
<button id="no-buy" type="button" class="order-button yes w-100p selected">BUY</button>
|
||||||
|
<button id="no-sell" type="button" class="order-button no w-100p">SELL</button>
|
||||||
|
<input hidden name="contract_id" value="{{.Id}}" />
|
||||||
|
<input id="no-side" hidden name="side" value="BUY" />
|
||||||
|
<input id="no-quantity" type="number" name="quantity" placeholder="quantity" disabled />
|
||||||
|
<input id="no-sats" type="number" name="sats" placeholder="sats" />
|
||||||
|
<label id="no-submit-label">BUY NO contracts</label>
|
||||||
|
<button type="submit">SUBMIT</button>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
const yesOrderBtn = document.querySelector("#yes-order")
|
||||||
|
const yesForm = document.querySelector("#yes-form")
|
||||||
|
const yesBuyBtn = document.querySelector("#yes-buy")
|
||||||
|
const yesSellBtn = document.querySelector("#yes-sell")
|
||||||
|
const yesSideInput = document.querySelector("#yes-side")
|
||||||
|
const yesQuantityInput = document.querySelector("#yes-quantity")
|
||||||
|
const yesSatsInput = document.querySelector("#yes-sats")
|
||||||
|
const yesSubmitLabel = document.querySelector("#yes-submit-label")
|
||||||
|
|
||||||
|
const noOrderBtn = document.querySelector("#no-order")
|
||||||
|
const noForm = document.querySelector("#no-form")
|
||||||
|
const noBuyBtn = document.querySelector("#no-buy")
|
||||||
|
const noSellBtn = document.querySelector("#no-sell")
|
||||||
|
const noSideInput = document.querySelector("#no-side")
|
||||||
|
const noQuantityInput = document.querySelector("#no-quantity")
|
||||||
|
const noSatsInput = document.querySelector("#no-sats")
|
||||||
|
const noSubmitLabel = document.querySelector("#no-submit-label")
|
||||||
|
|
||||||
|
yesOrderBtn.onclick = function () {
|
||||||
|
yesOrderBtn.classList.add("selected")
|
||||||
|
yesForm.style.display = "grid"
|
||||||
|
noOrderBtn.classList.remove("selected")
|
||||||
|
noForm.style.display = "none"
|
||||||
|
}
|
||||||
|
yesBuyBtn.onclick = function () {
|
||||||
|
yesSideInput.value = "BUY"
|
||||||
|
yesBuyBtn.classList.add("selected")
|
||||||
|
yesSellBtn.classList.remove("selected")
|
||||||
|
yesSubmitLabel.textContent = 'BUY YES contracts'
|
||||||
|
}
|
||||||
|
yesSellBtn.onclick = function () {
|
||||||
|
yesSideInput.value = "SELL"
|
||||||
|
yesBuyBtn.classList.remove("selected")
|
||||||
|
yesSellBtn.classList.add("selected")
|
||||||
|
yesSubmitLabel.textContent = 'SELL NO contracts'
|
||||||
|
}
|
||||||
|
yesSatsInput.onchange = async function(e) {
|
||||||
|
const sats = parseInt(e.target.value, 10)
|
||||||
|
const body = {
|
||||||
|
contract_id: "{{(index .Contracts 0).Id}}",
|
||||||
|
sats,
|
||||||
|
side: yesSideInput.value
|
||||||
|
}
|
||||||
|
const rBody = await fetch("/api/market/{{.Id}}/data", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
if (!rBody) return null;
|
||||||
|
const quantity = rBody.quantity
|
||||||
|
yesQuantityInput.value = quantity
|
||||||
|
}
|
||||||
|
noOrderBtn.onclick = function () {
|
||||||
|
noOrderBtn.classList.add("selected")
|
||||||
|
noForm.style.display = "grid"
|
||||||
|
yesOrderBtn.classList.remove("selected")
|
||||||
|
yesForm.style.display = "none"
|
||||||
|
}
|
||||||
|
noBuyBtn.onclick = function () {
|
||||||
|
noSideInput.value = "BUY"
|
||||||
|
noBuyBtn.classList.add("selected")
|
||||||
|
noSellBtn.classList.remove("selected")
|
||||||
|
noSubmitLabel.textContent = 'BUY NO contracts'
|
||||||
|
}
|
||||||
|
noSellBtn.onclick = function () {
|
||||||
|
noSideInput.value = "SELL"
|
||||||
|
noBuyBtn.classList.remove("selected")
|
||||||
|
noSellBtn.classList.add("selected")
|
||||||
|
noSubmitLabel.textContent = 'SELL YES contracts'
|
||||||
|
}
|
||||||
|
noSatsInput.onchange = async function(e) {
|
||||||
|
const sats = parseInt(e.target.value, 10)
|
||||||
|
const body = {
|
||||||
|
contract_id: "{{(index .Contracts 1).Id}}",
|
||||||
|
sats,
|
||||||
|
side: noSideInput.value
|
||||||
|
}
|
||||||
|
const rBody = await fetch("/api/market/{{.Id}}/data", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
if (!rBody) return null;
|
||||||
|
const quantity = rBody.quantity
|
||||||
|
noQuantityInput.value = quantity
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</html>
|
|
@ -35,6 +35,10 @@
|
||||||
</strong>
|
</strong>
|
||||||
</code>
|
</code>
|
||||||
<div class="font-mono mb-1">A prediction market using the lightning network [WIP]</div>
|
<div class="font-mono mb-1">A prediction market using the lightning network [WIP]</div>
|
||||||
|
<div class="font-mono mb-1">ACTIVE MARKETS</div>
|
||||||
|
{{ range .markets }}
|
||||||
|
<a href="/market/{{.Id}}">{{.Description}}</a>
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
<footer class="flex justify-center">
|
<footer class="flex justify-center">
|
||||||
<div>
|
<div>
|
||||||
|
|
Loading…
Reference in New Issue