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, side ORDER_SIDE NOT NULL,
quantity BIGINT NOT NULL, quantity BIGINT NOT NULL,
price 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) order_id UUID REFERENCES orders(id)
); );
ALTER TABLE orders ADD CONSTRAINT order_price CHECK(price > 0 AND price < 100); 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 { func (db *DB) CreateOrder(tx *sql.Tx, ctx context.Context, order *Order) error {
if _, err := tx.ExecContext(ctx, ""+ if _, err := tx.ExecContext(ctx, ""+
"INSERT INTO orders(share_id, pubkey, side, quantity, price, invoice_id) "+ "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 { order.ShareId, order.Pubkey, order.Side, order.Quantity, order.Price, order.InvoiceId); err != nil {
return err return err
} }
@ -131,7 +131,7 @@ func (db *DB) FetchUserOrders(pubkey string, orders *[]Order) error {
"FROM orders o " + "FROM orders o " +
"JOIN invoices i ON o.invoice_id = i.id " + "JOIN invoices i ON o.invoice_id = i.id " +
"JOIN shares s ON o.share_id = s.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" "ORDER BY o.created_at DESC"
rows, err := db.Query(query, pubkey) rows, err := db.Query(query, pubkey)
if err != nil { if err != nil {
@ -153,7 +153,7 @@ func (db *DB) FetchMarketOrders(marketId int64, orders *[]Order) error {
"FROM orders o " + "FROM orders o " +
"JOIN shares s ON o.share_id = s.id " + "JOIN shares s ON o.share_id = s.id " +
"JOIN invoices i ON i.id = o.invoice_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" "ORDER BY o.created_at DESC"
rows, err := db.Query(query, marketId) rows, err := db.Query(query, marketId)
if err != nil { if err != nil {
@ -266,16 +266,16 @@ func (db *DB) FetchMarketStats(marketId int64, stats *MarketStats) error {
return nil 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 := "" + query := "" +
"SELECT s.description, " + "SELECT s.description, " +
"SUM(CASE WHEN o.side = 'BUY' THEN o.quantity ELSE -o.quantity END) " + "SUM(CASE WHEN o.side = 'BUY' THEN o.quantity ELSE -o.quantity END) " +
"FROM orders o " + "FROM orders o " +
"JOIN invoices i ON i.id = o.invoice_id " + "JOIN invoices i ON i.id = o.invoice_id " +
"JOIN shares s ON s.id = o.share_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" "GROUP BY o.pubkey, s.description"
rows, err := db.Query(query, pubkey, marketId) rows, err := tx.QueryContext(ctx, query, pubkey, marketId)
if err != nil { if err != nil {
return err return err
} }

View File

@ -26,6 +26,7 @@ func HandleMarket(sc context.ServerContext) echo.HandlerFunc {
err error err error
data map[string]any data map[string]any
u db.User u db.User
tx *sql.Tx
) )
if marketId, err = strconv.ParseInt(c.Param("id"), 10, 64); err != nil { if marketId, err = strconv.ParseInt(c.Param("id"), 10, 64); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request") 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 { if session := c.Get("session"); session != nil {
u = session.(db.User) 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) 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 return err
} }
lib.Merge(&data, &map[string]any{"user": uBalance}) 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) 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 // Create (unconfirmed) order
if invoice, err = sc.Lnd.CreateInvoice(tx, ctx, sc.Db, o.Pubkey, msats, description); err != nil { o.InvoiceId = invoice.Id
tx.Rollback() if err := sc.Db.CreateOrder(tx, ctx, &o); err != nil {
return err tx.Rollback()
} return err
// Create QR code to pay HODL invoice }
if qr, err = lib.ToQR(invoice.PaymentRequest); err != nil { // need to commit before starting to poll invoice status
tx.Rollback() tx.Commit()
return err go sc.Lnd.CheckInvoice(sc.Db, hash)
}
if hash, err = lntypes.MakeHashFromStr(invoice.Hash); err != nil { // TODO: find matching orders
tx.Rollback()
return err data = map[string]any{
"id": invoice.Id,
"bolt11": invoice.PaymentRequest,
"amount": msats,
"qr": qr,
}
return c.JSON(http.StatusPaymentRequired, data)
} }
// Create (unconfirmed) order // sell order: check user balance
o.InvoiceId = invoice.Id 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 { if err := sc.Db.CreateOrder(tx, ctx, &o); err != nil {
tx.Rollback() tx.Rollback()
return err return err
} }
// need to commit before starting to poll invoice status
tx.Commit() tx.Commit()
go sc.Lnd.CheckInvoice(sc.Db, hash) return c.JSON(http.StatusCreated, nil)
// TODO: find matching orders
data = map[string]any{
"id": invoice.Id,
"bolt11": invoice.PaymentRequest,
"amount": msats,
"qr": qr,
}
return c.JSON(http.StatusPaymentRequired, data)
} }
} }

View File

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

View File

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