diff --git a/db/init.sql b/db/init.sql index 9072d29..de22e94 100644 --- a/db/init.sql +++ b/db/init.sql @@ -12,11 +12,13 @@ CREATE TABLE sessions( pubkey TEXT NOT NULL REFERENCES users(pubkey), session_id VARCHAR(48) ); - +CREATE TYPE market_status AS ENUM ('WAITING_FOR_PAYMENT', 'ACTIVE', 'EXPIRED'); CREATE TABLE markets( id SERIAL PRIMARY KEY, description TEXT NOT NULL, - active BOOLEAN DEFAULT true + end_date TIMESTAMP WITH TIME ZONE NOT NULL, + status MARKET_STATUS NOT NULL DEFAULT 'WAITING_FOR_PAYMENT'; + invoice_id UUID NOT NULL UNIQUE REFERENCES invoices(id) ); CREATE EXTENSION "uuid-ossp"; CREATE TABLE shares( diff --git a/db/market.go b/db/market.go index 2e4e23c..52f0831 100644 --- a/db/market.go +++ b/db/market.go @@ -8,8 +8,30 @@ type FetchOrdersWhere struct { Confirmed bool } +func (db *DB) CreateMarket(market *Market) error { + if err := db.QueryRow(""+ + "INSERT INTO markets(description, end_date, status, invoice_id) "+ + "VALUES($1, $2, 'WAITING_FOR_PAYMENT', $3) "+ + "RETURNING id", market.Description, market.EndDate, market.InvoiceId).Scan(&market.Id); err != nil { + return err + } + // For now, we only support binary markets. + if _, err := db.Exec("INSERT INTO shares(market_id, description) VALUES ($1, 'YES'), ($1, 'NO')", market.Id); err != nil { + return err + } + return nil +} + +func (db *DB) MarkMarketAsActive(hash string) error { + _, err := db.Exec(""+ + "UPDATE markets SET status = 'ACTIVE' "+ + "WHERE invoice_id = (SELECT id FROM invoices WHERE hash = $1) "+ + "AND status = 'WAITING_FOR_PAYMENT'", hash) + return err +} + 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 { + if err := db.QueryRow("SELECT id, description, end_date, status FROM markets WHERE id = $1", marketId).Scan(&market.Id, &market.Description, &market.EndDate, &market.Status); err != nil { return err } return nil @@ -21,12 +43,12 @@ func (db *DB) FetchActiveMarkets(markets *[]Market) error { market Market err error ) - if rows, err = db.Query("SELECT id, description, active FROM markets WHERE active = true"); err != nil { + if rows, err = db.Query("SELECT id, description, end_date, status FROM markets WHERE status = 'ACTIVE'"); err != nil { return err } defer rows.Close() for rows.Next() { - rows.Scan(&market.Id, &market.Description, &market.Active) + rows.Scan(&market.Id, &market.Description, &market.EndDate, &market.Status) *markets = append(*markets, market) } return nil diff --git a/db/types.go b/db/types.go index 89e380c..27b3912 100644 --- a/db/types.go +++ b/db/types.go @@ -24,9 +24,11 @@ type ( SessionId string } Market struct { - Id Serial - Description string - Active bool + Id Serial `json:"id"` + Description string `json:"description"` + EndDate time.Time `json:"endDate"` + Status string `json:"status"` + InvoiceId UUID } Share struct { Id UUID diff --git a/lnd/invoice.go b/lnd/invoice.go index 337fa8d..ef3089c 100644 --- a/lnd/invoice.go +++ b/lnd/invoice.go @@ -2,6 +2,7 @@ package lnd import ( "context" + "database/sql" "log" "time" @@ -87,6 +88,10 @@ func (lnd *LNDClient) CheckInvoice(d *db.DB, hash lntypes.Hash) { break } if lnInvoice.AmountPaid > 0 { + if err = RunStateTransition(d, lnInvoice.Hash); err != nil { + handleLoopError(err) + continue + } if preimage, err = lntypes.MakePreimageFromStr(invoice.Preimage); err != nil { handleLoopError(err) continue @@ -107,6 +112,16 @@ func (lnd *LNDClient) CheckInvoice(d *db.DB, hash lntypes.Hash) { } } +func RunStateTransition(d *db.DB, hash lntypes.Hash) error { + if err := d.MarkMarketAsActive(hash.String()); err != nil { + if err == sql.ErrNoRows { + return nil + } + return err + } + return nil +} + func (lnd *LNDClient) CheckInvoices(d *db.DB) error { var ( invoices []db.Invoice diff --git a/server/router/handler/market.go b/server/router/handler/market.go index 419628f..5aef84f 100644 --- a/server/router/handler/market.go +++ b/server/router/handler/market.go @@ -47,6 +47,53 @@ func HandleMarket(sc context.ServerContext) echo.HandlerFunc { } } +func HandleCreateMarket(sc context.ServerContext) echo.HandlerFunc { + return func(c echo.Context) error { + var ( + u db.User + m db.Market + invoice *db.Invoice + msats int64 + invDescription string + data map[string]any + qr string + hash lntypes.Hash + err error + ) + if err := c.Bind(&m); err != nil { + return echo.NewHTTPError(http.StatusBadRequest) + } + + u = c.Get("session").(db.User) + msats = 1000 + // TODO: add [market:] for redirect after payment + invDescription = fmt.Sprintf("create market \"%s\" (%s)", m.Description, m.EndDate) + if invoice, err = sc.Lnd.CreateInvoice(sc.Db, u.Pubkey, msats, invDescription); err != nil { + return err + } + if qr, err = lib.ToQR(invoice.PaymentRequest); err != nil { + return err + } + if hash, err = lntypes.MakeHashFromStr(invoice.Hash); err != nil { + return err + } + go sc.Lnd.CheckInvoice(sc.Db, hash) + + m.InvoiceId = invoice.Id + if err := sc.Db.CreateMarket(&m); err != nil { + return err + } + + data = map[string]any{ + "id": invoice.Id, + "bolt11": invoice.PaymentRequest, + "amount": msats, + "qr": qr, + } + return c.JSON(http.StatusPaymentRequired, data) + } +} + func HandleOrder(sc context.ServerContext) echo.HandlerFunc { return func(c echo.Context) error { var ( diff --git a/server/router/router.go b/server/router/router.go index 32d095d..25a3b8b 100644 --- a/server/router/router.go +++ b/server/router/router.go @@ -41,6 +41,10 @@ func addFrontendRoutes(e *echo.Echo, sc ServerContext) { func addBackendRoutes(e *echo.Echo, sc ServerContext) { GET(e, sc, "/api/markets", handler.HandleMarkets) + POST(e, sc, "/api/market", + handler.HandleCreateMarket, + middleware.SessionGuard, + middleware.LNDGuard) GET(e, sc, "/api/market/:id", handler.HandleMarket) POST(e, sc, "/api/order", handler.HandleOrder, diff --git a/vue/src/components/Invoice.vue b/vue/src/components/Invoice.vue index 9c0e65d..367a4bf 100644 --- a/vue/src/components/Invoice.vue +++ b/vue/src/components/Invoice.vue @@ -110,7 +110,7 @@ await (async () => { if (body.Description) { const regexp = /\[market:(?[0-9]+)\]/ const m = body.Description.match(regexp) - const marketId = m.groups?.id + const marketId = m?.groups?.id if (marketId) { body.DescriptionMarketId = marketId body.Description = body.Description.replace(regexp, '') diff --git a/vue/src/components/MarketForm.vue b/vue/src/components/MarketForm.vue index 6fa0cdb..ee960e7 100644 --- a/vue/src/components/MarketForm.vue +++ b/vue/src/components/MarketForm.vue @@ -14,17 +14,27 @@ diff --git a/vue/src/components/MarketList.vue b/vue/src/components/MarketList.vue index 5b2365f..7539799 100644 --- a/vue/src/components/MarketList.vue +++ b/vue/src/components/MarketList.vue @@ -1,7 +1,7 @@