From 278fd98db4cdd60bab1bf9e927b7b690431cbfc8 Mon Sep 17 00:00:00 2001 From: ekzyis Date: Mon, 15 Jul 2024 12:57:51 +0200 Subject: [PATCH] Add market form + page * market form with question, description and end date * markets cost 1k sats * a goroutine polls pending invoices from the db and checks LND for their status * markets are listed on front page (after paid) * market page contains buttons to bet yes or no TODO: * show correct market percentage * show how percentage changed over time in chart * validate end date * implement betting / order form --- db/schema.sql | 7 +- go.mod | 2 +- go.sum | 2 + lnd/lnd.go | 70 +++++++++++ main.go | 2 +- public/css/_tw-input.css | 58 ++++++++- server/router/handler/index.go | 35 +++++- server/router/handler/invoice.go | 69 +++++++++++ server/router/handler/market.go | 117 +++++++++++++++++++ server/router/pages/components/invoice.templ | 84 +++++++++++++ server/router/pages/components/modal.templ | 28 +++++ server/router/pages/components/qr.templ | 18 +++ server/router/pages/index.templ | 90 +++++++++++++- server/router/pages/lnAuth.templ | 1 - server/router/pages/market.templ | 36 ++++++ server/router/router.go | 5 + tailwind.config.js | 7 +- types/types.go | 34 +++++- 18 files changed, 646 insertions(+), 19 deletions(-) create mode 100644 server/router/handler/invoice.go create mode 100644 server/router/handler/market.go create mode 100644 server/router/pages/components/invoice.templ create mode 100644 server/router/pages/components/modal.templ create mode 100644 server/router/pages/market.templ diff --git a/db/schema.sql b/db/schema.sql index e2f2e31..88be482 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -1,5 +1,6 @@ CREATE TABLE users( id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL DEFAULT md5(random()::text), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, ln_pubkey TEXT UNIQUE, nostr_pubkey TEXT UNIQUE, @@ -24,10 +25,9 @@ CREATE TABLE invoices( user_id INTEGER NOT NULL REFERENCES users(id), msats BIGINT NOT NULL, msats_received BIGINT, - preimage TEXT NOT NULL UNIQUE, hash TEXT NOT NULL UNIQUE, bolt11 TEXT NOT NULL, - created_at TIMESTAMP WITH TIME ZONE NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, confirmed_at TIMESTAMP WITH TIME ZONE, held_since TIMESTAMP WITH TIME ZONE, @@ -37,7 +37,8 @@ CREATE TABLE invoices( CREATE TABLE markets( id SERIAL PRIMARY KEY, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - description TEXT NOT NULL, + question TEXT NOT NULL, + description TEXT, end_date TIMESTAMP WITH TIME ZONE NOT NULL, settled_at TIMESTAMP WITH TIME ZONE, user_id INTEGER NOT NULL REFERENCES users(id), diff --git a/go.mod b/go.mod index f11bf88..8dba26e 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,7 @@ require ( github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/decred/dcrd/lru v1.0.0 // indirect github.com/dsnet/compress v0.0.1 // indirect - github.com/dustin/go-humanize v1.0.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/fergusstrange/embedded-postgres v1.10.0 // indirect github.com/go-errors/errors v1.0.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect diff --git a/go.sum b/go.sum index 39c9b4a..7d3e9e7 100644 --- a/go.sum +++ b/go.sum @@ -161,6 +161,8 @@ github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5Jflh github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= 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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= diff --git a/lnd/lnd.go b/lnd/lnd.go index 3e0efd7..df5f779 100644 --- a/lnd/lnd.go +++ b/lnd/lnd.go @@ -1,9 +1,15 @@ package lnd import ( + "context" + "database/sql" "log" + "time" + "git.ekzyis.com/ekzyis/delphi.market/db" "github.com/lightninglabs/lndclient" + "github.com/lightningnetwork/lnd/invoices" + "github.com/lightningnetwork/lnd/lntypes" ) type LNDClient struct { @@ -25,3 +31,67 @@ func New(config *LNDConfig) (*LNDClient, error) { log.Printf("Connected to %s running LND v%s", config.LndAddress, lnd.Version.Version) return lnd, nil } + +func (lnd *LNDClient) CheckInvoices(db *db.DB) { + var ( + pending bool + rows *sql.Rows + hash lntypes.Hash + inv *lndclient.Invoice + err error + ) + for { + // fetch all pending invoices + if rows, err = db.Query("" + + "SELECT hash, expires_at " + + "FROM invoices " + + "WHERE confirmed_at IS NULL AND expires_at > CURRENT_TIMESTAMP"); err != nil { + log.Printf("error checking invoices: %v", err) + } + + pending = false + + for rows.Next() { + pending = true + + var ( + h string + expiresAt time.Time + ) + rows.Scan(&h, &expiresAt) + + if hash, err = lntypes.MakeHashFromStr(h); err != nil { + log.Printf("error parsing hash: %v", err) + continue + } + + if inv, err = lnd.Client.LookupInvoice(context.TODO(), hash); err != nil { + log.Printf("error looking up invoice: %v", err) + continue + } + + if !inv.State.IsFinal() { + log.Printf("invoice pending: %s %s", h, time.Until(expiresAt)) + continue + } + + if inv.State == invoices.ContractSettled { + if _, err = db.Exec( + "UPDATE invoices SET msats_received = $1, confirmed_at = $2 WHERE hash = $3", + inv.AmountPaid, inv.SettleDate, h); err != nil { + log.Printf("error updating invoice %s: %v", h, err) + } + log.Printf("invoice confirmed: %s", h) + } else if inv.State == invoices.ContractCanceled { + log.Printf("invoice expired: %s", h) + } + } + + // poll faster if there are pending invoices + if pending { + time.Sleep(1 * time.Second) + } else { + time.Sleep(5 * time.Second) + } + } +} diff --git a/main.go b/main.go index 1ca81f1..b608d21 100644 --- a/main.go +++ b/main.go @@ -68,7 +68,7 @@ func init() { log.Printf("[warn] error connecting to LND: %v\n", err) lnd_ = nil } else { - // lnd_.CheckInvoices(db_) + go lnd_.CheckInvoices(db_) } ctx = server.Context{ diff --git a/public/css/_tw-input.css b/public/css/_tw-input.css index f28c2a4..f381fab 100644 --- a/public/css/_tw-input.css +++ b/public/css/_tw-input.css @@ -10,6 +10,10 @@ --nostr: #8d45dd; --black: #000; --white: #fff; + --bg-success: #149e613d; + --fg-success: #35df8d; + --bg-error: #f5395e3d; + --fg-error: #ff7386; } @layer base { @@ -41,13 +45,17 @@ a:not(.no-link), button[hx-get], - button[hx-post] { + button[hx-post], + button[type="submit"], + .button { transition: background-color 150ms ease-in, color 150ms ease-in; } a:not(.no-link):hover, button[hx-get]:hover, - button[hx-post]:hover { + button[hx-post]:hover, + button[type="submit"]:hover, + .button:hover { background-color: var(--color); color: var(--background); } @@ -57,13 +65,16 @@ text-decoration: underline; } - button[hx-post] { + button[hx-post], + button[type="submit"], + .button { border-width: 1px; } nav a, button[hx-get], - button[hx-post] { + button[hx-post], + button[type="submit"] { padding: 0 0.25em; } @@ -80,6 +91,41 @@ color: var(--muted); } + .text-reset { + color: var(--color); + } + + .hitbox { + padding: 15px; + margin: -15px; + } + + .neon { + border: none; + padding: 0.5em 3em; + transition: background-color 150ms ease-in, color 150ms ease-in; + } + + .neon.success { + background-color: var(--bg-success); + color: var(--fg-success); + } + + .neon.success:hover { + background-color: var(--fg-success); + color: var(--white); + } + + .neon.error { + background-color: var(--bg-error); + color: var(--fg-error); + } + + .neon.error:hover { + background-color: var(--fg-error); + color: var(--white); + } + .figlet { display: flex; align-items: center; @@ -115,4 +161,8 @@ .nostr:hover { filter: brightness(125%) drop-shadow(0 0 0.33rem var(--nostr)); } + + #modal { + backdrop-filter: blur(10px); + } } \ No newline at end of file diff --git a/server/router/handler/index.go b/server/router/handler/index.go index e0ae7ed..289e0cf 100644 --- a/server/router/handler/index.go +++ b/server/router/handler/index.go @@ -1,13 +1,46 @@ package handler import ( + "database/sql" + "git.ekzyis.com/ekzyis/delphi.market/server/router/context" "git.ekzyis.com/ekzyis/delphi.market/server/router/pages" + "git.ekzyis.com/ekzyis/delphi.market/types" "github.com/labstack/echo/v4" ) func HandleIndex(sc context.Context) echo.HandlerFunc { return func(c echo.Context) error { - return pages.Index().Render(context.RenderContext(sc, c), c.Response().Writer) + var ( + db = sc.Db + ctx = c.Request().Context() + rows *sql.Rows + err error + markets []types.Market + ) + + if rows, err = db.QueryContext(ctx, ""+ + "SELECT m.id, m.question, m.description, m.created_at, m.end_date, "+ + "u.id, u.name, u.created_at, u.ln_pubkey, u.nostr_pubkey, u.msats "+ + "FROM markets m "+ + "JOIN users u ON m.user_id = u.id "+ + "JOIN invoices i ON m.invoice_id = i.id "+ + "WHERE i.confirmed_at IS NOT NULL"); err != nil { + return err + } + + for rows.Next() { + var m types.Market + var u types.User + if err = rows.Scan( + &m.Id, &m.Question, &m.Description, &m.CreatedAt, &m.EndDate, + &u.Id, &u.Name, &u.CreatedAt, &u.LnPubkey, &u.NostrPubkey, &u.Msats); err != nil { + return err + } + m.User = u + markets = append(markets, m) + } + + return pages.Index(markets).Render(context.RenderContext(sc, c), c.Response().Writer) } } diff --git a/server/router/handler/invoice.go b/server/router/handler/invoice.go new file mode 100644 index 0000000..450a32d --- /dev/null +++ b/server/router/handler/invoice.go @@ -0,0 +1,69 @@ +package handler + +import ( + "database/sql" + "fmt" + "net/http" + "regexp" + "time" + + "git.ekzyis.com/ekzyis/delphi.market/server/router/context" + "git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components" + "git.ekzyis.com/ekzyis/delphi.market/types" + "github.com/a-h/templ" + "github.com/labstack/echo/v4" +) + +func HandleInvoice(sc context.Context) echo.HandlerFunc { + return func(c echo.Context) error { + var ( + db = sc.Db + ctx = c.Request().Context() + hash = c.Param("hash") + u = c.Get("session").(types.User) + inv = types.Invoice{} + expiresIn int + paid bool + redirectUrl templ.SafeURL + qr templ.Component + err error + ) + + if err = db.QueryRowContext(ctx, ""+ + "SELECT user_id, msats, COALESCE(msats_received, 0), expires_at, confirmed_at, bolt11, COALESCE(description, '') "+ + "FROM invoices "+ + "WHERE hash = $1", hash). + Scan(&inv.UserId, &inv.Msats, &inv.MsatsReceived, &inv.ExpiresAt, &inv.ConfirmedAt, &inv.Bolt11, &inv.Description); err != nil { + if err == sql.ErrNoRows { + return echo.NewHTTPError(http.StatusNotFound) + } + c.Logger().Error(err) + return echo.NewHTTPError(http.StatusInternalServerError) + } + + if u.Id != inv.UserId { + return echo.NewHTTPError(http.StatusNotFound) + } + + expiresIn = int(time.Until(inv.ExpiresAt).Seconds()) + paid = inv.MsatsReceived >= inv.Msats + redirectUrl = toRedirectUrl(inv.Description) + + qr = components.Invoice(hash, inv.Bolt11, int(inv.Msats), expiresIn, paid, redirectUrl) + + return components.Modal(qr).Render(context.RenderContext(sc, c), c.Response().Writer) + } +} + +var ( + marketRegexp = regexp.MustCompile("^create market (?P[0-9]+)$") +) + +func toRedirectUrl(description string) templ.SafeURL { + var m []string + if m = marketRegexp.FindStringSubmatch(description); m != nil { + marketId := m[marketRegexp.SubexpIndex("id")] + return templ.SafeURL(fmt.Sprintf("/market/%s", marketId)) + } + return "/" +} diff --git a/server/router/handler/market.go b/server/router/handler/market.go new file mode 100644 index 0000000..8238e9f --- /dev/null +++ b/server/router/handler/market.go @@ -0,0 +1,117 @@ +package handler + +import ( + "database/sql" + "fmt" + "net/http" + "time" + + "git.ekzyis.com/ekzyis/delphi.market/server/router/context" + "git.ekzyis.com/ekzyis/delphi.market/server/router/pages" + "git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components" + "git.ekzyis.com/ekzyis/delphi.market/types" + "github.com/a-h/templ" + "github.com/labstack/echo/v4" + "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" +) + +func HandleCreate(sc context.Context) echo.HandlerFunc { + return func(c echo.Context) error { + var ( + db = sc.Db + lnd = sc.Lnd + tx *sql.Tx + ctx = c.Request().Context() + u = c.Get("session").(types.User) + question = c.FormValue("question") + description = c.FormValue("description") + endDate = c.FormValue("end_date") + hash lntypes.Hash + paymentRequest string + cost = lnwire.MilliSatoshi(1000e3) + expiry = int64(600) + expiresAt = time.Now().Add(time.Second * time.Duration(expiry)) + invoiceId int + marketId int + invDescription string + qr templ.Component + err error + ) + // TODO: validation + + if tx, err = db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}); err != nil { + return err + } + + if hash, paymentRequest, err = lnd.Client.AddInvoice(ctx, + &invoicesrpc.AddInvoiceData{ + Value: cost, + Expiry: expiry, + }); err != nil { + return err + } + + if err = tx.QueryRowContext(ctx, ""+ + "INSERT INTO invoices (user_id, msats, hash, bolt11, expires_at) "+ + "VALUES ($1, $2, $3, $4, $5) "+ + "RETURNING id", + u.Id, cost, hash.String(), paymentRequest, expiresAt).Scan(&invoiceId); err != nil { + return err + } + + if err = tx.QueryRowContext(ctx, ""+ + "INSERT INTO markets (question, description, end_date, user_id, invoice_id) "+ + "VALUES ($1, $2, $3, $4, $5) "+ + "RETURNING id", + question, description, endDate, u.Id, invoiceId).Scan(&marketId); err != nil { + return err + } + + invDescription = fmt.Sprintf("create market %d", marketId) + if _, err = tx.ExecContext(ctx, ""+ + "UPDATE invoices SET description = $1 WHERE id = $2", + invDescription, invoiceId); err != nil { + return err + } + + if err = tx.Commit(); err != nil { + return err + } + + qr = components.Invoice(hash.String(), paymentRequest, int(cost), int(expiry), false, toRedirectUrl(invDescription)) + + return components.Modal(qr).Render(context.RenderContext(sc, c), c.Response().Writer) + } +} + +func HandleMarket(sc context.Context) echo.HandlerFunc { + return func(c echo.Context) error { + var ( + db = sc.Db + ctx = c.Request().Context() + id = c.Param("id") + m = types.Market{} + u = types.User{} + err error + ) + + if err = db.QueryRowContext(ctx, ""+ + "SELECT m.id, m.question, m.description, m.created_at, m.end_date, "+ + "u.id, u.name, u.created_at, u.ln_pubkey, u.nostr_pubkey, msats "+ + "FROM markets m JOIN users u ON m.user_id = u.id "+ + "WHERE m.id = $1", id).Scan( + &m.Id, &m.Question, &m.Description, &m.CreatedAt, &m.EndDate, + &u.Id, &u.Name, &u.CreatedAt, &u.LnPubkey, &u.NostrPubkey, &u.Msats); err != nil { + if err == sql.ErrNoRows { + return echo.NewHTTPError(http.StatusNotFound) + } + return err + } + + m.User = u + + return pages.Market(m).Render(context.RenderContext(sc, c), c.Response().Writer) + } +} diff --git a/server/router/pages/components/invoice.templ b/server/router/pages/components/invoice.templ new file mode 100644 index 0000000..b5aa5d2 --- /dev/null +++ b/server/router/pages/components/invoice.templ @@ -0,0 +1,84 @@ +package components + +import ( + "fmt" +) + +templ Invoice(hash string, bolt11 string, msats int, expiresIn int, paid bool, redirectUrl templ.SafeURL) { +
+
+
Payment Required
+
@Qr(bolt11, "lightning:"+bolt11)
+
{ format(msats) }
+ @InvoiceStatus(hash, expiresIn, paid, redirectUrl) +
+ +
+} + +templ InvoiceStatus(hash string, expiresIn int, paid bool, redirectUrl templ.SafeURL) { + if paid { +
PAID
+
+ } + else if expiresIn <= 0 { +
EXPIRED
+ } else { + +
+ +
+ } +} + +func format(msats int) string { + sats := msats / 1000 + if sats == 1 { + return fmt.Sprintf("%d sat", sats) + } + return fmt.Sprintf("%d sats", sats) +} \ No newline at end of file diff --git a/server/router/pages/components/modal.templ b/server/router/pages/components/modal.templ new file mode 100644 index 0000000..3c5e927 --- /dev/null +++ b/server/router/pages/components/modal.templ @@ -0,0 +1,28 @@ +package components + +templ Modal(component templ.Component) { + if component != nil { + + } else { + + } +} diff --git a/server/router/pages/components/qr.templ b/server/router/pages/components/qr.templ index b0e2a1b..739a782 100644 --- a/server/router/pages/components/qr.templ +++ b/server/router/pages/components/qr.templ @@ -13,11 +13,29 @@ templ Qr(value string, href string) { > + + { value } + + + @CopyButton(value) } else { } } +templ CopyButton(value string) { +
+ +} + func qrEncode(value string) string { png, err := qrcode.Encode(value, qrcode.Medium, 256) if err != nil { diff --git a/server/router/pages/index.templ b/server/router/pages/index.templ index 7247772..f979f7b 100644 --- a/server/router/pages/index.templ +++ b/server/router/pages/index.templ @@ -1,17 +1,99 @@ package pages -import "git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components" +import ( + c "git.ekzyis.com/ekzyis/delphi.market/server/router/context" + "git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components" + "git.ekzyis.com/ekzyis/delphi.market/types" + "fmt" + "github.com/dustin/go-humanize" +) -templ Index() { +templ Index(markets []types.Market) { - @components.Head() + @components.Head(), @components.Nav()
@components.Figlet("random", "delphi") -
A prediction market using the lightning network
+
A prediction market using the lightning network
+
+
+ + +
+ if ctx.Value(c.ReqPathContextKey).(string) == "/" { +
+ for _, m := range markets { + + { m.Question } +
{m.User.Name} / {humanize.Time(m.CreatedAt)} / {humanize.Time(m.EndDate)}
+
+
51%
+
0
+ } +
+ } else { +
+ + +
+ + optional +
+ + + + +
+ } +
+ @components.Modal(nil) @components.Footer() } + +func tabStyle(path string, tab string) string { + class := "!no-underline" + if path == tab { + class += " font-bold border-b-none" + } + return class +} + +func rowStyle(position int, max int) string { + return "pb-3 my-3" +} \ No newline at end of file diff --git a/server/router/pages/lnAuth.templ b/server/router/pages/lnAuth.templ index 02d15cf..3d3f867 100644 --- a/server/router/pages/lnAuth.templ +++ b/server/router/pages/lnAuth.templ @@ -18,7 +18,6 @@ templ LnAuth(lnurl string, action string) { hx-push-url="true" > @components.Qr(lnurl, "lightning:"+lnurl) - { lnurl }
diff --git a/server/router/pages/market.templ b/server/router/pages/market.templ new file mode 100644 index 0000000..4031b10 --- /dev/null +++ b/server/router/pages/market.templ @@ -0,0 +1,36 @@ +package pages + +import ( + "git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components" + "git.ekzyis.com/ekzyis/delphi.market/types" + "github.com/dustin/go-humanize" +) + +// TODO: Add countdown? Use or at least show somewhere precise timestamps? + +templ Market(m types.Market) { + + @components.Head() + + @components.Nav() +
+ + @components.Figlet("random", "market") + +
{ m.Question }
+
{humanize.Time(m.EndDate)}
+
+
+ { m.Description } +
― {m.User.Name}, { humanize.Time(m.CreatedAt) }
+
+
+ + +
+
+ @components.Modal(nil) + @components.Footer() + + +} \ No newline at end of file diff --git a/server/router/router.go b/server/router/router.go index 804f890..bca25ab 100644 --- a/server/router/router.go +++ b/server/router/router.go @@ -14,6 +14,9 @@ func Init(e *echo.Echo, sc Context) { e.Use(middleware.Session(sc)) e.GET("/", handler.HandleIndex(sc)) + e.GET("/create", handler.HandleIndex(sc)) + e.POST("/create", handler.HandleCreate(sc), middleware.SessionGuard(sc)) + e.GET("/market/:id", handler.HandleMarket(sc)) e.GET("/about", handler.HandleAbout(sc)) e.GET("/login", handler.HandleAuth(sc, "login")) @@ -25,4 +28,6 @@ func Init(e *echo.Echo, sc Context) { e.GET("/user", handler.HandleUser(sc), middleware.SessionGuard(sc)) e.POST("/logout", handler.HandleLogout(sc), middleware.SessionGuard(sc)) + + e.GET("/invoice/:hash", handler.HandleInvoice(sc), middleware.SessionGuard(sc)) } diff --git a/tailwind.config.js b/tailwind.config.js index 90ba410..e7f3003 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -6,7 +6,12 @@ module.exports = { center: true, padding: '1rem' }, - extend: {}, + extend: { + colors: { + 'background': '191d21', + 'muted': '#6c757d', + }, + }, }, plugins: [ function ({ addComponents }) { diff --git a/types/types.go b/types/types.go index 92b62af..4232d01 100644 --- a/types/types.go +++ b/types/types.go @@ -1,11 +1,39 @@ package types -import "time" +import ( + "time" + + "gopkg.in/guregu/null.v4" +) type User struct { Id int + Name string CreatedAt time.Time - LnPubkey string - NostrPubkey string + LnPubkey null.String + NostrPubkey null.String Msats int64 } + +type Invoice struct { + Id int + UserId int + Msats int64 + MsatsReceived int64 + Hash string + Bolt11 string + CreatedAt time.Time + ExpiresAt time.Time + ConfirmedAt null.Time + HeldSince bool + Description string +} + +type Market struct { + Id int + User User + Question string + Description string + CreatedAt time.Time + EndDate time.Time +}