Order cancelation
This commit is contained in:
parent
3979bbd86f
commit
49cacb266c
|
@ -6,7 +6,8 @@ CREATE TABLE lnauth(
|
||||||
);
|
);
|
||||||
CREATE TABLE users(
|
CREATE TABLE users(
|
||||||
pubkey TEXT PRIMARY KEY,
|
pubkey TEXT PRIMARY KEY,
|
||||||
last_seen TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
last_seen TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
msats BIGINT NOT NULL DEFAULT 0
|
||||||
);
|
);
|
||||||
CREATE TABLE sessions(
|
CREATE TABLE sessions(
|
||||||
pubkey TEXT NOT NULL REFERENCES users(pubkey),
|
pubkey TEXT NOT NULL REFERENCES users(pubkey),
|
||||||
|
@ -42,6 +43,7 @@ CREATE TYPE order_side AS ENUM ('BUY', 'SELL');
|
||||||
CREATE TABLE orders(
|
CREATE TABLE orders(
|
||||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP WITH TIME ZONE,
|
||||||
share_id UUID NOT NULL REFERENCES shares(id),
|
share_id UUID NOT NULL REFERENCES shares(id),
|
||||||
pubkey TEXT NOT NULL REFERENCES users(pubkey),
|
pubkey TEXT NOT NULL REFERENCES users(pubkey),
|
||||||
side ORDER_SIDE NOT NULL,
|
side ORDER_SIDE NOT NULL,
|
||||||
|
|
20
db/market.go
20
db/market.go
|
@ -73,17 +73,17 @@ func (db *DB) FetchShare(tx *sql.Tx, ctx context.Context, shareId string, share
|
||||||
|
|
||||||
func (db *DB) FetchOrders(where *FetchOrdersWhere, orders *[]Order) error {
|
func (db *DB) FetchOrders(where *FetchOrdersWhere, orders *[]Order) error {
|
||||||
query := "" +
|
query := "" +
|
||||||
"SELECT o.id, share_id, o.pubkey, o.side, o.quantity, o.price, o.invoice_id, o.created_at, s.description, s.market_id, i.confirmed_at " +
|
"SELECT o.id, share_id, o.pubkey, o.side, o.quantity, o.price, o.invoice_id, o.created_at, o.deleted_at, s.description, s.market_id, i.confirmed_at " +
|
||||||
"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 "
|
"WHERE o.deleted_at IS NULL "
|
||||||
var args []any
|
var args []any
|
||||||
if where.MarketId > 0 {
|
if where.MarketId > 0 {
|
||||||
query += "share_id = ANY(SELECT id FROM shares WHERE market_id = $1) "
|
query += "AND share_id = ANY(SELECT id FROM shares WHERE market_id = $1) "
|
||||||
args = append(args, where.MarketId)
|
args = append(args, where.MarketId)
|
||||||
} else if where.Pubkey != "" {
|
} else if where.Pubkey != "" {
|
||||||
query += "o.pubkey = $1 "
|
query += "AND o.pubkey = $1 "
|
||||||
args = append(args, where.Pubkey)
|
args = append(args, where.Pubkey)
|
||||||
}
|
}
|
||||||
if where.Confirmed {
|
if where.Confirmed {
|
||||||
|
@ -98,7 +98,7 @@ func (db *DB) FetchOrders(where *FetchOrdersWhere, orders *[]Order) error {
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var order Order
|
var order Order
|
||||||
rows.Scan(&order.Id, &order.ShareId, &order.Pubkey, &order.Side, &order.Quantity, &order.Price, &order.InvoiceId, &order.CreatedAt, &order.Share.Description, &order.Share.MarketId, &order.Invoice.ConfirmedAt)
|
rows.Scan(&order.Id, &order.ShareId, &order.Pubkey, &order.Side, &order.Quantity, &order.Price, &order.InvoiceId, &order.CreatedAt, &order.DeletedAt, &order.Share.Description, &order.Share.MarketId, &order.Invoice.ConfirmedAt)
|
||||||
*orders = append(*orders, order)
|
*orders = append(*orders, order)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -116,12 +116,12 @@ func (db *DB) CreateOrder(tx *sql.Tx, ctx context.Context, order *Order) error {
|
||||||
|
|
||||||
func (db *DB) FetchOrder(tx *sql.Tx, ctx context.Context, orderId string, order *Order) error {
|
func (db *DB) FetchOrder(tx *sql.Tx, ctx context.Context, orderId string, order *Order) error {
|
||||||
query := "" +
|
query := "" +
|
||||||
"SELECT o.id, o.share_id, o.pubkey, o.side, o.quantity, o.price, o.invoice_id, o.created_at, s.description, s.market_id, i.confirmed_at " +
|
"SELECT o.id, o.share_id, o.pubkey, o.side, o.quantity, o.price, o.created_at, o.deleted_at, s.description, s.market_id, i.confirmed_at, o.invoice_id, COALESCE(i.msats_received, 0) " +
|
||||||
"FROM orders o " +
|
"FROM orders o " +
|
||||||
"JOIN invoices i ON o.invoice_id = i.id " +
|
"LEFT 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.id = $1"
|
"WHERE o.id = $1"
|
||||||
return tx.QueryRowContext(ctx, query, orderId).Scan(&order.Id, &order.ShareId, &order.Pubkey, &order.Side, &order.Quantity, &order.Price, &order.InvoiceId, &order.CreatedAt, &order.Share.Description, &order.MarketId, &order.Invoice.ConfirmedAt)
|
return tx.QueryRowContext(ctx, query, orderId).Scan(&order.Id, &order.ShareId, &order.Pubkey, &order.Side, &order.Quantity, &order.Price, &order.CreatedAt, &order.DeletedAt, &order.Share.Description, &order.MarketId, &order.Invoice.ConfirmedAt, &order.InvoiceId, &order.Invoice.MsatsReceived)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) FetchUserOrders(pubkey string, orders *[]Order) error {
|
func (db *DB) FetchUserOrders(pubkey string, orders *[]Order) error {
|
||||||
|
@ -131,7 +131,7 @@ func (db *DB) FetchUserOrders(pubkey string, orders *[]Order) error {
|
||||||
"FROM orders o " +
|
"FROM orders o " +
|
||||||
"LEFT JOIN invoices i ON o.invoice_id = i.id " +
|
"LEFT 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 ( (o.side = 'BUY' AND i.confirmed_at IS NOT NULL) OR o.side = 'SELL' ) " +
|
"WHERE o.pubkey = $1 AND ( (o.side = 'BUY' AND i.confirmed_at IS NOT NULL) OR o.side = 'SELL' ) AND o.deleted_at IS NULL " +
|
||||||
"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 " +
|
||||||
"LEFT JOIN invoices i ON o.invoice_id = i.id " +
|
"LEFT JOIN invoices i ON o.invoice_id = i.id " +
|
||||||
"WHERE s.market_id = $1 AND ( (o.side = 'BUY' AND i.confirmed_at IS NOT NULL) OR o.side = 'SELL' ) " +
|
"WHERE s.market_id = $1 AND ( (o.side = 'BUY' AND i.confirmed_at IS NOT NULL) OR o.side = 'SELL' ) AND o.deleted_at IS NULL " +
|
||||||
"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 {
|
||||||
|
|
|
@ -52,6 +52,7 @@ type (
|
||||||
Order struct {
|
Order struct {
|
||||||
Id UUID
|
Id UUID
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
|
DeletedAt null.Time
|
||||||
ShareId string `json:"sid"`
|
ShareId string `json:"sid"`
|
||||||
ShareDescription string
|
ShareDescription string
|
||||||
Share
|
Share
|
||||||
|
@ -59,7 +60,7 @@ type (
|
||||||
Side string `json:"side"`
|
Side string `json:"side"`
|
||||||
Quantity int64 `json:"quantity"`
|
Quantity int64 `json:"quantity"`
|
||||||
Price int64 `json:"price"`
|
Price int64 `json:"price"`
|
||||||
InvoiceId UUID
|
InvoiceId null.String
|
||||||
Invoice
|
Invoice
|
||||||
OrderId UUID
|
OrderId UUID
|
||||||
}
|
}
|
||||||
|
|
|
@ -208,7 +208,7 @@ func HandleOrder(sc context.ServerContext) echo.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create (unconfirmed) order
|
// Create (unconfirmed) order
|
||||||
o.InvoiceId = invoice.Id
|
o.InvoiceId.String = invoice.Id
|
||||||
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
|
||||||
|
@ -217,8 +217,6 @@ func HandleOrder(sc context.ServerContext) echo.HandlerFunc {
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
go sc.Lnd.CheckInvoice(sc.Db, hash)
|
go sc.Lnd.CheckInvoice(sc.Db, hash)
|
||||||
|
|
||||||
// TODO: find matching orders
|
|
||||||
|
|
||||||
data = map[string]any{
|
data = map[string]any{
|
||||||
"id": invoice.Id,
|
"id": invoice.Id,
|
||||||
"bolt11": invoice.PaymentRequest,
|
"bolt11": invoice.PaymentRequest,
|
||||||
|
@ -247,6 +245,82 @@ func HandleOrder(sc context.ServerContext) echo.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HandleDeleteOrder(sc context.ServerContext) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
var (
|
||||||
|
orderId string
|
||||||
|
tx *sql.Tx
|
||||||
|
u db.User
|
||||||
|
o db.Order
|
||||||
|
msats int64
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if orderId = c.Param("id"); orderId == "" {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
u = c.Get("session").(db.User)
|
||||||
|
|
||||||
|
// transaction start
|
||||||
|
ctx, cancel := context_.WithTimeout(c.Request().Context(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if tx, err = sc.Db.BeginTx(ctx, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Commit()
|
||||||
|
|
||||||
|
if err = sc.Db.FetchOrder(tx, ctx, orderId, &o); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Pubkey != o.Pubkey {
|
||||||
|
// order does not belong to user
|
||||||
|
tx.Rollback()
|
||||||
|
return echo.NewHTTPError(http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.OrderId != "" {
|
||||||
|
// order already settled
|
||||||
|
tx.Rollback()
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.DeletedAt.Valid {
|
||||||
|
// order already deleted
|
||||||
|
tx.Rollback()
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.Invoice.ConfirmedAt.Valid {
|
||||||
|
// order already paid: we need to move paid sats to user balance before deleting the order
|
||||||
|
msats = o.Invoice.MsatsReceived
|
||||||
|
if res, err := tx.ExecContext(ctx, "UPDATE users SET msats = msats + $1 WHERE pubkey = $2", msats, u.Pubkey); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
// make sure exactly one row was affected
|
||||||
|
if rowsAffected, err := res.RowsAffected(); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
} else if rowsAffected != 1 {
|
||||||
|
tx.Rollback()
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = tx.ExecContext(ctx, "UPDATE orders SET deleted_at = CURRENT_TIMESTAMP WHERE id = $1", o.Id); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func HandleOrders(sc context.ServerContext) echo.HandlerFunc {
|
func HandleOrders(sc context.ServerContext) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -52,6 +52,9 @@ func addBackendRoutes(e *echo.Echo, sc ServerContext) {
|
||||||
handler.HandleOrder,
|
handler.HandleOrder,
|
||||||
middleware.SessionGuard,
|
middleware.SessionGuard,
|
||||||
middleware.LNDGuard)
|
middleware.LNDGuard)
|
||||||
|
DELETE(e, sc, "/api/order/:id",
|
||||||
|
handler.HandleDeleteOrder,
|
||||||
|
middleware.SessionGuard)
|
||||||
GET(e, sc, "/api/orders",
|
GET(e, sc, "/api/orders",
|
||||||
handler.HandleOrders,
|
handler.HandleOrders,
|
||||||
middleware.SessionGuard)
|
middleware.SessionGuard)
|
||||||
|
@ -75,6 +78,10 @@ func POST(e *echo.Echo, sc ServerContext, path string, scF HandlerFunc, scM ...M
|
||||||
return e.POST(path, scF(sc), toMiddlewareFunc(sc, scM...)...)
|
return e.POST(path, scF(sc), toMiddlewareFunc(sc, scM...)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DELETE(e *echo.Echo, sc ServerContext, path string, scF HandlerFunc, scM ...MiddlewareFunc) *echo.Route {
|
||||||
|
return e.DELETE(path, scF(sc), toMiddlewareFunc(sc, scM...)...)
|
||||||
|
}
|
||||||
|
|
||||||
func Use(e *echo.Echo, sc ServerContext, scM ...MiddlewareFunc) {
|
func Use(e *echo.Echo, sc ServerContext, scM ...MiddlewareFunc) {
|
||||||
e.Use(toMiddlewareFunc(sc, scM...)...)
|
e.Use(toMiddlewareFunc(sc, scM...)...)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
<th></th>
|
<th></th>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<OrderRow :order="o" v-for="o in orders" :key="o.Id" @mouseover="() => mouseover(o.Id)"
|
<OrderRow :order="o" v-for="o in orders" :key="o.Id" @mouseover="() => mouseover(o.Id)" :selected="selected"
|
||||||
:selected="selected" :click="click" />
|
:onMatchClick="onMatchClick" />
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,7 +36,7 @@ const mouseover = (oid) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const click = (order) => {
|
const onMatchClick = (order) => {
|
||||||
// redirect to form with prefilled inputs to match order
|
// redirect to form with prefilled inputs to match order
|
||||||
if (order.side === 'BUY') {
|
if (order.side === 'BUY') {
|
||||||
// match BUY YES with BUY NO and vice versa
|
// match BUY YES with BUY NO and vice versa
|
||||||
|
|
|
@ -5,8 +5,9 @@
|
||||||
</td>
|
</td>
|
||||||
<td :title="order.CreatedAt" class="hidden-sm">{{ ago(new Date(order.CreatedAt)) }}</td>
|
<td :title="order.CreatedAt" class="hidden-sm">{{ ago(new Date(order.CreatedAt)) }}</td>
|
||||||
<td :class="'font-mono ' + statusClassName + ' ' + selectedClassName" @mouseover="mouseover">{{ order.Status }}</td>
|
<td :class="'font-mono ' + statusClassName + ' ' + selectedClassName" @mouseover="mouseover">{{ order.Status }}</td>
|
||||||
<td v-if="showContextMenu && !!session.pubkey">
|
<td v-if="showContextMenu">
|
||||||
<button @click="() => click(order)" :disabled="mine">match</button>
|
<button @click="() => onMatchClick?.(order)" v-if="showMatch">match</button>
|
||||||
|
<button @click="() => cancelOrder(order)" v-if="showCancel">cancel</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
@ -17,13 +18,14 @@ import ago from 's-ago'
|
||||||
import { useSession } from '@/stores/session'
|
import { useSession } from '@/stores/session'
|
||||||
|
|
||||||
const session = useSession()
|
const session = useSession()
|
||||||
const props = defineProps(['order', 'selected', 'click'])
|
const props = defineProps(['order', 'selected', 'onMatchClick'])
|
||||||
|
|
||||||
const order = ref(props.order)
|
const order = ref(props.order)
|
||||||
const showContextMenu = ref(false)
|
const showContextMenu = ref(false)
|
||||||
const click = ref(props.click)
|
const onMatchClick = ref(props.onMatchClick)
|
||||||
|
const mine = computed(() => order.value.Pubkey === session?.pubkey)
|
||||||
const mine = order.value.Pubkey === session?.pubkey
|
const showMatch = computed(() => !mine.value && order.value.Status !== 'EXECUTED')
|
||||||
|
const showCancel = computed(() => mine.value && order.value.Status !== 'EXECUTED')
|
||||||
|
|
||||||
const statusClassName = computed(() => {
|
const statusClassName = computed(() => {
|
||||||
const status = order.value.Status
|
const status = order.value.Status
|
||||||
|
@ -43,15 +45,18 @@ const selectedClassName = computed(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const mouseover = () => {
|
const mouseover = () => {
|
||||||
if (!!props.click && order.value.Status === 'PENDING') {
|
showContextMenu.value = true && !!session.pubkey
|
||||||
showContextMenu.value = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mouseleave = () => {
|
const mouseleave = () => {
|
||||||
showContextMenu.value = false
|
showContextMenu.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cancelOrder = async () => {
|
||||||
|
const url = '/api/order/' + order.value.Id
|
||||||
|
await fetch(url, { method: 'DELETE' }).catch(console.error)
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
Loading…
Reference in New Issue