Create sell orders
This commit is contained in:
parent
00475b7914
commit
fd6111b590
@ -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);
|
||||||
|
12
db/market.go
12
db/market.go
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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([])
|
||||||
|
@ -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')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user