Add sell order form

This commit is contained in:
ekzyis 2023-11-29 08:11:56 +01:00
parent dc082ec4f2
commit 0ae9f671d4
10 changed files with 325 additions and 119 deletions

View File

@ -265,3 +265,31 @@ func (db *DB) FetchMarketStats(marketId int64, stats *MarketStats) error {
}
return nil
}
func (db *DB) FetchUserBalance(marketId int64, pubkey string, balance *map[string]any) error {
query := "" +
"SELECT s.description, " +
"SUM(CASE WHEN o.side = 'BUY' THEN o.quantity ELSE -o.quantity END) " +
"FROM orders o " +
"JOIN invoices i ON i.id = o.invoice_id " +
"JOIN shares s ON s.id = o.share_id " +
"WHERE o.pubkey = $1 AND s.market_id = $2 AND i.confirmed_at IS NOT NULL AND o.order_id IS NOT NULL " +
"GROUP BY o.pubkey, s.description"
rows, err := db.Query(query, pubkey, marketId)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
sdesc string
val int
)
if err = rows.Scan(&sdesc, &val); err != nil {
return err
}
(*balance)[sdesc] = val
}
return nil
}

View File

@ -25,6 +25,7 @@ func HandleMarket(sc context.ServerContext) echo.HandlerFunc {
orders []db.Order
err error
data map[string]any
u db.User
)
if marketId, err = strconv.ParseInt(c.Param("id"), 10, 64); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
@ -45,6 +46,14 @@ func HandleMarket(sc context.ServerContext) echo.HandlerFunc {
"Description": market.Description,
"Shares": shares,
}
if session := c.Get("session"); session != nil {
u = session.(db.User)
uBalance := make(map[string]any)
if err = sc.Db.FetchUserBalance(marketId, u.Pubkey, &uBalance); err != nil {
return err
}
lib.Merge(&data, &map[string]any{"user": uBalance})
}
return c.JSON(http.StatusOK, data)
}
}

View File

@ -0,0 +1,132 @@
<template>
<div>
<button type="button" :class="yesClass" class="label success font-mono mx-1 my-3"
@click.prevent="toggleYes">YES</button>
<button type="button" :class="noClass" class="label error font-mono mx-1 my-3" @click.prevent="toggleNo">NO</button>
<form v-show="showForm" @submit.prevent="submitForm">
<label for="stake">how much?</label>
<input name="stake" v-model="stake" type="number" min="0" placeholder="sats" required />
<label for="certainty">how sure?</label>
<input name="certainty" v-model="certainty" type="number" min="0.01" max="0.99" step="0.01" required />
<label>you receive:</label>
<label>{{ format(shares) }} {{ selected }} shares @ {{ format(price) }} sats</label>
<label>you pay:</label>
<label>{{ format(cost) }} sats</label>
<label>if you win:</label>
<label>+{{ format(profit) }} sats</label>
<button class="col-span-2" type="submit">submit buy order</button>
</form>
<div v-if="err" class="red text-center">{{ err }}</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const router = useRouter()
const route = useRoute()
const marketId = route.params.id
const selected = ref(route.query.share || null)
const showForm = computed(() => selected.value !== null)
const err = ref(null)
// how much wants the user bet?
const stake = ref(route.query.stake || 100)
// how sure is the user he will win?
const certainty = ref(route.query.certainty || 0.5)
// price per share: more risk, lower price, higher reward
const price = computed(() => certainty.value * 100)
// how many (full) shares can be bought?
const shares = computed(() => {
const val = price.value > 0 ? stake.value / price.value : null
// only full shares can be bought
return Math.round(val)
})
// how much does this order cost?
const cost = computed(() => {
return shares.value * price.value
})
// how high is the potential reward?
const profit = computed(() => {
// shares expire at 10 or 0 sats
const val = (100 * shares.value) - cost.value
return isNaN(val) ? 0 : val
})
const format = (x, i = 3) => x === null ? null : x >= 1 ? Math.round(x) : x === 0 ? x : x.toFixed(i)
const market = ref(null)
const url = '/api/market/' + marketId
await fetch(url)
.then(r => r.json())
.then(body => {
market.value = body
})
.catch(console.error)
// Currently, we only support binary markets.
// (only events which can be answered with YES and NO)
const yesShareId = computed(() => {
return market?.value.Shares.find(s => s.Description === 'YES').Id
})
const noShareId = computed(() => {
return market?.value.Shares.find(s => s.Description === 'NO').Id
})
const shareId = computed(() => {
return selected.value === 'YES' ? yesShareId.value : noShareId.value
})
const submitForm = async () => {
// TODO validate form
const url = window.origin + '/api/order'
const body = JSON.stringify({
sid: shareId.value,
quantity: shares.value,
price: price.value,
// TODO support selling
side: 'BUY'
})
const res = await fetch(url, { method: 'POST', headers: { 'Content-type': 'application/json' }, body })
const resBody = await res.json()
if (res.status !== 402) {
err.value = `error: server responded with HTTP ${resBody.status}`
return
}
const invoiceId = resBody.id
router.push('/invoice/' + invoiceId)
}
const toggleYes = () => {
selected.value = selected.value === 'YES' ? null : 'YES'
}
const toggleNo = () => {
selected.value = selected.value === 'NO' ? null : 'NO'
}
const yesClass = computed(() => selected.value === 'YES' ? ['active'] : [])
const noClass = computed(() => selected.value === 'NO' ? ['active'] : [])
</script>
<style scoped>
.success.active {
background-color: #35df8d;
color: white;
}
.error.active {
background-color: #ff7386;
color: white;
}
form {
margin: 0 auto;
display: grid;
grid-template-columns: auto auto;
}
form>* {
margin: 0.5em 1em;
}
</style>

View File

@ -18,7 +18,7 @@
</nav>
</header>
<Suspense>
<router-view class="m-3" />
<router-view />
</Suspense>
</template>

View File

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

View File

@ -1,5 +1,5 @@
<template>
<div class="text w-auto">
<div class="text w-auto mt-3">
<table>
<thead>
<th>description</th>

View File

@ -1,5 +1,5 @@
<template>
<div>
<div class="mt-3">
<div class="mb-1">
<span>YES: {{ (currentYes * 100).toFixed(2) }}%</span>
<span>NO: {{ (currentNo * 100).toFixed(2) }}%</span>

View File

@ -1,131 +1,31 @@
<template>
<div>
<button type="button" :class="yesClass" class="label success font-mono mx-1 my-3"
@click.prevent="toggleYes">YES</button>
<button type="button" :class="noClass" class="label error font-mono mx-1 my-3" @click.prevent="toggleNo">NO</button>
<form v-show="showForm" @submit.prevent="submitForm">
<label for="stake">how much?</label>
<input name="stake" v-model="stake" type="number" min="0" placeholder="sats" required />
<label for="certainty">how sure?</label>
<input name="certainty" v-model="certainty" type="number" min="0.01" max="0.99" step="0.01" required />
<label>you receive:</label>
<label>{{ format(shares) }} {{ selected }} shares @ {{ format(price) }} sats</label>
<label>you pay:</label>
<label>{{ format(cost) }} sats</label>
<label>if you win:</label>
<label>{{ format(profit) }} sats</label>
<button class="col-span-2" type="submit">submit order</button>
</form>
<div v-if="err" class="red text-center">{{ err }}</div>
<header class="flex flex-row text-center justify-center pt-1">
<nav>
<StyledLink :to="'/market/' + marketId + '/form/buy'">buy</StyledLink>
<StyledLink :to="'/market/' + marketId + '/form/sell'">sell</StyledLink>
</nav>
</header>
<router-view />
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useRoute } from 'vue-router'
import StyledLink from '@/components/StyledLink'
const router = useRouter()
const route = useRoute()
const marketId = route.params.id
const selected = ref(route.query.share || null)
const showForm = computed(() => selected.value !== null)
const yesClass = computed(() => selected.value === 'YES' ? ['active'] : [])
const noClass = computed(() => selected.value === 'NO' ? ['active'] : [])
const err = ref(null)
// how much wants the user bet?
const stake = ref(route.query.stake || 100)
// how sure is the user he will win?
const certainty = ref(route.query.certainty || 0.5)
// price per share: more risk, lower price, higher reward
const price = computed(() => certainty.value * 100)
// how many (full) shares can be bought?
const shares = computed(() => {
const val = price.value > 0 ? stake.value / price.value : null
// only full shares can be bought
return Math.round(val)
})
// how much does this order cost?
const cost = computed(() => {
return shares.value * price.value
})
// how high is the potential reward?
const profit = computed(() => {
// shares expire at 10 or 0 sats
const val = (100 * shares.value) - cost.value
return isNaN(val) ? 0 : val
})
const format = (x, i = 3) => x === null ? null : x >= 1 ? Math.round(x) : x === 0 ? x : x.toFixed(i)
const market = ref(null)
const url = '/api/market/' + marketId
await fetch(url)
.then(r => r.json())
.then(body => {
market.value = body
})
.catch(console.error)
// Currently, we only support binary markets.
// (only events which can be answered with YES and NO)
const yesShareId = computed(() => {
return market?.value.Shares.find(s => s.Description === 'YES').Id
})
const noShareId = computed(() => {
return market?.value.Shares.find(s => s.Description === 'NO').Id
})
const shareId = computed(() => {
return selected.value === 'YES' ? yesShareId.value : noShareId.value
})
const toggleYes = () => {
selected.value = selected.value === 'YES' ? null : 'YES'
}
const toggleNo = () => {
selected.value = selected.value === 'NO' ? null : 'NO'
}
const submitForm = async () => {
// TODO validate form
const url = window.origin + '/api/order'
const body = JSON.stringify({
sid: shareId.value,
quantity: shares.value,
price: price.value,
// TODO support selling
side: 'BUY'
})
const res = await fetch(url, { method: 'POST', headers: { 'Content-type': 'application/json' }, body })
const resBody = await res.json()
if (res.status !== 402) {
err.value = `error: server responded with HTTP ${resBody.status}`
return
}
const invoiceId = resBody.id
router.push('/invoice/' + invoiceId)
}
</script>
<style scoped>
.success.active {
background-color: #35df8d;
color: white;
nav {
display: flex;
justify-content: center;
}
.error.active {
background-color: #ff7386;
color: white;
}
form {
margin: 0 auto;
display: grid;
grid-template-columns: auto auto;
}
form>* {
margin: 0.5em 1em;
nav>a {
margin: 0 3px;
}
</style>

View File

@ -0,0 +1,123 @@
<template>
<div>
<button type="button" :class="yesClass" class="label success font-mono mx-1 my-3"
@click.prevent="toggleYes">YES</button>
<button type="button" :class="noClass" class="label error font-mono mx-1 my-3" @click.prevent="toggleNo">NO</button>
<form v-show="showForm" @submit.prevent="submitForm">
<label for="inv">you have:</label>
<label name="inv">{{ userShares }} shares</label>
<label for="shares">how many?</label>
<input name="shares" v-model="shares" type="number" min="1" :max="userShares" placeholder="shares" required />
<label for="price">price?</label>
<input name="price" v-model="price" type="number" min="1" max="99" step="1" required />
<label>you sell:</label>
<label>{{ shares }} {{ selected }} shares @ {{ price }} sats</label>
<label>you make:</label>
<label>+{{ format(profit) }} sats</label>
<button class="col-span-2" type="submit">submit sell order</button>
</form>
<div v-if="err" class="red text-center">{{ err }}</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const router = useRouter()
const route = useRoute()
const marketId = route.params.id
const selected = ref(route.query.share || null)
const showForm = computed(() => selected.value !== null)
const err = ref(null)
// how many shares wants the user sell?
const shares = ref(route.query.shares || 1)
// at which price?
const price = ref(route.query.price || 50)
// how high is the potential reward?
const profit = computed(() => {
const val = shares.value * price.value
return isNaN(val) ? 0 : val
})
const format = (x, i = 3) => x === null ? null : x >= 1 ? Math.round(x) : x === 0 ? x : x.toFixed(i)
const market = ref(null)
const url = '/api/market/' + marketId
await fetch(url)
.then(r => r.json())
.then(body => {
market.value = body
})
.catch(console.error)
// Currently, we only support binary markets.
// (only events which can be answered with YES and NO)
const yesShareId = computed(() => {
return market?.value.Shares.find(s => s.Description === 'YES').Id
})
const noShareId = computed(() => {
return market?.value.Shares.find(s => s.Description === 'NO').Id
})
const shareId = computed(() => {
return selected.value === 'YES' ? yesShareId.value : noShareId.value
})
const userShares = computed(() => {
return selected.value === 'YES' ? market.value.user.YES : market.value.user.NO
})
const submitForm = async () => {
// TODO validate form
const url = window.origin + '/api/order'
const body = JSON.stringify({
sid: shareId.value,
quantity: shares.value,
price: price.value,
// TODO support selling
side: 'SELL'
})
const res = await fetch(url, { method: 'POST', headers: { 'Content-type': 'application/json' }, body })
const resBody = await res.json()
if (res.status !== 402) {
err.value = `error: server responded with HTTP ${resBody.status}`
return
}
const invoiceId = resBody.id
router.push('/invoice/' + invoiceId)
}
const toggleYes = () => {
selected.value = selected.value === 'YES' ? null : 'YES'
}
const toggleNo = () => {
selected.value = selected.value === 'NO' ? null : 'NO'
}
const yesClass = computed(() => selected.value === 'YES' ? ['active'] : [])
const noClass = computed(() => selected.value === 'NO' ? ['active'] : [])
</script>
<style scoped>
.success.active {
background-color: #35df8d;
color: white;
}
.error.active {
background-color: #ff7386;
color: white;
}
form {
margin: 0 auto;
display: grid;
grid-template-columns: auto auto;
}
form>* {
margin: 0.5em 1em;
}
</style>

View File

@ -16,6 +16,8 @@ import UserOrders from '@/components/UserOrders'
import OrderForm from '@/components/OrderForm'
import MarketOrders from '@/components/MarketOrders'
import MarketStats from '@/components/MarketStats'
import BuyOrderForm from '@/components/BuyOrderForm'
import SellOrderForm from '@/components/SellOrderForm'
const routes = [
{
@ -37,7 +39,19 @@ const routes = [
path: '/market/:id',
component: MarketView,
children: [
{ path: 'form', name: 'form', component: OrderForm },
{
path: 'form',
name: 'form',
component: OrderForm,
children: [
{
path: 'buy', name: 'form-buy', component: BuyOrderForm
},
{
path: 'sell', name: 'form-sell', component: SellOrderForm
}
]
},
{ path: 'orders', name: 'market-orders', component: MarketOrders },
{ path: 'stats', name: 'market-stats', component: MarketStats }
]