Allow creation of markets
This commit is contained in:
parent
0e587d48ca
commit
ce64608894
|
@ -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(
|
||||||
|
|
28
db/market.go
28
db/market.go
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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, '')
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue