Remove old code
* removed code that will not be used * removed code that will most likely be rewritten
This commit is contained in:
parent
4ba568951d
commit
ee3e5e82e4
2
Makefile
2
Makefile
@ -1,6 +1,6 @@
|
|||||||
.PHONY: build run test
|
.PHONY: build run test
|
||||||
|
|
||||||
SOURCE := $(shell find db env lib lnd pages public server -type f) main.go
|
SOURCE := $(shell find db env lib lnd public server -type f) main.go
|
||||||
|
|
||||||
build: delphi.market
|
build: delphi.market
|
||||||
|
|
||||||
|
106
db/invoice.go
106
db/invoice.go
@ -1,106 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (db *DB) CreateInvoice(tx *sql.Tx, ctx context.Context, invoice *Invoice) error {
|
|
||||||
if err := tx.QueryRowContext(ctx, ""+
|
|
||||||
"INSERT INTO invoices(pubkey, msats, preimage, hash, bolt11, created_at, expires_at, description) "+
|
|
||||||
"VALUES($1, $2, $3, $4, $5, $6, $7, $8) "+
|
|
||||||
"RETURNING id",
|
|
||||||
invoice.Pubkey, invoice.Msats, invoice.Preimage, invoice.Hash, invoice.PaymentRequest, invoice.CreatedAt, invoice.ExpiresAt, invoice.Description).Scan(&invoice.Id); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type FetchInvoiceWhere struct {
|
|
||||||
Id string
|
|
||||||
Hash string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) FetchInvoice(where *FetchInvoiceWhere, invoice *Invoice) error {
|
|
||||||
var (
|
|
||||||
query = "SELECT id, pubkey, msats, preimage, hash, bolt11, created_at, expires_at, confirmed_at, held_since, COALESCE(description, '') FROM invoices "
|
|
||||||
args []any
|
|
||||||
)
|
|
||||||
if where.Id != "" {
|
|
||||||
query += "WHERE id = $1"
|
|
||||||
args = append(args, where.Id)
|
|
||||||
} else if where.Hash != "" {
|
|
||||||
query += "WHERE hash = $1"
|
|
||||||
args = append(args, where.Hash)
|
|
||||||
}
|
|
||||||
if err := db.QueryRow(query, args...).Scan(
|
|
||||||
&invoice.Id, &invoice.Pubkey, &invoice.Msats, &invoice.Preimage, &invoice.Hash,
|
|
||||||
&invoice.PaymentRequest, &invoice.CreatedAt, &invoice.ExpiresAt, &invoice.ConfirmedAt, &invoice.HeldSince, &invoice.Description); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type FetchInvoicesWhere struct {
|
|
||||||
Unconfirmed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) FetchInvoices(where *FetchInvoicesWhere, invoices *[]Invoice) error {
|
|
||||||
var (
|
|
||||||
rows *sql.Rows
|
|
||||||
invoice Invoice
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
var (
|
|
||||||
query = "SELECT id, pubkey, msats, preimage, hash, bolt11, created_at, expires_at, confirmed_at, held_since, COALESCE(description, '') FROM invoices "
|
|
||||||
)
|
|
||||||
if where.Unconfirmed {
|
|
||||||
query += "WHERE confirmed_at IS NULL"
|
|
||||||
}
|
|
||||||
if rows, err = db.Query(query); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
rows.Scan(
|
|
||||||
&invoice.Id, &invoice.Pubkey, &invoice.Msats, &invoice.Preimage, &invoice.Hash,
|
|
||||||
&invoice.PaymentRequest, &invoice.CreatedAt, &invoice.ExpiresAt, &invoice.ConfirmedAt, &invoice.HeldSince, &invoice.Description)
|
|
||||||
*invoices = append(*invoices, invoice)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) FetchUserInvoices(pubkey string, invoices *[]Invoice) error {
|
|
||||||
var (
|
|
||||||
rows *sql.Rows
|
|
||||||
invoice Invoice
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
var (
|
|
||||||
query = "" +
|
|
||||||
"SELECT id, pubkey, msats, preimage, hash, bolt11, created_at, expires_at, confirmed_at, held_since, COALESCE(description, ''), " +
|
|
||||||
"CASE WHEN confirmed_at IS NOT NULL THEN 'PAID' WHEN expires_at < CURRENT_TIMESTAMP THEN 'EXPIRED' ELSE 'PENDING' END AS status " +
|
|
||||||
"FROM invoices " +
|
|
||||||
"WHERE pubkey = $1 " +
|
|
||||||
"ORDER BY created_at DESC"
|
|
||||||
)
|
|
||||||
if rows, err = db.Query(query, pubkey); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
rows.Scan(
|
|
||||||
&invoice.Id, &invoice.Pubkey, &invoice.Msats, &invoice.Preimage, &invoice.Hash,
|
|
||||||
&invoice.PaymentRequest, &invoice.CreatedAt, &invoice.ExpiresAt, &invoice.ConfirmedAt, &invoice.HeldSince, &invoice.Description, &invoice.Status)
|
|
||||||
*invoices = append(*invoices, invoice)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) ConfirmInvoice(tx *sql.Tx, c context.Context, hash string, confirmedAt time.Time, msatsReceived int) error {
|
|
||||||
if _, err := tx.ExecContext(c, "UPDATE invoices SET confirmed_at = $2, msats_received = $3 WHERE hash = $1", hash, confirmedAt, msatsReceived); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
18
db/lnauth.go
18
db/lnauth.go
@ -1,18 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
func (db *DB) CreateLNAuth(lnAuth *LNAuth) error {
|
|
||||||
err := db.QueryRow(
|
|
||||||
"INSERT INTO lnauth(k1, lnurl) VALUES($1, $2) RETURNING session_id",
|
|
||||||
lnAuth.K1, lnAuth.LNURL).Scan(&lnAuth.SessionId)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) FetchSessionId(k1 string, sessionId *string) error {
|
|
||||||
err := db.QueryRow("SELECT session_id FROM lnauth WHERE k1 = $1", k1).Scan(sessionId)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) DeleteLNAuth(lnAuth *LNAuth) error {
|
|
||||||
_, err := db.Exec("DELETE FROM lnauth WHERE k1 = $1", lnAuth.K1)
|
|
||||||
return err
|
|
||||||
}
|
|
353
db/market.go
353
db/market.go
@ -1,353 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FetchOrdersWhere struct {
|
|
||||||
MarketId int
|
|
||||||
Pubkey string
|
|
||||||
Confirmed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) CreateMarket(tx *sql.Tx, ctx context.Context, market *Market) error {
|
|
||||||
if err := tx.QueryRowContext(ctx, ""+
|
|
||||||
"INSERT INTO markets(description, end_date, pubkey, invoice_id) "+
|
|
||||||
"VALUES($1, $2, $3, $4) "+
|
|
||||||
"RETURNING id", market.Description, market.EndDate, market.Pubkey, market.InvoiceId).Scan(&market.Id); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// For now, we only support binary markets.
|
|
||||||
if _, err := tx.Exec("INSERT INTO shares(market_id, description) VALUES ($1, 'YES'), ($1, 'NO')", market.Id); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) FetchMarket(marketId int, market *Market) error {
|
|
||||||
if err := db.QueryRow("SELECT id, description, end_date, pubkey, settled_at FROM markets WHERE id = $1", marketId).Scan(&market.Id, &market.Description, &market.EndDate, &market.Pubkey, &market.SettledAt); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) FetchActiveMarkets(markets *[]Market) error {
|
|
||||||
var (
|
|
||||||
rows *sql.Rows
|
|
||||||
market Market
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if rows, err = db.Query("" +
|
|
||||||
"SELECT m.id, m.description, m.end_date FROM markets m " +
|
|
||||||
"JOIN invoices i ON i.id = m.invoice_id WHERE i.confirmed_at IS NOT NULL"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
rows.Scan(&market.Id, &market.Description, &market.EndDate)
|
|
||||||
*markets = append(*markets, market)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) FetchShares(marketId int, shares *[]Share) error {
|
|
||||||
rows, err := db.Query("SELECT id, market_id, description, win FROM shares WHERE market_id = $1 ORDER BY description DESC", marketId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
var share Share
|
|
||||||
rows.Scan(&share.Id, &share.MarketId, &share.Description, &share.Win)
|
|
||||||
*shares = append(*shares, share)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) FetchShare(tx *sql.Tx, ctx context.Context, shareId string, share *Share) error {
|
|
||||||
return tx.QueryRowContext(ctx, "SELECT id, market_id, description FROM shares WHERE id = $1", shareId).Scan(&share.Id, &share.MarketId, &share.Description)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) FetchOrders(where *FetchOrdersWhere, orders *[]Order) error {
|
|
||||||
query := "" +
|
|
||||||
"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 " +
|
|
||||||
"JOIN invoices i ON o.invoice_id = i.id " +
|
|
||||||
"JOIN shares s ON o.share_id = s.id " +
|
|
||||||
"WHERE o.deleted_at IS NULL "
|
|
||||||
var args []any
|
|
||||||
if where.MarketId > 0 {
|
|
||||||
query += "AND share_id = ANY(SELECT id FROM shares WHERE market_id = $1) "
|
|
||||||
args = append(args, where.MarketId)
|
|
||||||
} else if where.Pubkey != "" {
|
|
||||||
query += "AND o.pubkey = $1 "
|
|
||||||
args = append(args, where.Pubkey)
|
|
||||||
}
|
|
||||||
if where.Confirmed {
|
|
||||||
query += "AND o.order_id IS NOT NULL "
|
|
||||||
}
|
|
||||||
query += "AND (i.confirmed_at IS NOT NULL OR i.expires_at > CURRENT_TIMESTAMP) "
|
|
||||||
query += "ORDER BY price DESC"
|
|
||||||
rows, err := db.Query(query, args...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
var order Order
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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, CASE WHEN $6 = '' THEN NULL ELSE $6::UUID END)",
|
|
||||||
order.ShareId, order.Pubkey, order.Side, order.Quantity, order.Price, order.InvoiceId.String); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) FetchOrder(tx *sql.Tx, ctx context.Context, orderId string, order *Order) error {
|
|
||||||
query := "" +
|
|
||||||
"SELECT o.id, o.share_id, o.pubkey, o.side, o.quantity, o.price, o.created_at, o.deleted_at, o.order_id, s.description, s.market_id, i.confirmed_at, o.invoice_id, COALESCE(i.msats_received, 0) " +
|
|
||||||
"FROM orders o " +
|
|
||||||
"LEFT JOIN invoices i ON o.invoice_id = i.id " +
|
|
||||||
"JOIN shares s ON o.share_id = s.id " +
|
|
||||||
"WHERE o.id = $1"
|
|
||||||
return tx.QueryRowContext(ctx, query, orderId).Scan(&order.Id, &order.ShareId, &order.Pubkey, &order.Side, &order.Quantity, &order.Price, &order.CreatedAt, &order.DeletedAt, &order.OrderId, &order.Share.Description, &order.MarketId, &order.Invoice.ConfirmedAt, &order.InvoiceId, &order.Invoice.MsatsReceived)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) FetchUserOrders(pubkey string, orders *[]Order) error {
|
|
||||||
query := "" +
|
|
||||||
"SELECT o.id, share_id, o.pubkey, o.side, o.quantity, o.price, o.created_at, o.deleted_at, s.description, s.market_id, i.confirmed_at, " +
|
|
||||||
"CASE WHEN o.order_id IS NOT NULL THEN 'EXECUTED' WHEN o.deleted_at IS NOT NULL THEN 'CANCELED' ELSE 'PENDING' END AS status, o.order_id, o.invoice_id " +
|
|
||||||
"FROM orders o " +
|
|
||||||
"LEFT JOIN invoices i ON o.invoice_id = i.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' ) " +
|
|
||||||
"ORDER BY o.created_at DESC"
|
|
||||||
rows, err := db.Query(query, pubkey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
var order Order
|
|
||||||
rows.Scan(&order.Id, &order.ShareId, &order.Pubkey, &order.Side, &order.Quantity, &order.Price, &order.CreatedAt, &order.DeletedAt, &order.ShareDescription, &order.Share.MarketId, &order.Invoice.ConfirmedAt, &order.Status, &order.OrderId, &order.InvoiceId)
|
|
||||||
*orders = append(*orders, order)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) FetchMarketOrders(marketId int64, orders *[]Order) error {
|
|
||||||
query := "" +
|
|
||||||
"SELECT o.id, share_id, o.pubkey, o.side, o.quantity, o.price, o.created_at, s.description, s.market_id, " +
|
|
||||||
"CASE WHEN o.order_id IS NOT NULL THEN 'EXECUTED' ELSE 'PENDING' END AS status, o.order_id, o.invoice_id " +
|
|
||||||
"FROM orders o " +
|
|
||||||
"JOIN shares s ON o.share_id = s.id " +
|
|
||||||
"LEFT JOIN invoices i ON o.invoice_id = i.id " +
|
|
||||||
"WHERE s.market_id = $1 AND o.deleted_at IS NULL 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 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
var order Order
|
|
||||||
rows.Scan(&order.Id, &order.ShareId, &order.Pubkey, &order.Side, &order.Quantity, &order.Price, &order.CreatedAt, &order.ShareDescription, &order.Share.MarketId, &order.Status, &order.OrderId, &order.InvoiceId)
|
|
||||||
*orders = append(*orders, order)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) RunMatchmaking(orderId string) {
|
|
||||||
var (
|
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
tx *sql.Tx
|
|
||||||
o1 Order
|
|
||||||
o2 Order
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
ctx, cancel = context.WithTimeout(context.TODO(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
if tx, err = db.BeginTx(ctx, nil); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// TODO: assert that order was confirmed
|
|
||||||
if err = db.FetchOrder(tx, ctx, orderId, &o1); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
tx.Rollback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = db.FindOrderMatches(tx, ctx, &o1, &o2); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
tx.Rollback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if o2.OrderId.Valid {
|
|
||||||
log.Printf("assertion failed: order %s matched order %s but order_id already set to %s\n", o1.Id, o2.Id, o2.OrderId.String)
|
|
||||||
tx.Rollback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if o1.Id == o2.Id {
|
|
||||||
log.Printf("assertion failed: order %s matched itself", o1.Id)
|
|
||||||
tx.Rollback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = db.MatchOrders(tx, ctx, &o1, &o2); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
tx.Rollback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) FindOrderMatches(tx *sql.Tx, ctx context.Context, o1 *Order, o2 *Order) error {
|
|
||||||
query := "" +
|
|
||||||
"SELECT o.id, o.order_id, o.side, o.quantity, o.price, o.pubkey FROM orders o " +
|
|
||||||
"JOIN shares s ON s.id = o.share_id " +
|
|
||||||
"LEFT JOIN invoices i ON i.id = o.invoice_id " +
|
|
||||||
// only match orders which are not soft deleted
|
|
||||||
"WHERE o.deleted_at IS NULL " +
|
|
||||||
// only match orders which are not already settled
|
|
||||||
"AND o.order_id IS NULL " +
|
|
||||||
// only match orders from other users
|
|
||||||
"AND o.pubkey <> $1 " +
|
|
||||||
// orders must always be for same market and have same quantity
|
|
||||||
"AND o.quantity = $2 AND s.market_id = $3 " +
|
|
||||||
// BUY orders must have been confirmed by paying the invoice
|
|
||||||
"AND CASE WHEN o.side = 'BUY' THEN i.confirmed_at IS NOT NULL ELSE 1=1 END " +
|
|
||||||
"AND (" +
|
|
||||||
// -- BUY orders match if they are for different shares and the sum of their prices equal 100
|
|
||||||
// -- example: BUY 5 YES @ 60 <> BUY 5 NO @ 40
|
|
||||||
" ( $5 = 'BUY' AND o.side = 'BUY' AND o.price = (100-$6) AND o.share_id <> $4 ) " +
|
|
||||||
// -- BUY orders match SELL orders if they are for the same share and have same price
|
|
||||||
// -- example: BUY 5 YES @ 60 <> SELL 5 YES @ 60
|
|
||||||
" OR ( $5 = 'BUY' AND o.side = 'SELL' AND o.price = $6 AND o.share_id = $4 ) " +
|
|
||||||
// -- SELL orders match BUY orders if they are for the same share and have same price
|
|
||||||
// -- example: SELL 5 YES @ 60 <> BUY 5 YES @ 60
|
|
||||||
" OR ( $5 = 'SELL' AND o.side = 'BUY' AND o.price = $6 AND o.share_id = $4 ) " +
|
|
||||||
") " +
|
|
||||||
// match oldest order first
|
|
||||||
"ORDER BY o.created_at ASC LIMIT 1"
|
|
||||||
return tx.QueryRowContext(ctx, query, o1.Pubkey, o1.Quantity, o1.Share.MarketId, o1.ShareId, o1.Side, o1.Price).Scan(&o2.Id, &o2.OrderId, &o2.Side, &o2.Quantity, &o2.Price, &o2.Pubkey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) MatchOrders(tx *sql.Tx, ctx context.Context, o1 *Order, o2 *Order) error {
|
|
||||||
var err error
|
|
||||||
if _, err = tx.ExecContext(ctx, "UPDATE orders SET order_id = $1 WHERE id = $2", o1.Id, o2.Id); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err = tx.ExecContext(ctx, "UPDATE orders SET order_id = $1 WHERE id = $2", o2.Id, o1.Id); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if o1.Side == "SELL" {
|
|
||||||
if _, err = tx.ExecContext(ctx, "UPDATE users SET msats = msats + $1 WHERE pubkey = $2", (o1.Price*o1.Quantity)*1000, o1.Pubkey); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if o2.Side == "SELL" {
|
|
||||||
if _, err = tx.ExecContext(ctx, "UPDATE users SET msats = msats + $1 WHERE pubkey = $2", (o2.Price*o2.Quantity)*1000, o2.Pubkey); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Printf("Matched orders: %s <> %s\n", o1.Id, o2.Id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// [
|
|
||||||
//
|
|
||||||
// { "x": <timestamp>, "y": { <share_description>: <score>, ... } },
|
|
||||||
//
|
|
||||||
// ]
|
|
||||||
type MarketStat struct {
|
|
||||||
X time.Time `json:"x"`
|
|
||||||
Y map[string]int `json:"y"`
|
|
||||||
}
|
|
||||||
type MarketStats = []MarketStat
|
|
||||||
|
|
||||||
func (db *DB) FetchMarketStats(marketId int64, stats *MarketStats) error {
|
|
||||||
query := "" +
|
|
||||||
"SELECT " +
|
|
||||||
"s.description, " +
|
|
||||||
"GREATEST(i.confirmed_at, i2.confirmed_at) AS confirmed_at, " +
|
|
||||||
"SUM(o.price * o.quantity) OVER (PARTITION BY o.share_id ORDER BY o.created_at ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS score " +
|
|
||||||
"FROM orders o " +
|
|
||||||
"JOIN orders o2 ON o2.id = o.order_id " +
|
|
||||||
"JOIN shares s ON s.id = o.share_id " +
|
|
||||||
"JOIN invoices i ON i.id = o.invoice_id " +
|
|
||||||
"JOIN invoices i2 ON i2.id = o2.invoice_id " +
|
|
||||||
"WHERE s.market_id = $1 AND i.confirmed_at IS NOT NULL AND o.order_id IS NOT NULL ORDER BY i.confirmed_at ASC"
|
|
||||||
rows, err := db.Query(query, marketId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
var stat MarketStat
|
|
||||||
var (
|
|
||||||
timestamp time.Time
|
|
||||||
description string
|
|
||||||
score int
|
|
||||||
)
|
|
||||||
rows.Scan(&description, ×tamp, &score)
|
|
||||||
stat.X = timestamp
|
|
||||||
stat.Y = map[string]int{
|
|
||||||
description: score,
|
|
||||||
}
|
|
||||||
*stats = append(*stats, stat)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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 " +
|
|
||||||
"LEFT 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 " +
|
|
||||||
// ignore canceled orders
|
|
||||||
"AND o.deleted_at IS NULL " +
|
|
||||||
"AND ( " +
|
|
||||||
// shares from BUY orders are received if they were paid (necessary precondition for matchmaking) and found a matching order
|
|
||||||
" (o.side = 'BUY' AND i.confirmed_at IS NOT NULL AND o.order_id IS NOT NULL) " +
|
|
||||||
// shares from SELL orders are deducted immediately to prevent double-spends
|
|
||||||
" OR o.side = 'SELL' " +
|
|
||||||
") " +
|
|
||||||
"GROUP BY o.pubkey, s.description"
|
|
||||||
rows, err := tx.QueryContext(ctx, query, pubkey, marketId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
var (
|
|
||||||
sdesc string
|
|
||||||
val int
|
|
||||||
)
|
|
||||||
if err = rows.Scan(&sdesc, &val); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
(*balance)[sdesc] = val
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
func (db *DB) CreateSession(s *Session) error {
|
|
||||||
_, err := db.Exec("INSERT INTO sessions(pubkey, session_id) VALUES($1, $2)", s.Pubkey, s.SessionId)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) FetchSession(s *Session) error {
|
|
||||||
query := "SELECT u.pubkey, u.msats FROM sessions s LEFT JOIN users u ON u.pubkey = s.pubkey WHERE session_id = $1"
|
|
||||||
err := db.QueryRow(query, s.SessionId).Scan(&s.Pubkey, &s.Msats)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) DeleteSession(s *Session) error {
|
|
||||||
_, err := db.Exec("DELETE FROM sessions where session_id = $1", s.SessionId)
|
|
||||||
return err
|
|
||||||
}
|
|
80
db/types.go
80
db/types.go
@ -1,80 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gopkg.in/guregu/null.v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
Serial = int
|
|
||||||
UUID = string
|
|
||||||
LNAuth struct {
|
|
||||||
K1 string
|
|
||||||
LNURL string
|
|
||||||
CreatedAt time.Time
|
|
||||||
SessionId string
|
|
||||||
}
|
|
||||||
User struct {
|
|
||||||
Pubkey string
|
|
||||||
Msats int64
|
|
||||||
LastSeen time.Time
|
|
||||||
}
|
|
||||||
Session struct {
|
|
||||||
Pubkey string
|
|
||||||
Msats int64
|
|
||||||
SessionId string
|
|
||||||
}
|
|
||||||
Market struct {
|
|
||||||
Id Serial `json:"id"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
EndDate time.Time `json:"endDate"`
|
|
||||||
SettledAt null.Time `json:"settledAt"`
|
|
||||||
Pubkey string `json:"pubkey"`
|
|
||||||
InvoiceId UUID
|
|
||||||
}
|
|
||||||
Share struct {
|
|
||||||
Id UUID `json:"sid"`
|
|
||||||
MarketId int
|
|
||||||
Description string
|
|
||||||
Win bool
|
|
||||||
}
|
|
||||||
Invoice struct {
|
|
||||||
Id UUID
|
|
||||||
Pubkey string
|
|
||||||
Msats int64
|
|
||||||
MsatsReceived int64
|
|
||||||
Preimage string
|
|
||||||
Hash string
|
|
||||||
PaymentRequest string
|
|
||||||
CreatedAt time.Time
|
|
||||||
ExpiresAt time.Time
|
|
||||||
ConfirmedAt null.Time
|
|
||||||
HeldSince null.Time
|
|
||||||
Description string
|
|
||||||
Status string
|
|
||||||
}
|
|
||||||
Order struct {
|
|
||||||
Id UUID
|
|
||||||
CreatedAt time.Time
|
|
||||||
DeletedAt null.Time
|
|
||||||
ShareId string `json:"sid"`
|
|
||||||
ShareDescription string
|
|
||||||
Share
|
|
||||||
Pubkey string
|
|
||||||
Side string `json:"side"`
|
|
||||||
Quantity int64 `json:"quantity"`
|
|
||||||
Price int64 `json:"price"`
|
|
||||||
InvoiceId null.String
|
|
||||||
Invoice
|
|
||||||
OrderId null.String
|
|
||||||
}
|
|
||||||
Withdrawal struct {
|
|
||||||
Id UUID
|
|
||||||
CreatedAt time.Time
|
|
||||||
DeletedAt null.Time
|
|
||||||
Pubkey string
|
|
||||||
Bolt11 string `json:"bolt11"`
|
|
||||||
PaidAt null.Time
|
|
||||||
}
|
|
||||||
)
|
|
17
db/user.go
17
db/user.go
@ -1,17 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
func (db *DB) CreateUser(u *User) error {
|
|
||||||
_, err := db.Exec(
|
|
||||||
"INSERT INTO users(pubkey) VALUES ($1) ON CONFLICT(pubkey) DO UPDATE SET last_seen = CURRENT_TIMESTAMP",
|
|
||||||
u.Pubkey)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) FetchUser(pubkey string, u *User) error {
|
|
||||||
return db.QueryRow("SELECT pubkey, last_seen FROM users WHERE pubkey = $1", pubkey).Scan(&u.Pubkey, &u.LastSeen)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) UpdateUser(u *User) error {
|
|
||||||
_, err := db.Exec("UPDATE users SET last_seen = $1 WHERE pubkey = $2", u.LastSeen, u.Pubkey)
|
|
||||||
return err
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (db *DB) CreateWithdrawal(tx *sql.Tx, c context.Context, w *Withdrawal) error {
|
|
||||||
if _, err := tx.ExecContext(c, "INSERT INTO withdrawals(pubkey, bolt11) VALUES ($1, $2)", w.Pubkey, w.Bolt11); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -31,6 +31,6 @@ trap cleanup EXIT
|
|||||||
restart
|
restart
|
||||||
tail -f server.log &
|
tail -f server.log &
|
||||||
|
|
||||||
while inotifywait -r -e modify db/ env/ lib/ lnd/ pages/ public/ server/; do
|
while inotifywait -r -e modify db/ env/ lib/ lnd/ public/ server/; do
|
||||||
restart
|
restart
|
||||||
done
|
done
|
||||||
|
163
lnd/invoice.go
163
lnd/invoice.go
@ -1,163 +0,0 @@
|
|||||||
package lnd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
|
||||||
"github.com/lightninglabs/lndclient"
|
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (lnd *LNDClient) CreateInvoice(tx *sql.Tx, ctx context.Context, d *db.DB, pubkey string, msats int64, description string) (*db.Invoice, error) {
|
|
||||||
var (
|
|
||||||
expiry time.Duration = time.Hour
|
|
||||||
preimage lntypes.Preimage
|
|
||||||
hash lntypes.Hash
|
|
||||||
paymentRequest string
|
|
||||||
lnInvoice *lndclient.Invoice
|
|
||||||
dbInvoice *db.Invoice
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if preimage, err = generateNewPreimage(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
hash = preimage.Hash()
|
|
||||||
if paymentRequest, err = lnd.Invoices.AddHoldInvoice(ctx, &invoicesrpc.AddInvoiceData{
|
|
||||||
Hash: &hash,
|
|
||||||
Value: lnwire.MilliSatoshi(msats),
|
|
||||||
Expiry: int64(expiry / time.Millisecond),
|
|
||||||
}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if lnInvoice, err = lnd.Client.LookupInvoice(ctx, hash); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
dbInvoice = &db.Invoice{
|
|
||||||
Pubkey: pubkey,
|
|
||||||
Msats: msats,
|
|
||||||
Preimage: preimage.String(),
|
|
||||||
PaymentRequest: paymentRequest,
|
|
||||||
Hash: hash.String(),
|
|
||||||
CreatedAt: lnInvoice.CreationDate,
|
|
||||||
ExpiresAt: lnInvoice.CreationDate.Add(expiry),
|
|
||||||
Description: description,
|
|
||||||
}
|
|
||||||
if err := d.CreateInvoice(tx, ctx, dbInvoice); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return dbInvoice, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lnd *LNDClient) CheckInvoice(d *db.DB, hash lntypes.Hash) {
|
|
||||||
var (
|
|
||||||
pollInterval = 5 * time.Second
|
|
||||||
invoice db.Invoice
|
|
||||||
lnInvoice *lndclient.Invoice
|
|
||||||
preimage lntypes.Preimage
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if err = d.FetchInvoice(&db.FetchInvoiceWhere{Hash: hash.String()}, &invoice); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
|
|
||||||
var tx *sql.Tx
|
|
||||||
if tx, err = d.BeginTx(ctx, nil); err != nil {
|
|
||||||
cancel()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLoopError := func(err error) {
|
|
||||||
log.Println(err)
|
|
||||||
tx.Rollback()
|
|
||||||
cancel()
|
|
||||||
time.Sleep(pollInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("lookup invoice: hash=%s", hash)
|
|
||||||
if lnInvoice, err = lnd.Client.LookupInvoice(ctx, hash); err != nil {
|
|
||||||
handleLoopError(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if time.Now().After(invoice.ExpiresAt) {
|
|
||||||
// cancel invoices after expiration if no matching order found yet
|
|
||||||
if err = lnd.Invoices.CancelInvoice(ctx, hash); err != nil {
|
|
||||||
handleLoopError(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Printf("invoice expired: hash=%s", hash)
|
|
||||||
tx.Commit()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if lnInvoice.AmountPaid == lnInvoice.Amount {
|
|
||||||
if preimage, err = lntypes.MakePreimageFromStr(invoice.Preimage); err != nil {
|
|
||||||
handleLoopError(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// TODO settle invoice after matching order was found
|
|
||||||
if err = lnd.Invoices.SettleInvoice(ctx, preimage); err != nil {
|
|
||||||
handleLoopError(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err = d.ConfirmInvoice(tx, ctx, hash.String(), time.Now(), int(lnInvoice.AmountPaid)); err != nil {
|
|
||||||
handleLoopError(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Printf("invoice confirmed: hash=%s", hash)
|
|
||||||
|
|
||||||
// Run matchmaking if an order was paid
|
|
||||||
var orderId string
|
|
||||||
var deleted bool
|
|
||||||
if err = d.QueryRowContext(ctx,
|
|
||||||
"SELECT o.id, o.deleted_at IS NOT NULL FROM orders o WHERE invoice_id = (SELECT i.id FROM invoices i WHERE hash = $1)",
|
|
||||||
hash.String(),
|
|
||||||
).Scan(&orderId, &deleted); err != nil && err != sql.ErrNoRows {
|
|
||||||
handleLoopError(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if deleted {
|
|
||||||
// order was canceled before it was paid. refund sats immediately.
|
|
||||||
// this can happen if the market was settled between creating the order and paying the corresponding invoice.
|
|
||||||
if _, err := tx.ExecContext(ctx, "UPDATE users SET msats = msats + $1", int64(lnInvoice.AmountPaid)); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
log.Printf("order %s canceled. refunded sats to user.", orderId)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if orderId != "" {
|
|
||||||
go d.RunMatchmaking(orderId)
|
|
||||||
}
|
|
||||||
tx.Commit()
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(pollInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lnd *LNDClient) CheckInvoices(d *db.DB) error {
|
|
||||||
var (
|
|
||||||
invoices []db.Invoice
|
|
||||||
err error
|
|
||||||
hash lntypes.Hash
|
|
||||||
)
|
|
||||||
if err = d.FetchInvoices(&db.FetchInvoicesWhere{Unconfirmed: true}, &invoices); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, invoice := range invoices {
|
|
||||||
if hash, err = lntypes.MakeHashFromStr(invoice.Hash); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
go lnd.CheckInvoice(d, hash)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
package lnd
|
|
@ -10,21 +10,31 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (lnd *LNDClient) PayInvoice(tx *sql.Tx, bolt11 string) error {
|
func (lnd *LNDClient) PayInvoice(tx *sql.Tx, bolt11 string) error {
|
||||||
maxFeeSats := btcutil.Amount(10)
|
|
||||||
ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
log.Printf("attempting to pay bolt11 %s ...\n", bolt11)
|
log.Printf("attempting to pay bolt11 %s ...\n", bolt11)
|
||||||
|
|
||||||
|
maxFeeSats := btcutil.Amount(10)
|
||||||
payChan := lnd.Client.PayInvoice(ctx, bolt11, maxFeeSats, nil)
|
payChan := lnd.Client.PayInvoice(ctx, bolt11, maxFeeSats, nil)
|
||||||
|
|
||||||
res := <-payChan
|
res := <-payChan
|
||||||
|
|
||||||
if res.Err != nil {
|
if res.Err != nil {
|
||||||
log.Printf("error paying bolt11: %s -- %s\n", bolt11, res.Err)
|
log.Printf("error paying bolt11: %s -- %s\n", bolt11, res.Err)
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return res.Err
|
return res.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("successfully paid bolt11: %s\n", bolt11)
|
log.Printf("successfully paid bolt11: %s\n", bolt11)
|
||||||
if _, err := tx.ExecContext(ctx, "UPDATE withdrawals SET paid_at = CURRENT_TIMESTAMP WHERE bolt11 = $1", bolt11); err != nil {
|
|
||||||
|
if _, err := tx.ExecContext(ctx,
|
||||||
|
"UPDATE withdrawals SET paid_at = CURRENT_TIMESTAMP WHERE bolt11 = $1",
|
||||||
|
bolt11,
|
||||||
|
); err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.Err
|
return res.Err
|
||||||
}
|
}
|
||||||
|
2
main.go
2
main.go
@ -61,7 +61,7 @@ func init() {
|
|||||||
log.Printf("[warn] error connecting to LND: %v\n", err)
|
log.Printf("[warn] error connecting to LND: %v\n", err)
|
||||||
lnd_ = nil
|
lnd_ = nil
|
||||||
} else {
|
} else {
|
||||||
lnd_.CheckInvoices(db_)
|
// lnd_.CheckInvoices(db_)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = server.Context{
|
ctx = server.Context{
|
||||||
|
@ -1,54 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>delphi.market</title>
|
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
|
||||||
<link rel="stylesheet" href="/css/index.css" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#091833" />
|
|
||||||
{{ if eq .ENV "development" }}
|
|
||||||
<script defer src="/hotreload.js"></script>
|
|
||||||
{{ end }}
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<header class="flex flex-row text-center justify-center pt-1">
|
|
||||||
<nav>
|
|
||||||
<a href="/">home</a>
|
|
||||||
{{ if .session }}
|
|
||||||
<a href='/user'>user</a>
|
|
||||||
{{ else }} <a href="/login">login</a> {{ end }}
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<div class="container flex flex-column text-center">
|
|
||||||
<code>
|
|
||||||
<strong>
|
|
||||||
<pre>
|
|
||||||
_ _ _ _
|
|
||||||
__| | ___| |_ __ | |__ (_)
|
|
||||||
/ _` |/ _ \ | '_ \| '_ \| |
|
|
||||||
| (_| | __/ | |_) | | | | |
|
|
||||||
\__,_|\___|_| .__/|_| |_|_|
|
|
||||||
|_| </pre>
|
|
||||||
</strong>
|
|
||||||
</code>
|
|
||||||
<div class="font-mono mb-1">A prediction market using the lightning network [WIP]</div>
|
|
||||||
<div class="font-mono mb-1">ACTIVE MARKETS</div>
|
|
||||||
{{ range .markets }}
|
|
||||||
<a href="/market/{{.Id}}">{{.Description}}</a>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
<footer class="flex justify-center">
|
|
||||||
<div>
|
|
||||||
<hr />
|
|
||||||
<code><a href="https://github.com/ekzyis/delphi.market/commit/{{.COMMIT_LONG_SHA}}">{{.VERSION}}</a></code>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,113 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>delphi.market</title>
|
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
|
||||||
<link rel="stylesheet" href="/css/index.css" />
|
|
||||||
<link rel="stylesheet" href="/market.css" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#091833" />
|
|
||||||
{{ if eq .ENV "development" }}
|
|
||||||
<script defer src="/hotreload.js"></script>
|
|
||||||
{{ end }}
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<header class="flex flex-row text-center justify-center pt-1">
|
|
||||||
<nav>
|
|
||||||
<a href="/">home</a>
|
|
||||||
{{ if .session }}
|
|
||||||
<a href='/user'>user</a>
|
|
||||||
{{ else }} <a href="/login">login</a> {{ end }}
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<div class="container flex flex-column text-center justify-center">
|
|
||||||
<code>
|
|
||||||
<strong>
|
|
||||||
<pre>
|
|
||||||
_ _ ___ ____
|
|
||||||
| || | / _ \___ \
|
|
||||||
| || |_| | | |__) |
|
|
||||||
|__ _| |_| / __/
|
|
||||||
|_| \___/_____|</pre>
|
|
||||||
</strong>
|
|
||||||
</code>
|
|
||||||
<div class="font-mono mb-1">
|
|
||||||
<div class="mb-1">Payment Required</div>
|
|
||||||
<div id="status" hidden>
|
|
||||||
<div id="status-label"></div>
|
|
||||||
<div id="countdown" hidden>Redirecting in 3 ...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="qr">
|
|
||||||
<a href="lightning:{{.lnurl}}">
|
|
||||||
<img class="m-auto mb-1" src="data:image/png;base64,{{.qr}}" width="50%" />
|
|
||||||
</a>
|
|
||||||
<div class="font-mono word-wrap mb-1">{{.lnurl}}</div>
|
|
||||||
<details class="font-mono mb-1 align-left">
|
|
||||||
<summary>details</summary>
|
|
||||||
<div>id: {{.invoice.Id}}</div>
|
|
||||||
<div>amount: {{div .invoice.Msats 1000}} sats</div>
|
|
||||||
<div>created: {{.invoice.CreatedAt}}</div>
|
|
||||||
<div>expiry : {{.invoice.ExpiresAt}}</div>
|
|
||||||
<div class="word-wrap">hash: {{.invoice.Hash}}</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
<script>
|
|
||||||
const statusElement = document.querySelector("#status")
|
|
||||||
const label = document.querySelector("#status-label")
|
|
||||||
const status = "{{.status}}"
|
|
||||||
const redirectUrl = "{{.redirectURL}}"
|
|
||||||
function poll() {
|
|
||||||
const invoiceId = "{{.invoice.Id}}"
|
|
||||||
const countdown = document.querySelector("#countdown")
|
|
||||||
const redirect = () => {
|
|
||||||
clearInterval(interval)
|
|
||||||
countdown.removeAttribute("hidden")
|
|
||||||
let timer = 2
|
|
||||||
const redirect = setInterval(() => {
|
|
||||||
countdown.textContent = `Redirecting in ${timer--} ...`
|
|
||||||
if (timer === -1) {
|
|
||||||
window.location.href = redirectUrl;
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
const interval = setInterval(async () => {
|
|
||||||
const body = await fetch(`/api/invoice/${invoiceId}`)
|
|
||||||
.then((r) => r.json())
|
|
||||||
.catch(console.error)
|
|
||||||
if (body.ConfirmedAt) {
|
|
||||||
statusElement.removeAttribute("hidden")
|
|
||||||
statusElement.classList.add("yes")
|
|
||||||
label.textContent = "Paid"
|
|
||||||
if (redirectUrl) redirect()
|
|
||||||
} else if (new Date(body.ExpiresAt) <= new Date()) {
|
|
||||||
statusElement.removeAttribute("hidden")
|
|
||||||
statusElement.classList.add("no")
|
|
||||||
label.textContent = "Expired"
|
|
||||||
if (redirectUrl) redirect()
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
if (status) {
|
|
||||||
console.log(status)
|
|
||||||
statusElement.removeAttribute("hidden")
|
|
||||||
label.textContent = status
|
|
||||||
if (status === "Paid") {
|
|
||||||
statusElement.classList.add("yes")
|
|
||||||
}
|
|
||||||
else if (status === "Expired") {
|
|
||||||
statusElement.classList.add("no")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else poll()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,77 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>delphi.market</title>
|
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
|
||||||
<link rel="stylesheet" href="/css/index.css" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#091833" />
|
|
||||||
{{ if eq .ENV "development" }}
|
|
||||||
<script defer src="/hotreload.js"></script>
|
|
||||||
{{ end }}
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<header class="flex flex-row text-center justify-center pt-1">
|
|
||||||
<nav>
|
|
||||||
<a href="/">home</a>
|
|
||||||
{{ if .session }}
|
|
||||||
<a href='/user'>user</a>
|
|
||||||
{{ else }} <a href="/login">login</a> {{ end }}
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<div class="container flex flex-column text-center justify-center">
|
|
||||||
<code>
|
|
||||||
<strong>
|
|
||||||
<pre>
|
|
||||||
_ _
|
|
||||||
| | ___ __ _(_)_ __
|
|
||||||
| |/ _ \ / _` | | '_ \
|
|
||||||
| | (_) | (_| | | | | |
|
|
||||||
|_|\___/ \__, |_|_| |_|
|
|
||||||
|___/ </pre>
|
|
||||||
</strong>
|
|
||||||
</code>
|
|
||||||
<div id="qr">
|
|
||||||
<div class="mb-1">Login with Lightning</div>
|
|
||||||
<a href="lightning:{{.lnurl}}"><img class="m-auto mb-1" src="data:image/png;base64,{{.qr}}" width="100%" /></a>
|
|
||||||
<div class="font-mono word-wrap">{{.lnurl}}</div>
|
|
||||||
</div>
|
|
||||||
<div id="lnauth-success" hidden>
|
|
||||||
<div>Login successful</div>
|
|
||||||
<div>You are <span id="lnauth-pubkey" class="font-mono"></span></div>
|
|
||||||
<div id="lnauth-countdown">Redirecting in 3 ...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
<script>
|
|
||||||
const qr = document.querySelector("#qr")
|
|
||||||
const success = document.querySelector("#lnauth-success")
|
|
||||||
const pubkey = document.querySelector("#lnauth-pubkey")
|
|
||||||
const countdown = document.querySelector("#lnauth-countdown")
|
|
||||||
const interval = setInterval(async () => {
|
|
||||||
const body = await fetch(`/api/session`)
|
|
||||||
.then((r) => r.json())
|
|
||||||
.catch(console.error)
|
|
||||||
if (body.pubkey) {
|
|
||||||
qr.setAttribute("hidden", true)
|
|
||||||
pubkey.textContent = body.pubkey.slice(0, 10)
|
|
||||||
success.removeAttribute("hidden")
|
|
||||||
clearInterval(interval)
|
|
||||||
let timer = 2
|
|
||||||
const redirect = setInterval(() => {
|
|
||||||
countdown.textContent = `Redirecting in ${timer--} ...`
|
|
||||||
if (timer === -1) {
|
|
||||||
window.location.href = "https://{{.PUBLIC_URL}}/";
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,82 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>delphi.market</title>
|
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
|
||||||
<link rel="stylesheet" href="/css/index.css" />
|
|
||||||
<link rel="stylesheet" href="/market.css" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#091833" />
|
|
||||||
{{ if eq .ENV "development" }}
|
|
||||||
<script defer src="/hotreload.js"></script>
|
|
||||||
{{ end }}
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<header class="flex flex-row text-center justify-center pt-1">
|
|
||||||
<nav>
|
|
||||||
<a href="/">home</a>
|
|
||||||
{{ if .session }}
|
|
||||||
<a href='/user'>user</a>
|
|
||||||
{{ else }} <a href="/login">login</a> {{ end }}
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<div class="container flex flex-column text-center">
|
|
||||||
<code>
|
|
||||||
<strong>
|
|
||||||
<pre>
|
|
||||||
_ _
|
|
||||||
_ __ ___ __ _ _ __| | _____| |_
|
|
||||||
| '_ ` _ \ / _` | '__| |/ / _ \ __|
|
|
||||||
| | | | | | (_| | | | < __/ |_
|
|
||||||
|_| |_| |_|\__,_|_| |_|\_\___|\__|</pre>
|
|
||||||
</strong>
|
|
||||||
</code>
|
|
||||||
<div class="font-mono mb-1">{{.Description}}</div>
|
|
||||||
<div class="align-left">
|
|
||||||
<span class="font-mono mb-1"><strong>Order Book</strong></span>
|
|
||||||
<table class="w-100p mb-1">
|
|
||||||
<tr>
|
|
||||||
<th class="align-left"></th>
|
|
||||||
<th class="align-right"></th>
|
|
||||||
</tr>
|
|
||||||
{{ range .Orders }}
|
|
||||||
<tr class='{{ if eq .Side "BUY" }}yes{{ else }}no{{ end }}'>
|
|
||||||
<td class="align-center">{{ .Side }}</td>
|
|
||||||
<td class="align-center">{{ .Share.Description }}</td>
|
|
||||||
<td class="align-right">{{.Quantity}} @ {{.Price}} ⚡</td>
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<div class="align-left">
|
|
||||||
<span class="font-mono mb-1"><strong>Order Form</strong></span>
|
|
||||||
<form id="form" class="order-form" action="/market/{{$.Id}}/order" method="post">
|
|
||||||
<button id="buy" type="button" class="order-button yes w-100p selected">BUY</button>
|
|
||||||
<button id="sell" type="button" class="order-button no w-100p">SELL</button>
|
|
||||||
<input id="market-id" hidden name="market_id" value="{{$.Id}}" />
|
|
||||||
<input id="side" hidden name="side" value="BUY" />
|
|
||||||
<label>share</label>
|
|
||||||
<select name="share_id">
|
|
||||||
<option value="{{.YesShare.Id}}">YES</option>
|
|
||||||
<option value="{{.NoShare.Id}}">NO</option>
|
|
||||||
</select>
|
|
||||||
<label>quantity</label>
|
|
||||||
<input id="quantity" type="number" name="quantity" placeholder="quantity" />
|
|
||||||
<label>price [sats ⚡]</label>
|
|
||||||
<input id="price" type="number" name="price" placeholder="price" />
|
|
||||||
<label id="submit-label"></label>
|
|
||||||
<button type="submit">SUBMIT</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
<script src="/order.js"></script>
|
|
||||||
|
|
||||||
</html>
|
|
106
pages/user.html
106
pages/user.html
@ -1,106 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>delphi.market</title>
|
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
|
||||||
<link rel="stylesheet" href="/css/index.css" />
|
|
||||||
<link rel="stylesheet" href="/market.css" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#091833" />
|
|
||||||
{{ if eq .ENV "development" }}
|
|
||||||
<script defer src="/hotreload.js"></script>
|
|
||||||
{{ end }}
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<header class="flex flex-row text-center justify-center pt-1">
|
|
||||||
<nav>
|
|
||||||
<a href="/">home</a>
|
|
||||||
{{ if .session }}
|
|
||||||
<a href='/user'>user</a>
|
|
||||||
{{ else }} <a href="/login">login</a> {{ end }}
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<div class="container flex flex-column text-center">
|
|
||||||
<code>
|
|
||||||
<strong>
|
|
||||||
<pre>
|
|
||||||
_ _ ___ ___ _ __
|
|
||||||
| | | / __|/ _ \ '__|
|
|
||||||
| |_| \__ \ __/ |
|
|
||||||
\__,_|___/\___|_| </pre>
|
|
||||||
</strong>
|
|
||||||
</code>
|
|
||||||
<div class="font-mono mb-1 align-left word-wrap">
|
|
||||||
<div class="mb-1">
|
|
||||||
You are: {{substr .session.Pubkey 0 8}}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<form class="align-left" action="/logout" method="post">
|
|
||||||
<button type="submit">logout</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="align-left mb-1">
|
|
||||||
<span class="font-mono mb-1"><strong>Open Orders</strong></span>
|
|
||||||
<table class="w-100p mb-1">
|
|
||||||
<tr>
|
|
||||||
<th class="align-center">Market</th>
|
|
||||||
<th class="align-center"></th>
|
|
||||||
<th class="align-center"></th>
|
|
||||||
<th class="align-right"></th>
|
|
||||||
</tr>
|
|
||||||
{{ range .Orders }}
|
|
||||||
{{ if .Invoice.ConfirmedAt.Valid }}
|
|
||||||
<tr class='{{ if eq .Side "BUY" }}yes{{ else }}no{{ end }}'>
|
|
||||||
<td class="align-center">
|
|
||||||
<a href="/market/{{.Share.MarketId}}">{{.Share.MarketId}}</a>
|
|
||||||
</td>
|
|
||||||
<td class="align-center">{{.Side}}</td>
|
|
||||||
<td class="align-center">{{.Share.Description}}</td>
|
|
||||||
<td class="align-right">{{.Quantity}} @ {{.Price}} ⚡</td>
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="align-left mb-1">
|
|
||||||
<span class="font-mono mb-1"><strong>Unpaid Orders</strong></span>
|
|
||||||
<table class="w-100p mb-1">
|
|
||||||
<tr>
|
|
||||||
<th class="align-center">Market</th>
|
|
||||||
<th class="align-center"></th>
|
|
||||||
<th class="align-center"></th>
|
|
||||||
<th class="align-right"></th>
|
|
||||||
<th class="align-center">Invoice</th>
|
|
||||||
</tr>
|
|
||||||
{{ range .Orders }}
|
|
||||||
{{ if not .Invoice.ConfirmedAt.Valid }}
|
|
||||||
<tr class='{{ if eq .Side "BUY" }}yes{{ else }}no{{ end }}'>
|
|
||||||
<td class="align-center">
|
|
||||||
<a href="/market/{{.Share.MarketId}}">{{.Share.MarketId}}</a>
|
|
||||||
</td>
|
|
||||||
<td class="align-center">{{.Side}}</td>
|
|
||||||
<td class="align-center">{{.Share.Description}}</td>
|
|
||||||
<td class="align-right">{{.Quantity}} @ {{.Price}} ⚡</td>
|
|
||||||
<td class="align-center"><a href="/invoice/{{.InvoiceId}}">invoice</a></td>
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<footer class="flex justify-center">
|
|
||||||
<div>
|
|
||||||
<hr />
|
|
||||||
<code><a href="https://github.com/ekzyis/delphi.market/commit/{{.COMMIT_LONG_SHA}}">{{.VERSION}}</a></code>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,16 +0,0 @@
|
|||||||
const marketId = document.querySelector("#market-id").value
|
|
||||||
|
|
||||||
const buyBtn = document.querySelector("#buy")
|
|
||||||
const sellBtn = document.querySelector("#sell")
|
|
||||||
const sideInput = document.querySelector("#side")
|
|
||||||
|
|
||||||
buyBtn.onclick = function (e) {
|
|
||||||
buyBtn.classList.add("selected")
|
|
||||||
sellBtn.classList.remove("selected")
|
|
||||||
sideInput.setAttribute("value", "BUY")
|
|
||||||
}
|
|
||||||
sellBtn.onclick = function(e) {
|
|
||||||
buyBtn.classList.remove("selected")
|
|
||||||
sellBtn.classList.add("selected")
|
|
||||||
sideInput.setAttribute("value", 'SELL')
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
.order-button {
|
|
||||||
border: none;
|
|
||||||
margin: auto;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-form {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
grid-template-rows: 1fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sx-1 {
|
|
||||||
margin: 0 1rem;
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yes {
|
|
||||||
background-color: rgba(20,158,97,.24);
|
|
||||||
color: #35df8d;
|
|
||||||
}
|
|
||||||
button.yes:hover {
|
|
||||||
background-color: #35df8d;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
button.yes.selected {
|
|
||||||
background-color: #35df8d;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no {
|
|
||||||
background-color: rgba(245,57,94,.24);
|
|
||||||
color: #ff7386;
|
|
||||||
}
|
|
||||||
button.no:hover {
|
|
||||||
background-color: #ff7386;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
button.no.selected {
|
|
||||||
background-color: #ff7386;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
font-size: small;
|
|
||||||
}
|
|
@ -1,9 +1,6 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages"
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
@ -14,16 +11,3 @@ func HandleIndex(sc context.Context) echo.HandlerFunc {
|
|||||||
return pages.Index().Render(context.RenderContext(sc, c), c.Response().Writer)
|
return pages.Index().Render(context.RenderContext(sc, c), c.Response().Writer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleMarkets(sc context.Context) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
var (
|
|
||||||
markets []db.Market
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if err = sc.Db.FetchActiveMarkets(&markets); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, markets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,110 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/lib"
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
|
||||||
)
|
|
||||||
|
|
||||||
func HandleInvoiceStatus(sc context.Context) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
var (
|
|
||||||
invoiceId string
|
|
||||||
invoice db.Invoice
|
|
||||||
u db.User
|
|
||||||
qr string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
invoiceId = c.Param("id")
|
|
||||||
if err = sc.Db.FetchInvoice(&db.FetchInvoiceWhere{Id: invoiceId}, &invoice); err == sql.ErrNoRows {
|
|
||||||
return echo.NewHTTPError(http.StatusNotFound)
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if u = c.Get("session").(db.User); invoice.Pubkey != u.Pubkey {
|
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
if qr, err = lib.ToQR(invoice.PaymentRequest); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
invoice.Preimage = ""
|
|
||||||
data := map[string]any{
|
|
||||||
"Id": invoice.Id,
|
|
||||||
"Msats": invoice.Msats,
|
|
||||||
"MsatsReceived": invoice.MsatsReceived,
|
|
||||||
"Hash": invoice.Hash,
|
|
||||||
"PaymentRequest": invoice.PaymentRequest,
|
|
||||||
"CreatedAt": invoice.CreatedAt,
|
|
||||||
"ExpiresAt": invoice.ExpiresAt,
|
|
||||||
"ConfirmedAt": invoice.ConfirmedAt,
|
|
||||||
"HeldSince": invoice.HeldSince,
|
|
||||||
"Description": invoice.Description,
|
|
||||||
"Qr": qr,
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandleInvoice(sc context.Context) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
var (
|
|
||||||
invoiceId string
|
|
||||||
invoice db.Invoice
|
|
||||||
u db.User
|
|
||||||
hash lntypes.Hash
|
|
||||||
qr string
|
|
||||||
status string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
invoiceId = c.Param("id")
|
|
||||||
if err = sc.Db.FetchInvoice(&db.FetchInvoiceWhere{Id: invoiceId}, &invoice); err == sql.ErrNoRows {
|
|
||||||
return echo.NewHTTPError(http.StatusNotFound)
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if u = c.Get("session").(db.User); invoice.Pubkey != u.Pubkey {
|
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
if hash, err = lntypes.MakeHashFromStr(invoice.Hash); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
go sc.Lnd.CheckInvoice(sc.Db, hash)
|
|
||||||
if qr, err = lib.ToQR(invoice.PaymentRequest); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if invoice.ConfirmedAt.Valid {
|
|
||||||
status = "Paid"
|
|
||||||
} else if time.Now().After(invoice.ExpiresAt) {
|
|
||||||
status = "Expired"
|
|
||||||
}
|
|
||||||
data := map[string]any{
|
|
||||||
"session": c.Get("session"),
|
|
||||||
"invoice": invoice,
|
|
||||||
"status": status,
|
|
||||||
"lnurl": invoice.PaymentRequest,
|
|
||||||
"qr": qr,
|
|
||||||
}
|
|
||||||
return c.Render(http.StatusOK, "invoice.html", data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandleInvoices(sc context.Context) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
var (
|
|
||||||
u db.User
|
|
||||||
invoices []db.Invoice
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
u = c.Get("session").(db.User)
|
|
||||||
if err = sc.Db.FetchUserInvoices(u.Pubkey, &invoices); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, invoices)
|
|
||||||
}
|
|
||||||
}
|
|
57
server/router/handler/lnauth_test.go
Normal file
57
server/router/handler/lnauth_test.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package handler_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/handler"
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/test"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
test.Init(&db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLnAuth(t *testing.T) {
|
||||||
|
var (
|
||||||
|
assert = assert.New(t)
|
||||||
|
e *echo.Echo
|
||||||
|
c echo.Context
|
||||||
|
sc context.Context
|
||||||
|
req *http.Request
|
||||||
|
rec *httptest.ResponseRecorder
|
||||||
|
cookies []*http.Cookie
|
||||||
|
sessionId string
|
||||||
|
dbSessionId string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
sc = context.Context{Db: db}
|
||||||
|
e, req, rec = test.HTTPMocks("GET", "/login", nil)
|
||||||
|
c = e.NewContext(req, rec)
|
||||||
|
|
||||||
|
err = handler.HandleLogin(sc)(c)
|
||||||
|
assert.NoErrorf(err, "handler returned error")
|
||||||
|
|
||||||
|
// Set-Cookie header present
|
||||||
|
cookies = rec.Result().Cookies()
|
||||||
|
assert.Equalf(len(cookies), 1, "wrong number of Set-Cookie headers")
|
||||||
|
assert.Equalf(cookies[0].Name, "session", "wrong cookie name")
|
||||||
|
|
||||||
|
// new challenge inserted
|
||||||
|
sessionId = cookies[0].Value
|
||||||
|
err = db.QueryRow("SELECT session_id FROM lnauth WHERE session_id = $1", sessionId).Scan(&dbSessionId)
|
||||||
|
if !assert.NoError(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// inserted challenge matches cookie value
|
||||||
|
assert.Equalf(sessionId, dbSessionId, "wrong session id")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLnAuthCallback(t *testing.T) {
|
||||||
|
t.Skip()
|
||||||
|
}
|
@ -1,11 +1,6 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/auth"
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages"
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
@ -16,65 +11,3 @@ func HandleLogin(sc context.Context) echo.HandlerFunc {
|
|||||||
return pages.Login().Render(context.RenderContext(sc, c), c.Response().Writer)
|
return pages.Login().Render(context.RenderContext(sc, c), c.Response().Writer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// func HandleLogin(sc context.Context) echo.HandlerFunc {
|
|
||||||
// return func(c echo.Context) error {
|
|
||||||
// var (
|
|
||||||
// lnAuth *auth.LNAuth
|
|
||||||
// dbLnAuth db.LNAuth
|
|
||||||
// err error
|
|
||||||
// expires time.Time = time.Now().Add(60 * 60 * 24 * 365 * time.Second)
|
|
||||||
// qr string
|
|
||||||
// data map[string]any
|
|
||||||
// )
|
|
||||||
// if lnAuth, err = auth.NewLNAuth(); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// dbLnAuth = db.LNAuth{K1: lnAuth.K1, LNURL: lnAuth.LNURL}
|
|
||||||
// if err = sc.Db.CreateLNAuth(&dbLnAuth); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// c.SetCookie(&http.Cookie{Name: "session", HttpOnly: true, Path: "/", Value: dbLnAuth.SessionId, Secure: true, Expires: expires})
|
|
||||||
// if qr, err = lib.ToQR(lnAuth.LNURL); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// data = map[string]any{
|
|
||||||
// "lnurl": lnAuth.LNURL,
|
|
||||||
// "qr": qr,
|
|
||||||
// }
|
|
||||||
// return c.JSON(http.StatusOK, data)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
func HandleLoginCallback(sc context.Context) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
var (
|
|
||||||
query auth.LNAuthResponse
|
|
||||||
sessionId string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if err := c.Bind(&query); err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
if err = sc.Db.FetchSessionId(query.K1, &sessionId); err == sql.ErrNoRows {
|
|
||||||
return echo.NewHTTPError(http.StatusNotFound, map[string]string{"reason": "session not found"})
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if ok, err := auth.VerifyLNAuth(&query); err != nil {
|
|
||||||
return err
|
|
||||||
} else if !ok {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, map[string]string{"reason": "bad signature"})
|
|
||||||
}
|
|
||||||
if err = sc.Db.CreateUser(&db.User{Pubkey: query.Key}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = sc.Db.CreateSession(&db.Session{Pubkey: query.Key, SessionId: sessionId}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = sc.Db.DeleteLNAuth(&db.LNAuth{K1: query.K1}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, map[string]string{"status": "OK"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,123 +0,0 @@
|
|||||||
package handler_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
db_ "git.ekzyis.com/ekzyis/delphi.market/db"
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/auth"
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/handler"
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/test"
|
|
||||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
test.Init(&db)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLogin(t *testing.T) {
|
|
||||||
var (
|
|
||||||
assert = assert.New(t)
|
|
||||||
e *echo.Echo
|
|
||||||
c echo.Context
|
|
||||||
sc context.Context
|
|
||||||
req *http.Request
|
|
||||||
rec *httptest.ResponseRecorder
|
|
||||||
cookies []*http.Cookie
|
|
||||||
sessionId string
|
|
||||||
dbSessionId string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
sc = context.Context{Db: db}
|
|
||||||
e, req, rec = test.HTTPMocks("GET", "/login", nil)
|
|
||||||
c = e.NewContext(req, rec)
|
|
||||||
|
|
||||||
err = handler.HandleLogin(sc)(c)
|
|
||||||
assert.NoErrorf(err, "handler returned error")
|
|
||||||
|
|
||||||
// Set-Cookie header present
|
|
||||||
cookies = rec.Result().Cookies()
|
|
||||||
assert.Equalf(len(cookies), 1, "wrong number of Set-Cookie headers")
|
|
||||||
assert.Equalf(cookies[0].Name, "session", "wrong cookie name")
|
|
||||||
|
|
||||||
// new challenge inserted
|
|
||||||
sessionId = cookies[0].Value
|
|
||||||
err = db.QueryRow("SELECT session_id FROM lnauth WHERE session_id = $1", sessionId).Scan(&dbSessionId)
|
|
||||||
if !assert.NoError(err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// inserted challenge matches cookie value
|
|
||||||
assert.Equalf(sessionId, dbSessionId, "wrong session id")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoginCallback(t *testing.T) {
|
|
||||||
var (
|
|
||||||
assert = assert.New(t)
|
|
||||||
e *echo.Echo
|
|
||||||
c echo.Context
|
|
||||||
sc context.Context
|
|
||||||
req *http.Request
|
|
||||||
rec *httptest.ResponseRecorder
|
|
||||||
sk *secp256k1.PrivateKey
|
|
||||||
pk *secp256k1.PublicKey
|
|
||||||
lnAuth *auth.LNAuth
|
|
||||||
dbLnAuth *db_.LNAuth
|
|
||||||
u *db_.User
|
|
||||||
s *db_.Session
|
|
||||||
key string
|
|
||||||
sig string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
lnAuth, err = auth.NewLNAuth()
|
|
||||||
if !assert.NoErrorf(err, "error creating challenge") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dbLnAuth = &db_.LNAuth{K1: lnAuth.K1, LNURL: lnAuth.LNURL}
|
|
||||||
err = db.CreateLNAuth(dbLnAuth)
|
|
||||||
if !assert.NoErrorf(err, "error inserting challenge") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sk, pk, err = test.GenerateKeyPair()
|
|
||||||
if !assert.NoErrorf(err, "error generating keypair") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sig, err = test.Sign(sk, lnAuth.K1)
|
|
||||||
if !assert.NoErrorf(err, "error signing k1") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
key = hex.EncodeToString(pk.SerializeCompressed())
|
|
||||||
|
|
||||||
sc = context.Context{Db: db}
|
|
||||||
e, req, rec = test.HTTPMocks("GET", fmt.Sprintf("/api/login?k1=%s&key=%s&sig=%s", lnAuth.K1, key, sig), nil)
|
|
||||||
c = e.NewContext(req, rec)
|
|
||||||
|
|
||||||
err = handler.HandleLoginCallback(sc)(c)
|
|
||||||
assert.NoErrorf(err, "handler returned error")
|
|
||||||
|
|
||||||
// user created
|
|
||||||
u = new(db_.User)
|
|
||||||
err = db.FetchUser(key, u)
|
|
||||||
if assert.NoErrorf(err, "error fetching user") {
|
|
||||||
assert.Equalf(u.Pubkey, key, "pubkeys do not match")
|
|
||||||
}
|
|
||||||
|
|
||||||
// session created
|
|
||||||
s = &db_.Session{SessionId: dbLnAuth.SessionId}
|
|
||||||
err = db.FetchSession(s)
|
|
||||||
if assert.NoErrorf(err, "error fetching session") {
|
|
||||||
assert.Equalf(s.Pubkey, u.Pubkey, "session pubkey does not match user pubkey")
|
|
||||||
}
|
|
||||||
|
|
||||||
// challenge deleted
|
|
||||||
err = db.FetchSessionId(u.Pubkey, &dbLnAuth.SessionId)
|
|
||||||
assert.ErrorIs(err, sql.ErrNoRows, "challenge not deleted")
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
func HandleLogout(sc context.Context) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
var (
|
|
||||||
cookie *http.Cookie
|
|
||||||
sessionId string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if cookie, err = c.Cookie("session"); err != nil {
|
|
||||||
// cookie not found
|
|
||||||
return c.JSON(http.StatusNotFound, map[string]string{"reason": "session not found"})
|
|
||||||
}
|
|
||||||
sessionId = cookie.Value
|
|
||||||
if err = sc.Db.DeleteSession(&db.Session{SessionId: sessionId}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// tell browser that cookie is expired and thus can be deleted
|
|
||||||
c.SetCookie(&http.Cookie{Name: "session", HttpOnly: true, Path: "/", Value: sessionId, Secure: true, Expires: time.Now()})
|
|
||||||
return c.JSON(http.StatusSeeOther, map[string]string{"status": "OK"})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +1,9 @@
|
|||||||
package handler_test
|
package handler_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
db_ "git.ekzyis.com/ekzyis/delphi.market/db"
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/handler"
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/test"
|
"git.ekzyis.com/ekzyis/delphi.market/test"
|
||||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -22,51 +11,5 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLogout(t *testing.T) {
|
func TestLogout(t *testing.T) {
|
||||||
var (
|
t.Skip()
|
||||||
assert = assert.New(t)
|
|
||||||
e *echo.Echo
|
|
||||||
c echo.Context
|
|
||||||
sc context.Context
|
|
||||||
req *http.Request
|
|
||||||
rec *httptest.ResponseRecorder
|
|
||||||
pk *secp256k1.PublicKey
|
|
||||||
s *db_.Session
|
|
||||||
key string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
sc = context.Context{Db: db}
|
|
||||||
e, req, rec = test.HTTPMocks("POST", "/logout", nil)
|
|
||||||
|
|
||||||
_, pk, err = test.GenerateKeyPair()
|
|
||||||
if !assert.NoErrorf(err, "error generating keypair") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
key = hex.EncodeToString(pk.SerializeCompressed())
|
|
||||||
err = sc.Db.CreateUser(&db_.User{Pubkey: key})
|
|
||||||
if !assert.NoErrorf(err, "error creating user") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s = &db_.Session{Pubkey: key}
|
|
||||||
err = sc.Db.QueryRow("SELECT encode(gen_random_uuid()::text::bytea, 'base64')").Scan(&s.SessionId)
|
|
||||||
if !assert.NoErrorf(err, "error creating session id") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// create session
|
|
||||||
err = sc.Db.CreateSession(s)
|
|
||||||
if !assert.NoErrorf(err, "error creating session") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// session authentication via cookie
|
|
||||||
req.Header.Add("cookie", fmt.Sprintf("session=%s", s.SessionId))
|
|
||||||
|
|
||||||
c = e.NewContext(req, rec)
|
|
||||||
err = handler.HandleLogout(sc)(c)
|
|
||||||
assert.NoErrorf(err, "handler returned error")
|
|
||||||
|
|
||||||
// session must have been deleted
|
|
||||||
err = sc.Db.FetchSession(s)
|
|
||||||
assert.ErrorIsf(err, sql.ErrNoRows, "session not deleted")
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,475 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
context_ "context"
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/lib"
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
|
||||||
)
|
|
||||||
|
|
||||||
func HandleMarket(sc context.Context) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
var (
|
|
||||||
marketId int64
|
|
||||||
market db.Market
|
|
||||||
shares []db.Share
|
|
||||||
orders []db.Order
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
if err = sc.Db.FetchMarket(int(marketId), &market); err == sql.ErrNoRows {
|
|
||||||
return echo.NewHTTPError(http.StatusNotFound, "Not Found")
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = sc.Db.FetchShares(market.Id, &shares); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = sc.Db.FetchOrders(&db.FetchOrdersWhere{MarketId: market.Id, Confirmed: true}, &orders); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
data = map[string]any{
|
|
||||||
"Id": market.Id,
|
|
||||||
"Pubkey": market.Pubkey,
|
|
||||||
"Description": market.Description,
|
|
||||||
"SettledAt": market.SettledAt,
|
|
||||||
"Shares": shares,
|
|
||||||
}
|
|
||||||
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(tx, ctx, int(marketId), u.Pubkey, &uBalance); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
lib.Merge(&data, &map[string]any{"user": uBalance})
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandleCreateMarket(sc context.Context) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
var (
|
|
||||||
tx *sql.Tx
|
|
||||||
u db.User
|
|
||||||
m db.Market
|
|
||||||
invoice *db.Invoice
|
|
||||||
msats int64
|
|
||||||
invDescription string
|
|
||||||
data map[string]any
|
|
||||||
qr string
|
|
||||||
hash lntypes.Hash
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if err := c.Bind(&m); err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
|
|
||||||
u = c.Get("session").(db.User)
|
|
||||||
m.Pubkey = u.Pubkey
|
|
||||||
msats = 1000
|
|
||||||
// TODO: add [market:<id>] for redirect after payment
|
|
||||||
invDescription = fmt.Sprintf("create market \"%s\"", m.Description)
|
|
||||||
if invoice, err = sc.Lnd.CreateInvoice(tx, ctx, sc.Db, u.Pubkey, msats, invDescription); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
m.InvoiceId = invoice.Id
|
|
||||||
if err := sc.Db.CreateMarket(tx, ctx, &m); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// need to commit before starting to poll invoice status
|
|
||||||
tx.Commit()
|
|
||||||
go sc.Lnd.CheckInvoice(sc.Db, hash)
|
|
||||||
|
|
||||||
data = map[string]any{
|
|
||||||
"id": invoice.Id,
|
|
||||||
"bolt11": invoice.PaymentRequest,
|
|
||||||
"amount": msats,
|
|
||||||
"qr": qr,
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusPaymentRequired, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandleMarketOrders(sc context.Context) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
var (
|
|
||||||
marketId int64
|
|
||||||
orders []db.Order
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if marketId, err = strconv.ParseInt(c.Param("id"), 10, 64); err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
|
|
||||||
}
|
|
||||||
if err = sc.Db.FetchMarketOrders(marketId, &orders); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, orders)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandleOrder(sc context.Context) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
var (
|
|
||||||
tx *sql.Tx
|
|
||||||
u db.User
|
|
||||||
o db.Order
|
|
||||||
s db.Share
|
|
||||||
m db.Market
|
|
||||||
invoice *db.Invoice
|
|
||||||
msats int64
|
|
||||||
description string
|
|
||||||
data map[string]any
|
|
||||||
qr string
|
|
||||||
hash lntypes.Hash
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if err := c.Bind(&o); err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
u = c.Get("session").(db.User)
|
|
||||||
o.Pubkey = u.Pubkey
|
|
||||||
msats = o.Quantity * o.Price * 1000
|
|
||||||
|
|
||||||
// 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.FetchShare(tx, ctx, o.ShareId, &s); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = sc.Db.FetchMarket(s.MarketId, &m); err == sql.ErrNoRows {
|
|
||||||
return c.JSON(http.StatusNotFound, nil)
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.SettledAt.Valid {
|
|
||||||
return c.JSON(http.StatusBadRequest, map[string]string{"reason": "market already settled"})
|
|
||||||
}
|
|
||||||
|
|
||||||
description = fmt.Sprintf("%s %d %s shares @ %d sats [market:%d]", strings.ToUpper(o.Side), o.Quantity, s.Description, o.Price, s.MarketId)
|
|
||||||
|
|
||||||
if o.Side == "BUY" {
|
|
||||||
// BUY orders require payment
|
|
||||||
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.String = 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)
|
|
||||||
|
|
||||||
data = map[string]any{
|
|
||||||
"id": invoice.Id,
|
|
||||||
"bolt11": invoice.PaymentRequest,
|
|
||||||
"amount": msats,
|
|
||||||
"qr": qr,
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusPaymentRequired, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
tx.Commit()
|
|
||||||
return c.JSON(http.StatusCreated, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandleDeleteOrder(sc context.Context) 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.Valid {
|
|
||||||
// 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
|
|
||||||
// TODO update order and session on client
|
|
||||||
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.Context) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
var (
|
|
||||||
u db.User
|
|
||||||
orders []db.Order
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
u = c.Get("session").(db.User)
|
|
||||||
if err = sc.Db.FetchUserOrders(u.Pubkey, &orders); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, orders)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandleMarketStats(sc context.Context) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
var (
|
|
||||||
marketId int64
|
|
||||||
stats db.MarketStats
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if marketId, err = strconv.ParseInt(c.Param("id"), 10, 64); err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
|
|
||||||
}
|
|
||||||
if err = sc.Db.FetchMarketStats(marketId, &stats); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, stats)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandleMarketSettlement(sc context.Context) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
var (
|
|
||||||
marketId int64
|
|
||||||
market db.Market
|
|
||||||
s db.Share
|
|
||||||
tx *sql.Tx
|
|
||||||
u db.User
|
|
||||||
query string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if marketId, err = strconv.ParseInt(c.Param("id"), 10, 64); err != nil {
|
|
||||||
return c.JSON(http.StatusBadRequest, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = c.Bind(&s); err != nil || s.Id == "" {
|
|
||||||
return c.JSON(http.StatusBadRequest, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = sc.Db.FetchMarket(int(marketId), &market); err == sql.ErrNoRows {
|
|
||||||
return c.JSON(http.StatusNotFound, map[string]string{"reason": "market not found"})
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
u = c.Get("session").(db.User)
|
|
||||||
|
|
||||||
// only market owner can settle market
|
|
||||||
if market.Pubkey != u.Pubkey {
|
|
||||||
return c.JSON(http.StatusForbidden, map[string]string{"reason": "not your market"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// market already settled?
|
|
||||||
if market.SettledAt.Valid {
|
|
||||||
return c.JSON(http.StatusBadRequest, map[string]string{"reason": "market already settled"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// transaction start
|
|
||||||
ctx, cancel := context_.WithTimeout(c.Request().Context(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
if tx, err = sc.Db.BeginTx(ctx, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer tx.Commit()
|
|
||||||
|
|
||||||
// refund users for pending BUY orders
|
|
||||||
query = "" +
|
|
||||||
"UPDATE users u SET msats = msats + pending_orders.msats_received FROM ( " +
|
|
||||||
" SELECT o.pubkey, i.msats_received " +
|
|
||||||
" FROM orders o " +
|
|
||||||
" LEFT JOIN invoices i ON i.id = o.invoice_id " +
|
|
||||||
" JOIN shares s ON s.id = o.share_id " +
|
|
||||||
" WHERE s.market_id = $1 " +
|
|
||||||
// an order is pending if it wasn't canceled and wasn't matched yet
|
|
||||||
// (the o.side = 'BUY' shouldn't be necessary since i.msats_received will be NULL for SELL orders anyway
|
|
||||||
// but added here for clarification anyway)
|
|
||||||
" AND o.side = 'BUY' AND o.deleted_at IS NULL AND o.order_id IS NULL " +
|
|
||||||
") AS pending_orders WHERE pending_orders.pubkey = u.pubkey"
|
|
||||||
if _, err = tx.ExecContext(ctx, query, marketId); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// now cancel pending orders
|
|
||||||
query = "" +
|
|
||||||
"UPDATE orders o SET deleted_at = CURRENT_TIMESTAMP WHERE id IN ( " +
|
|
||||||
// basically same subquery as above
|
|
||||||
" SELECT o.id FROM orders o " +
|
|
||||||
" JOIN shares s ON s.id = o.share_id " +
|
|
||||||
// again, orders are pending if they weren't canceled and weren't matched yet
|
|
||||||
" WHERE s.market_id = $1 AND o.deleted_at IS NULL and o.order_id IS NULL " +
|
|
||||||
")"
|
|
||||||
if _, err = tx.ExecContext(ctx, query, marketId); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// payout
|
|
||||||
query = "" +
|
|
||||||
// * 100 since winning shares expire at 100 sats per share
|
|
||||||
// * 1000 to convert sats to msats
|
|
||||||
"UPDATE users u SET msats = msats + (user_shares.quantity * 100 * 1000) " +
|
|
||||||
"FROM ( " +
|
|
||||||
" SELECT o.pubkey, o.share_id, " +
|
|
||||||
" SUM(CASE WHEN o.side = 'BUY' THEN o.quantity ELSE -o.quantity END) AS quantity " +
|
|
||||||
" FROM orders o " +
|
|
||||||
" LEFT JOIN invoices i ON i.id = o.invoice_id " +
|
|
||||||
" JOIN shares s ON s.id = o.share_id " +
|
|
||||||
// only consider uncanceled orders for winning shares
|
|
||||||
" WHERE s.market_id = $1 AND o.deleted_at IS NULL AND s.id = $2 " +
|
|
||||||
// BUY orders must be paid and be matched. SELL orders must simply not be canceled to be considered.
|
|
||||||
" 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, o.share_id " +
|
|
||||||
") AS user_shares WHERE user_shares.pubkey = u.pubkey"
|
|
||||||
if _, err = tx.ExecContext(ctx, query, marketId, s.Id); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = tx.ExecContext(ctx, "UPDATE markets SET settled_at = CURRENT_TIMESTAMP WHERE id = $1", marketId); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = tx.ExecContext(ctx, "UPDATE shares SET win = (id = $1) WHERE market_id = $2", s.Id, marketId); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.Commit()
|
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, nil)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
func HandleCheckSession(sc context.Context) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
var (
|
|
||||||
cookie *http.Cookie
|
|
||||||
s db.Session
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if cookie, err = c.Cookie("session"); err != nil {
|
|
||||||
return c.JSON(http.StatusBadRequest, map[string]string{"reason": "cookie required"})
|
|
||||||
}
|
|
||||||
s = db.Session{SessionId: cookie.Value}
|
|
||||||
if err = sc.Db.FetchSession(&s); err == sql.ErrNoRows {
|
|
||||||
return c.JSON(http.StatusBadRequest, map[string]string{"reason": "session not found"})
|
|
||||||
} else if err != nil {
|
|
||||||
return c.JSON(http.StatusInternalServerError, nil)
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, map[string]any{"pubkey": s.Pubkey, "msats": s.Msats})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
func HandleUser(sc context.Context) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
var (
|
|
||||||
u db.User
|
|
||||||
orders []db.Order
|
|
||||||
err error
|
|
||||||
data map[string]any
|
|
||||||
)
|
|
||||||
u = c.Get("session").(db.User)
|
|
||||||
if err = sc.Db.FetchOrders(&db.FetchOrdersWhere{Pubkey: u.Pubkey}, &orders); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
data = map[string]any{
|
|
||||||
"session": c.Get("session"),
|
|
||||||
"user": u,
|
|
||||||
"Orders": orders,
|
|
||||||
}
|
|
||||||
return c.Render(http.StatusOK, "user.html", data)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
context_ "context"
|
|
||||||
"database/sql"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/lightningnetwork/lnd/zpay32"
|
|
||||||
)
|
|
||||||
|
|
||||||
func HandleWithdrawal(sc context.Context) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
var (
|
|
||||||
w db.Withdrawal
|
|
||||||
u db.User
|
|
||||||
inv *zpay32.Invoice
|
|
||||||
tx *sql.Tx
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if err := c.Bind(&w); err != nil {
|
|
||||||
code := http.StatusBadRequest
|
|
||||||
return c.JSON(code, map[string]any{"status": code, "reason": "bolt11 required"})
|
|
||||||
}
|
|
||||||
|
|
||||||
if inv, err = zpay32.Decode(w.Bolt11, sc.Lnd.ChainParams); err != nil {
|
|
||||||
code := http.StatusBadRequest
|
|
||||||
return c.JSON(code, map[string]any{"status": code, "reason": "zpay32 decode error"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
|
|
||||||
u = c.Get("session").(db.User)
|
|
||||||
w.Pubkey = u.Pubkey
|
|
||||||
|
|
||||||
// TODO deduct network fee from user balance
|
|
||||||
if u.Msats < int64(*inv.MilliSat) {
|
|
||||||
tx.Rollback()
|
|
||||||
code := http.StatusBadRequest
|
|
||||||
return c.JSON(code, map[string]any{"status": code, "reason": "insufficient balance"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// create withdrawal
|
|
||||||
if err = sc.Db.CreateWithdrawal(tx, ctx, &w); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
if strings.Contains(err.Error(), "violates unique constraint") {
|
|
||||||
code := http.StatusBadRequest
|
|
||||||
return c.JSON(code, map[string]any{"status": code, "reason": "bolt11 already submitted"})
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// pay invoice via LND
|
|
||||||
if err = sc.Lnd.PayInvoice(tx, w.Bolt11); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// deduct balance from user
|
|
||||||
if _, err = tx.ExecContext(ctx, "UPDATE users SET msats = msats - $1 WHERE pubkey = $2", int64(*inv.MilliSat), u.Pubkey); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.Commit()
|
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, nil)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +1,8 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
@ -13,27 +10,24 @@ import (
|
|||||||
func Session(sc context.Context) echo.MiddlewareFunc {
|
func Session(sc context.Context) echo.MiddlewareFunc {
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
var (
|
// TODO: implement session middleware
|
||||||
cookie *http.Cookie
|
// var (
|
||||||
err error
|
// cookie *http.Cookie
|
||||||
s *db.Session
|
// err error
|
||||||
u *db.User
|
// s *db.Session
|
||||||
)
|
// u *db.User
|
||||||
if cookie, err = c.Cookie("session"); err != nil {
|
// )
|
||||||
// cookie not found
|
// if cookie, err = c.Cookie("session"); err != nil {
|
||||||
return next(c)
|
// // cookie not found
|
||||||
}
|
// return next(c)
|
||||||
s = &db.Session{SessionId: cookie.Value}
|
// }
|
||||||
if err = sc.Db.FetchSession(s); err == nil {
|
// s = &db.Session{SessionId: cookie.Value}
|
||||||
// session found
|
// if err = sc.Db.FetchSession(s); err == nil {
|
||||||
u = &db.User{Pubkey: s.Pubkey, Msats: s.Msats, LastSeen: time.Now()}
|
// // session found
|
||||||
if err = sc.Db.UpdateUser(u); err != nil {
|
// c.Set("session", *u)
|
||||||
return err
|
// } else if err != sql.ErrNoRows {
|
||||||
}
|
// return err
|
||||||
c.Set("session", *u)
|
// }
|
||||||
} else if err != sql.ErrNoRows {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return next(c)
|
return next(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,13 +5,12 @@ import (
|
|||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/handler"
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/handler"
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Context = context.Context
|
type Context = context.Context
|
||||||
|
|
||||||
func Init(e *echo.Echo, sc Context) {
|
func Init(e *echo.Echo, sc Context) {
|
||||||
e.Use(middleware.Session(sc))
|
// e.Use(middleware.Session(sc))
|
||||||
|
|
||||||
e.GET("/", handler.HandleIndex(sc))
|
e.GET("/", handler.HandleIndex(sc))
|
||||||
e.GET("/about", handler.HandleAbout(sc))
|
e.GET("/about", handler.HandleAbout(sc))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user