Add order form
* add POST /api/order -> 402 Payment Required * redirect to /invoice/:id
This commit is contained in:
parent
55afc3b097
commit
ebdf0d1e90
@ -49,12 +49,12 @@ type (
|
||||
Order struct {
|
||||
Id UUID
|
||||
CreatedAt time.Time
|
||||
ShareId string `form:"share_id"`
|
||||
ShareId string `json:"sid"`
|
||||
Share
|
||||
Pubkey string
|
||||
Side string `form:"side"`
|
||||
Quantity int64 `form:"quantity"`
|
||||
Price int64 `form:"price"`
|
||||
Side string `json:"side"`
|
||||
Quantity int64 `json:"quantity"`
|
||||
Price int64 `json:"price"`
|
||||
InvoiceId UUID
|
||||
Invoice
|
||||
}
|
||||
|
@ -2,12 +2,10 @@ package handler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
||||
"git.ekzyis.com/ekzyis/delphi.market/env"
|
||||
"git.ekzyis.com/ekzyis/delphi.market/lib"
|
||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
||||
"github.com/labstack/echo/v4"
|
||||
@ -39,23 +37,17 @@ func HandleMarket(sc context.ServerContext) echo.HandlerFunc {
|
||||
return err
|
||||
}
|
||||
data = map[string]any{
|
||||
"session": c.Get("session"),
|
||||
"Id": market.Id,
|
||||
"Description": market.Description,
|
||||
// shares are sorted by description in descending order
|
||||
// that's how we know that YES must be the first share
|
||||
"YesShare": shares[0],
|
||||
"NoShare": shares[1],
|
||||
"Orders": orders,
|
||||
"Shares": shares,
|
||||
}
|
||||
return c.JSON(http.StatusOK, data)
|
||||
}
|
||||
}
|
||||
|
||||
func HandlePostOrder(sc context.ServerContext) echo.HandlerFunc {
|
||||
func HandleOrder(sc context.ServerContext) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var (
|
||||
marketId string
|
||||
u db.User
|
||||
o db.Order
|
||||
invoice *db.Invoice
|
||||
@ -65,7 +57,6 @@ func HandlePostOrder(sc context.ServerContext) echo.HandlerFunc {
|
||||
hash lntypes.Hash
|
||||
err error
|
||||
)
|
||||
marketId = c.Param("id")
|
||||
// TODO:
|
||||
// [ ] Step 0: If SELL order, check share balance of user
|
||||
// [x] Create HODL invoice
|
||||
@ -108,12 +99,11 @@ func HandlePostOrder(sc context.ServerContext) echo.HandlerFunc {
|
||||
// TODO: find matching orders
|
||||
|
||||
data = map[string]any{
|
||||
"session": c.Get("session"),
|
||||
"lnurl": invoice.PaymentRequest,
|
||||
"id": invoice.Id,
|
||||
"bolt11": invoice.PaymentRequest,
|
||||
"amount": msats,
|
||||
"qr": qr,
|
||||
"invoice": *invoice,
|
||||
"redirectURL": fmt.Sprintf("https://%s/market/%s", env.PublicURL, marketId),
|
||||
}
|
||||
return sc.Render(c, http.StatusPaymentRequired, "invoice.html", data)
|
||||
return c.JSON(http.StatusPaymentRequired, data)
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ func addFrontendRoutes(e *echo.Echo, sc ServerContext) {
|
||||
handler.HandleMarket,
|
||||
middleware.SessionGuard)
|
||||
POST(e, sc, "/market/:id/order",
|
||||
handler.HandlePostOrder,
|
||||
handler.HandleOrder,
|
||||
middleware.SessionGuard,
|
||||
middleware.LNDGuard)
|
||||
GET(e, sc, "/invoice/:id",
|
||||
@ -42,6 +42,10 @@ func addFrontendRoutes(e *echo.Echo, sc ServerContext) {
|
||||
func addBackendRoutes(e *echo.Echo, sc ServerContext) {
|
||||
GET(e, sc, "/api/markets", handler.HandleMarkets)
|
||||
GET(e, sc, "/api/market/:id", handler.HandleMarket)
|
||||
POST(e, sc, "/api/order",
|
||||
handler.HandleOrder,
|
||||
middleware.SessionGuard,
|
||||
middleware.LNDGuard)
|
||||
GET(e, sc, "/api/login", handler.HandleLogin)
|
||||
GET(e, sc, "/api/login/callback", handler.HandleLoginCallback)
|
||||
POST(e, sc, "/api/logout", handler.HandleLogout)
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<NavBar />
|
||||
<div id="container">
|
||||
<NavBar />
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
@ -38,6 +38,7 @@ body {
|
||||
}
|
||||
|
||||
#container {
|
||||
margin: 1em;
|
||||
margin: 1em auto;
|
||||
width: fit-content;
|
||||
}
|
||||
</style>
|
||||
|
@ -107,31 +107,6 @@ figcaption {
|
||||
}
|
||||
|
||||
.label {
|
||||
width: fit-content;
|
||||
margin: 1em auto;
|
||||
padding: 0.5em 3em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.label:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: rgba(20, 158, 97, .24);
|
||||
color: #35df8d;
|
||||
}
|
||||
|
||||
.success:hover {
|
||||
background-color: #35df8d;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: rgba(245, 57, 94, .24);
|
||||
color: #ff7386;
|
||||
}
|
||||
|
||||
.error:hover {
|
||||
background-color: #ff7386;
|
||||
}
|
||||
</style>
|
||||
|
@ -10,17 +10,63 @@
|
||||
</div>
|
||||
<div class="font-mono">{{ market.Description }}</div>
|
||||
<!-- eslint-enable -->
|
||||
<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" />
|
||||
<label for="certainty">how sure?</label>
|
||||
<input name="certainty" v-model="certainty" type="number" min="0" max="1" step="0.001"
|
||||
placeholder="fraction like 0.5" />
|
||||
<label>you receive:</label>
|
||||
<label>{{ format(shares) }} shares @ 🗲{{ format(price) }}</label>
|
||||
<label>you pay:</label>
|
||||
<label>🗲{{ format(cost) }}</label>
|
||||
<label>if you win:</label>
|
||||
<label>+🗲{{ format(profit) }}</label>
|
||||
<button class="col-span-2" type="submit">submit order</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
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(null)
|
||||
const showForm = computed(() => selected.value !== null)
|
||||
const yesClass = computed(() => selected.value === 'yes' ? ['active'] : [])
|
||||
const noClass = computed(() => selected.value === 'no' ? ['active'] : [])
|
||||
|
||||
// how much wants the user bet?
|
||||
const stake = ref(null)
|
||||
// how sure is the user he will win?
|
||||
const certainty = ref(null)
|
||||
// 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.toFixed(i)
|
||||
|
||||
const market = ref(null)
|
||||
|
||||
const url = '/api/market/' + marketId
|
||||
await fetch(url)
|
||||
.then(r => r.json())
|
||||
@ -28,5 +74,62 @@ await fetch(url)
|
||||
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()
|
||||
const invoiceId = resBody.id
|
||||
router.push('/invoice/' + invoiceId)
|
||||
}
|
||||
|
||||
</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>
|
||||
|
@ -13,6 +13,10 @@ button:hover {
|
||||
background: #8787A4;
|
||||
}
|
||||
|
||||
input {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #8787a4;
|
||||
text-decoration: underline;
|
||||
@ -27,3 +31,32 @@ a.selected {
|
||||
color: #ffffff;
|
||||
background: #8787A4;
|
||||
}
|
||||
|
||||
.label {
|
||||
border: none;
|
||||
width: fit-content;
|
||||
padding: 0.5em 3em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.label:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: rgba(20, 158, 97, .24);
|
||||
color: #35df8d;
|
||||
}
|
||||
|
||||
.success:hover {
|
||||
background-color: #35df8d;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: rgba(245, 57, 94, .24);
|
||||
color: #ff7386;
|
||||
}
|
||||
|
||||
.error:hover {
|
||||
background-color: #ff7386;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user