Allow creation of markets

This commit is contained in:
ekzyis 2023-11-20 02:47:16 +01:00
parent 0e587d48ca
commit ce64608894
9 changed files with 115 additions and 13 deletions

View File

@ -12,11 +12,13 @@ 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 TYPE market_status AS ENUM ('WAITING_FOR_PAYMENT', 'ACTIVE', 'EXPIRED');
CREATE TABLE markets( CREATE TABLE markets(
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
description TEXT NOT NULL, 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 EXTENSION "uuid-ossp";
CREATE TABLE shares( CREATE TABLE shares(

View File

@ -8,8 +8,30 @@ type FetchOrdersWhere struct {
Confirmed bool 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 { 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 err
} }
return nil return nil
@ -21,12 +43,12 @@ func (db *DB) FetchActiveMarkets(markets *[]Market) error {
market Market market Market
err error 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 return err
} }
defer rows.Close() defer rows.Close()
for rows.Next() { 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) *markets = append(*markets, market)
} }
return nil return nil

View File

@ -24,9 +24,11 @@ type (
SessionId string SessionId string
} }
Market struct { Market struct {
Id Serial Id Serial `json:"id"`
Description string Description string `json:"description"`
Active bool EndDate time.Time `json:"endDate"`
Status string `json:"status"`
InvoiceId UUID
} }
Share struct { Share struct {
Id UUID Id UUID

View File

@ -2,6 +2,7 @@ package lnd
import ( import (
"context" "context"
"database/sql"
"log" "log"
"time" "time"
@ -87,6 +88,10 @@ func (lnd *LNDClient) CheckInvoice(d *db.DB, hash lntypes.Hash) {
break break
} }
if lnInvoice.AmountPaid > 0 { if lnInvoice.AmountPaid > 0 {
if err = RunStateTransition(d, lnInvoice.Hash); err != nil {
handleLoopError(err)
continue
}
if preimage, err = lntypes.MakePreimageFromStr(invoice.Preimage); err != nil { if preimage, err = lntypes.MakePreimageFromStr(invoice.Preimage); err != nil {
handleLoopError(err) handleLoopError(err)
continue 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 { func (lnd *LNDClient) CheckInvoices(d *db.DB) error {
var ( var (
invoices []db.Invoice invoices []db.Invoice

View File

@ -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:<id>] 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 { func HandleOrder(sc context.ServerContext) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
var ( var (

View File

@ -41,6 +41,10 @@ func addFrontendRoutes(e *echo.Echo, sc ServerContext) {
func addBackendRoutes(e *echo.Echo, sc ServerContext) { func addBackendRoutes(e *echo.Echo, sc ServerContext) {
GET(e, sc, "/api/markets", handler.HandleMarkets) 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) GET(e, sc, "/api/market/:id", handler.HandleMarket)
POST(e, sc, "/api/order", POST(e, sc, "/api/order",
handler.HandleOrder, handler.HandleOrder,

View File

@ -110,7 +110,7 @@ await (async () => {
if (body.Description) { if (body.Description) {
const regexp = /\[market:(?<id>[0-9]+)\]/ const regexp = /\[market:(?<id>[0-9]+)\]/
const m = body.Description.match(regexp) const m = body.Description.match(regexp)
const marketId = m.groups?.id const marketId = m?.groups?.id
if (marketId) { if (marketId) {
body.DescriptionMarketId = marketId body.DescriptionMarketId = marketId
body.Description = body.Description.replace(regexp, '') body.Description = body.Description.replace(regexp, '')

View File

@ -14,17 +14,27 @@
<script setup> <script setup>
import { ref, defineProps } from 'vue' import { ref, defineProps } from 'vue'
import { useRouter } from 'vue-router'
defineProps(['onCancel']) defineProps(['onCancel'])
const router = useRouter()
const form = ref(null) const form = ref(null)
const description = ref(null) const description = ref(null)
const endDate = ref(null) const endDate = ref(null)
const parseEndDate = endDate => {
const [yyyy, mm, dd] = endDate.split('-')
return `${yyyy}-${mm}-${dd}T00:00:00.000Z`
}
const submitForm = async () => { const submitForm = async () => {
const url = window.origin + '/api/market' const url = window.origin + '/api/market'
const body = JSON.stringify({ description: description.value, endDate: endDate.value }) const body = JSON.stringify({ description: description.value, endDate: parseEndDate(endDate.value) })
await fetch(url, { method: 'post', headers: { 'Content-type': 'application/json' }, body }) const res = await fetch(url, { method: 'post', headers: { 'Content-type': 'application/json' }, body })
const resBody = await res.json()
const invoiceId = resBody.id
router.push('/invoice/' + invoiceId)
} }
</script> </script>

View File

@ -1,7 +1,7 @@
<template> <template>
<ul> <ul>
<li class="my-3" v-for="market in markets" :key="market.Id"> <li class="my-3" v-for="market in markets" :key="market.id">
<router-link :to="'/market/' + market.Id">{{ market.Description }}</router-link> <router-link :to="'/market/' + market.id">{{ market.description }}</router-link>
</li> </li>
</ul> </ul>
<button v-if="!showForm" @click.prevent="toggleForm">+ create market</button> <button v-if="!showForm" @click.prevent="toggleForm">+ create market</button>