Create sell orders

This commit is contained in:
ekzyis 2023-11-29 18:41:24 +01:00
parent 00475b7914
commit fd6111b590
5 changed files with 86 additions and 57 deletions

View File

@ -47,7 +47,7 @@ CREATE TABLE orders(
side ORDER_SIDE NOT NULL,
quantity BIGINT NOT NULL,
price BIGINT NOT NULL,
invoice_id UUID NOT NULL REFERENCES invoices(id),
invoice_id UUID REFERENCES invoices(id),
order_id UUID REFERENCES orders(id)
);
ALTER TABLE orders ADD CONSTRAINT order_price CHECK(price > 0 AND price < 100);

View File

@ -107,7 +107,7 @@ func (db *DB) FetchOrders(where *FetchOrdersWhere, orders *[]Order) error {
func (db *DB) CreateOrder(tx *sql.Tx, ctx context.Context, order *Order) error {
if _, err := tx.ExecContext(ctx, ""+
"INSERT INTO orders(share_id, pubkey, side, quantity, price, invoice_id) "+
"VALUES ($1, $2, $3, $4, $5, $6)",
"VALUES ($1, $2, $3, $4, $5, CASE WHEN $6 = '' THEN NULL ELSE $6::UUID END)",
order.ShareId, order.Pubkey, order.Side, order.Quantity, order.Price, order.InvoiceId); err != nil {
return err
}
@ -131,7 +131,7 @@ func (db *DB) FetchUserOrders(pubkey string, orders *[]Order) error {
"FROM orders o " +
"JOIN invoices i ON o.invoice_id = i.id " +
"JOIN shares s ON o.share_id = s.id " +
"WHERE o.pubkey = $1 AND i.confirmed_at IS NOT NULL " +
"WHERE o.pubkey = $1 AND ( (o.side = 'BUY' AND i.confirmed_at IS NOT NULL) OR o.side = 'SELL' ) " +
"ORDER BY o.created_at DESC"
rows, err := db.Query(query, pubkey)
if err != nil {
@ -153,7 +153,7 @@ func (db *DB) FetchMarketOrders(marketId int64, orders *[]Order) error {
"FROM orders o " +
"JOIN shares s ON o.share_id = s.id " +
"JOIN invoices i ON i.id = o.invoice_id " +
"WHERE s.market_id = $1 AND i.confirmed_at IS NOT NULL " +
"WHERE s.market_id = $1 AND ( (o.side = 'BUY' AND i.confirmed_at IS NOT NULL) OR o.side = 'SELL' ) " +
"ORDER BY o.created_at DESC"
rows, err := db.Query(query, marketId)
if err != nil {
@ -266,16 +266,16 @@ func (db *DB) FetchMarketStats(marketId int64, stats *MarketStats) error {
return nil
}
func (db *DB) FetchUserBalance(marketId int64, pubkey string, balance *map[string]any) error {
func (db *DB) FetchUserBalance(tx *sql.Tx, ctx context.Context, marketId int, 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 " +
"WHERE o.pubkey = $1 AND s.market_id = $2 AND ( (o.side = 'BUY' AND i.confirmed_at IS NOT NULL AND o.order_id IS NOT NULL) OR o.side = 'SELL' ) " +
"GROUP BY o.pubkey, s.description"
rows, err := db.Query(query, pubkey, marketId)
rows, err := tx.QueryContext(ctx, query, pubkey, marketId)
if err != nil {
return err
}

View File

@ -26,6 +26,7 @@ func HandleMarket(sc context.ServerContext) echo.HandlerFunc {
err error
data map[string]any
u db.User
tx *sql.Tx
)
if marketId, err = strconv.ParseInt(c.Param("id"), 10, 64); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
@ -48,8 +49,15 @@ func HandleMarket(sc context.ServerContext) echo.HandlerFunc {
}
if session := c.Get("session"); session != nil {
u = session.(db.User)
ctx, cancel := context_.WithTimeout(context_.TODO(), 10*time.Second)
defer cancel()
if tx, err = sc.Db.BeginTx(ctx, nil); err != nil {
return err
}
defer tx.Commit()
uBalance := make(map[string]any)
if err = sc.Db.FetchUserBalance(marketId, u.Pubkey, &uBalance); err != nil {
if err = sc.Db.FetchUserBalance(tx, ctx, int(marketId), u.Pubkey, &uBalance); err != nil {
tx.Rollback()
return err
}
lib.Merge(&data, &map[string]any{"user": uBalance})
@ -181,43 +189,61 @@ func HandleOrder(sc context.ServerContext) echo.HandlerFunc {
}
description = fmt.Sprintf("%s %d %s shares @ %d sats [market:%d]", strings.ToUpper(o.Side), o.Quantity, s.Description, o.Price, s.MarketId)
// TODO: if SELL order, check share balance of user
if o.Side == "BUY" {
// === Create invoice ===
// We do this for BUY and SELL orders such that we can continue to use `invoice.confirmed_at IS NOT NULL`
// to check for confirmed orders
if invoice, err = sc.Lnd.CreateInvoice(tx, ctx, sc.Db, o.Pubkey, msats, description); err != nil {
tx.Rollback()
return err
}
// Create QR code to pay HODL invoice
if qr, err = lib.ToQR(invoice.PaymentRequest); err != nil {
tx.Rollback()
return err
}
if hash, err = lntypes.MakeHashFromStr(invoice.Hash); err != nil {
tx.Rollback()
return err
}
// Create HODL invoice
if invoice, err = sc.Lnd.CreateInvoice(tx, ctx, sc.Db, o.Pubkey, msats, description); err != nil {
tx.Rollback()
return err
}
// Create QR code to pay HODL invoice
if qr, err = lib.ToQR(invoice.PaymentRequest); err != nil {
tx.Rollback()
return err
}
if hash, err = lntypes.MakeHashFromStr(invoice.Hash); err != nil {
tx.Rollback()
return err
// Create (unconfirmed) order
o.InvoiceId = invoice.Id
if err := sc.Db.CreateOrder(tx, ctx, &o); err != nil {
tx.Rollback()
return err
}
// need to commit before starting to poll invoice status
tx.Commit()
go sc.Lnd.CheckInvoice(sc.Db, hash)
// TODO: find matching orders
data = map[string]any{
"id": invoice.Id,
"bolt11": invoice.PaymentRequest,
"amount": msats,
"qr": qr,
}
return c.JSON(http.StatusPaymentRequired, data)
}
// Create (unconfirmed) order
o.InvoiceId = invoice.Id
// sell order: check user balance
balance := make(map[string]any)
if err = sc.Db.FetchUserBalance(tx, ctx, s.MarketId, o.Pubkey, &balance); err != nil {
return err
}
if balance[s.Description].(int) < int(o.Quantity) {
tx.Rollback()
return c.JSON(http.StatusBadRequest, nil)
}
// SELL orders don't require payment by user
if err := sc.Db.CreateOrder(tx, ctx, &o); err != nil {
tx.Rollback()
return err
}
// need to commit before starting to poll invoice status
tx.Commit()
go sc.Lnd.CheckInvoice(sc.Db, hash)
// TODO: find matching orders
data = map[string]any{
"id": invoice.Id,
"bolt11": invoice.PaymentRequest,
"amount": msats,
"qr": qr,
}
return c.JSON(http.StatusPaymentRequired, data)
return c.JSON(http.StatusCreated, nil)
}
}

View File

@ -38,11 +38,20 @@ function mouseover (oid) {
const click = (order) => {
// redirect to form with prefilled inputs to match order
const stake = order.quantity * (100 - order.price)
const certainty = (100 - order.price) / 100
const share = order.ShareDescription === 'YES' ? 'NO' : 'YES'
const side = 'BUY'
router.push(`/market/${marketId}/form?stake=${stake}&certainty=${certainty}&side=${side}&share=${share}`)
if (order.side === 'BUY') {
// match BUY YES with BUY NO and vice versa
const stake = order.quantity * (100 - order.price)
const certainty = (100 - order.price) / 100
const share = order.ShareDescription === 'YES' ? 'NO' : 'YES'
router.push(`/market/${marketId}/form/buy?stake=${stake}&certainty=${certainty}&share=${share}`)
}
if (order.side === 'SELL') {
// match SELL YES with BUY YES and vice versa
const stake = order.quantity * order.price
const certainty = order.price / 100
const share = order.ShareDescription === 'YES' ? 'YES' : 'NO'
router.push(`/market/${marketId}/form/buy?stake=${stake}&certainty=${certainty}&share=${share}`)
}
}
const orders = ref([])

View File

@ -13,8 +13,8 @@
<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>
<label>{{ format(profit) }} sats</label>
<button class="col-span-2" type="submit" :disabled="disabled">submit sell order</button>
</form>
<div v-if="err" class="red text-center">{{ err }}</div>
</div>
@ -34,7 +34,7 @@ const showForm = computed(() => selected.value !== null)
const err = ref(null)
// how many shares wants the user sell?
const shares = ref(route.query.shares || 1)
const shares = ref(route.query.shares || 0)
// at which price?
const price = ref(route.query.price || 50)
// how high is the potential reward?
@ -56,18 +56,12 @@ await fetch(url)
.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 yesShareId = computed(() => market?.value.Shares.find(s => s.Description === 'YES').Id)
const noShareId = computed(() => market?.value.Shares.find(s => s.Description === 'NO').Id)
const shareId = computed(() => selected.value === 'YES' ? yesShareId.value : noShareId.value)
const userShares = computed(() => (selected.value === 'YES' ? market.value.user?.YES : market.value.user?.NO) || 0)
const disabled = computed(() => userShares.value === 0)
const submitForm = async () => {
if (!session.isAuthenticated) return router.push('/login')