Merge branch 'vue-rewrite' into develop
6
Makefile
@ -1,6 +1,6 @@
|
|||||||
.PHONY: build run
|
.PHONY: build run test
|
||||||
|
|
||||||
SOURCE := $(shell find db env lib lnd pages public server -type f)
|
SOURCE := $(shell find db env lib lnd pages public server -type f) main.go
|
||||||
|
|
||||||
build: delphi.market
|
build: delphi.market
|
||||||
|
|
||||||
@ -10,3 +10,5 @@ delphi.market: $(SOURCE)
|
|||||||
run:
|
run:
|
||||||
go run .
|
go run .
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test -v -count=1 ./server/router/handler/...
|
||||||
|
80
db/db.go
@ -2,44 +2,70 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"log"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
"github.com/namsral/flag"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
db *DB
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DB struct {
|
type DB struct {
|
||||||
*sql.DB
|
*sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
var (
|
||||||
if err := godotenv.Load(); err != nil {
|
initSqlPath = "./db/init.sql"
|
||||||
log.Fatalf("error loading env vars: %s", err)
|
)
|
||||||
}
|
|
||||||
var dbUrl string
|
|
||||||
flag.StringVar(&dbUrl, "DATABASE_URL", "", "Database URL")
|
|
||||||
flag.Parse()
|
|
||||||
if dbUrl == "" {
|
|
||||||
log.Fatal("DATABASE_URL not set")
|
|
||||||
}
|
|
||||||
db = initDB(dbUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDB(url string) *DB {
|
func New(dbUrl string) (*DB, error) {
|
||||||
db, err := sql.Open("postgres", url)
|
var (
|
||||||
if err != nil {
|
db_ *sql.DB
|
||||||
log.Fatal(err)
|
db *DB
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if db_, err = sql.Open("postgres", dbUrl); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
// test connection
|
// test connection
|
||||||
_, err = db.Exec("SELECT 1")
|
if _, err = db_.Exec("SELECT 1"); err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
log.Fatal(err)
|
|
||||||
}
|
}
|
||||||
// TODO: run migrations
|
// TODO: run migrations
|
||||||
return &DB{DB: db}
|
db = &DB{DB: db_}
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Reset(dbName string) error {
|
||||||
|
var (
|
||||||
|
f []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if err = db.Clear(dbName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if f, err = ioutil.ReadFile(initSqlPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err = db.Exec(string(f)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Clear(dbName string) error {
|
||||||
|
var (
|
||||||
|
tables = []string{"lnauth", "users", "sessions", "markets", "shares", "invoices", "order_side", "orders", "matches"}
|
||||||
|
sql []string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
for _, t := range tables {
|
||||||
|
sql = append(sql, fmt.Sprintf("DROP TABLE IF EXISTS %s CASCADE", t))
|
||||||
|
}
|
||||||
|
sql = append(sql, "DROP EXTENSION IF EXISTS \"uuid-ossp\"")
|
||||||
|
sql = append(sql, "DROP TYPE IF EXISTS order_side")
|
||||||
|
for _, s := range sql {
|
||||||
|
if _, err = db.Exec(s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -12,18 +12,6 @@ CREATE TABLE sessions(
|
|||||||
pubkey TEXT NOT NULL REFERENCES users(pubkey),
|
pubkey TEXT NOT NULL REFERENCES users(pubkey),
|
||||||
session_id VARCHAR(48)
|
session_id VARCHAR(48)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE markets(
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
description TEXT NOT NULL,
|
|
||||||
active BOOLEAN DEFAULT true
|
|
||||||
);
|
|
||||||
CREATE EXTENSION "uuid-ossp";
|
|
||||||
CREATE TABLE shares(
|
|
||||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
||||||
market_id INTEGER REFERENCES markets(id),
|
|
||||||
description TEXT NOT NULL
|
|
||||||
);
|
|
||||||
CREATE TABLE invoices(
|
CREATE TABLE invoices(
|
||||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
pubkey TEXT NOT NULL REFERENCES users(pubkey),
|
pubkey TEXT NOT NULL REFERENCES users(pubkey),
|
||||||
@ -35,7 +23,20 @@ CREATE TABLE invoices(
|
|||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
confirmed_at TIMESTAMP WITH TIME ZONE,
|
confirmed_at TIMESTAMP WITH TIME ZONE,
|
||||||
held_since TIMESTAMP WITH TIME ZONE
|
held_since TIMESTAMP WITH TIME ZONE,
|
||||||
|
description TEXT
|
||||||
|
);
|
||||||
|
CREATE TABLE markets(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
end_date TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
invoice_id UUID NOT NULL UNIQUE REFERENCES invoices(id)
|
||||||
|
);
|
||||||
|
CREATE EXTENSION "uuid-ossp";
|
||||||
|
CREATE TABLE shares(
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
market_id INTEGER REFERENCES markets(id),
|
||||||
|
description TEXT NOT NULL
|
||||||
);
|
);
|
||||||
CREATE TYPE order_side AS ENUM ('BUY', 'SELL');
|
CREATE TYPE order_side AS ENUM ('BUY', 'SELL');
|
||||||
CREATE TABLE orders(
|
CREATE TABLE orders(
|
||||||
@ -49,6 +50,7 @@ CREATE TABLE orders(
|
|||||||
invoice_id UUID NOT NULL REFERENCES invoices(id)
|
invoice_id UUID NOT NULL REFERENCES invoices(id)
|
||||||
);
|
);
|
||||||
ALTER TABLE orders ADD CONSTRAINT order_price CHECK(price > 0 AND price < 100);
|
ALTER TABLE orders ADD CONSTRAINT order_price CHECK(price > 0 AND price < 100);
|
||||||
|
ALTER TABLE orders ADD CONSTRAINT order_quantity CHECK(quantity > 0);
|
||||||
CREATE TABLE matches(
|
CREATE TABLE matches(
|
||||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
oid1 UUID NOT NULL REFERENCES orders(id),
|
oid1 UUID NOT NULL REFERENCES orders(id),
|
@ -1,13 +1,16 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
func CreateInvoice(invoice *Invoice) error {
|
func (db *DB) CreateInvoice(invoice *Invoice) error {
|
||||||
if err := db.QueryRow(""+
|
if err := db.QueryRow(""+
|
||||||
"INSERT INTO invoices(pubkey, msats, preimage, hash, bolt11, created_at, expires_at) "+
|
"INSERT INTO invoices(pubkey, msats, preimage, hash, bolt11, created_at, expires_at, description) "+
|
||||||
"VALUES($1, $2, $3, $4, $5, $6, $7) "+
|
"VALUES($1, $2, $3, $4, $5, $6, $7, $8) "+
|
||||||
"RETURNING id",
|
"RETURNING id",
|
||||||
invoice.Pubkey, invoice.Msats, invoice.Preimage, invoice.Hash, invoice.PaymentRequest, invoice.CreatedAt, invoice.ExpiresAt).Scan(&invoice.Id); err != nil {
|
invoice.Pubkey, invoice.Msats, invoice.Preimage, invoice.Hash, invoice.PaymentRequest, invoice.CreatedAt, invoice.ExpiresAt, invoice.Description).Scan(&invoice.Id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -18,9 +21,9 @@ type FetchInvoiceWhere struct {
|
|||||||
Hash string
|
Hash string
|
||||||
}
|
}
|
||||||
|
|
||||||
func FetchInvoice(where *FetchInvoiceWhere, invoice *Invoice) error {
|
func (db *DB) FetchInvoice(where *FetchInvoiceWhere, invoice *Invoice) error {
|
||||||
var (
|
var (
|
||||||
query = "SELECT id, pubkey, msats, preimage, hash, bolt11, created_at, expires_at, confirmed_at, held_since FROM invoices "
|
query = "SELECT id, pubkey, msats, preimage, hash, bolt11, created_at, expires_at, confirmed_at, held_since, COALESCE(description, '') FROM invoices "
|
||||||
args []any
|
args []any
|
||||||
)
|
)
|
||||||
if where.Id != "" {
|
if where.Id != "" {
|
||||||
@ -32,13 +35,42 @@ func FetchInvoice(where *FetchInvoiceWhere, invoice *Invoice) error {
|
|||||||
}
|
}
|
||||||
if err := db.QueryRow(query, args...).Scan(
|
if err := db.QueryRow(query, args...).Scan(
|
||||||
&invoice.Id, &invoice.Pubkey, &invoice.Msats, &invoice.Preimage, &invoice.Hash,
|
&invoice.Id, &invoice.Pubkey, &invoice.Msats, &invoice.Preimage, &invoice.Hash,
|
||||||
&invoice.PaymentRequest, &invoice.CreatedAt, &invoice.ExpiresAt, &invoice.ConfirmedAt, &invoice.HeldSince); err != nil {
|
&invoice.PaymentRequest, &invoice.CreatedAt, &invoice.ExpiresAt, &invoice.ConfirmedAt, &invoice.HeldSince, &invoice.Description); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConfirmInvoice(hash string, confirmedAt time.Time, msatsReceived int) error {
|
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) ConfirmInvoice(hash string, confirmedAt time.Time, msatsReceived int) error {
|
||||||
if _, err := db.Exec("UPDATE invoices SET confirmed_at = $2, msats_received = $3 WHERE hash = $1", hash, confirmedAt, msatsReceived); err != nil {
|
if _, err := db.Exec("UPDATE invoices SET confirmed_at = $2, msats_received = $3 WHERE hash = $1", hash, confirmedAt, msatsReceived); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
func CreateLNAuth(lnAuth *LNAuth) error {
|
func (db *DB) CreateLNAuth(lnAuth *LNAuth) error {
|
||||||
err := db.QueryRow(
|
err := db.QueryRow(
|
||||||
"INSERT INTO lnauth(k1, lnurl) VALUES($1, $2) RETURNING session_id",
|
"INSERT INTO lnauth(k1, lnurl) VALUES($1, $2) RETURNING session_id",
|
||||||
lnAuth.K1, lnAuth.LNURL).Scan(&lnAuth.SessionId)
|
lnAuth.K1, lnAuth.LNURL).Scan(&lnAuth.SessionId)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func FetchSessionId(k1 string, sessionId *string) error {
|
func (db *DB) FetchSessionId(k1 string, sessionId *string) error {
|
||||||
err := db.QueryRow("SELECT session_id FROM lnauth WHERE k1 = $1", k1).Scan(sessionId)
|
err := db.QueryRow("SELECT session_id FROM lnauth WHERE k1 = $1", k1).Scan(sessionId)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteLNAuth(lnAuth *LNAuth) error {
|
func (db *DB) DeleteLNAuth(lnAuth *LNAuth) error {
|
||||||
_, err := db.Exec("DELETE FROM lnauth WHERE k1 = $1", lnAuth.K1)
|
_, err := db.Exec("DELETE FROM lnauth WHERE k1 = $1", lnAuth.K1)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
44
db/market.go
@ -2,31 +2,53 @@ package db
|
|||||||
|
|
||||||
import "database/sql"
|
import "database/sql"
|
||||||
|
|
||||||
func FetchMarket(marketId int, market *Market) error {
|
type FetchOrdersWhere struct {
|
||||||
if err := db.QueryRow("SELECT id, description FROM markets WHERE id = $1", marketId).Scan(&market.Id, &market.Description); err != nil {
|
MarketId int
|
||||||
|
Pubkey string
|
||||||
|
Confirmed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) CreateMarket(market *Market) error {
|
||||||
|
if err := db.QueryRow(""+
|
||||||
|
"INSERT INTO markets(description, end_date, invoice_id) "+
|
||||||
|
"VALUES($1, $2, $3) "+
|
||||||
|
"RETURNING id", market.Description, market.EndDate, market.InvoiceId).Scan(&market.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// For now, we only support binary markets.
|
||||||
|
if _, err := db.Exec("INSERT INTO shares(market_id, description) VALUES ($1, 'YES'), ($1, 'NO')", market.Id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FetchActiveMarkets(markets *[]Market) error {
|
func (db *DB) FetchMarket(marketId int, market *Market) error {
|
||||||
|
if err := db.QueryRow("SELECT id, description, end_date FROM markets WHERE id = $1", marketId).Scan(&market.Id, &market.Description, &market.EndDate); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) FetchActiveMarkets(markets *[]Market) error {
|
||||||
var (
|
var (
|
||||||
rows *sql.Rows
|
rows *sql.Rows
|
||||||
market Market
|
market Market
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if rows, err = db.Query("SELECT id, description, active FROM markets WHERE active = true"); err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
rows.Scan(&market.Id, &market.Description, &market.Active)
|
rows.Scan(&market.Id, &market.Description, &market.EndDate)
|
||||||
*markets = append(*markets, market)
|
*markets = append(*markets, market)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FetchShares(marketId int, shares *[]Share) error {
|
func (db *DB) FetchShares(marketId int, shares *[]Share) error {
|
||||||
rows, err := db.Query("SELECT id, market_id, description FROM shares WHERE market_id = $1 ORDER BY description DESC", marketId)
|
rows, err := db.Query("SELECT id, market_id, description FROM shares WHERE market_id = $1 ORDER BY description DESC", marketId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -40,13 +62,11 @@ func FetchShares(marketId int, shares *[]Share) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type FetchOrdersWhere struct {
|
func (db *DB) FetchShare(shareId string, share *Share) error {
|
||||||
MarketId int
|
return db.QueryRow("SELECT id, market_id, description FROM shares WHERE id = $1", shareId).Scan(&share.Id, &share.MarketId, &share.Description)
|
||||||
Pubkey string
|
|
||||||
Confirmed bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func FetchOrders(where *FetchOrdersWhere, orders *[]Order) error {
|
func (db *DB) FetchOrders(where *FetchOrdersWhere, orders *[]Order) error {
|
||||||
query := "" +
|
query := "" +
|
||||||
"SELECT o.id, share_id, o.pubkey, o.side, o.quantity, o.price, o.invoice_id, o.created_at, s.description, s.market_id, i.confirmed_at " +
|
"SELECT o.id, share_id, o.pubkey, o.side, o.quantity, o.price, o.invoice_id, o.created_at, s.description, s.market_id, i.confirmed_at " +
|
||||||
"FROM orders o " +
|
"FROM orders o " +
|
||||||
@ -79,7 +99,7 @@ func FetchOrders(where *FetchOrdersWhere, orders *[]Order) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateOrder(order *Order) error {
|
func (db *DB) CreateOrder(order *Order) error {
|
||||||
if _, err := db.Exec(""+
|
if _, err := db.Exec(""+
|
||||||
"INSERT INTO orders(share_id, pubkey, side, quantity, price, invoice_id) "+
|
"INSERT INTO orders(share_id, pubkey, side, quantity, price, invoice_id) "+
|
||||||
"VALUES ($1, $2, $3, $4, $5, $6)",
|
"VALUES ($1, $2, $3, $4, $5, $6)",
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
func CreateSession(s *Session) error {
|
func (db *DB) CreateSession(s *Session) error {
|
||||||
_, err := db.Exec("INSERT INTO sessions(pubkey, session_id) VALUES($1, $2)", s.Pubkey, s.SessionId)
|
_, err := db.Exec("INSERT INTO sessions(pubkey, session_id) VALUES($1, $2)", s.Pubkey, s.SessionId)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func FetchSession(s *Session) error {
|
func (db *DB) FetchSession(s *Session) error {
|
||||||
err := db.QueryRow("SELECT pubkey FROM sessions WHERE session_id = $1", s.SessionId).Scan(&s.Pubkey)
|
err := db.QueryRow("SELECT pubkey FROM sessions WHERE session_id = $1", s.SessionId).Scan(&s.Pubkey)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteSession(s *Session) error {
|
func (db *DB) DeleteSession(s *Session) error {
|
||||||
_, err := db.Exec("DELETE FROM sessions where session_id = $1", s.SessionId)
|
_, err := db.Exec("DELETE FROM sessions where session_id = $1", s.SessionId)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
45
db/types.go
@ -6,39 +6,35 @@ import (
|
|||||||
"gopkg.in/guregu/null.v4"
|
"gopkg.in/guregu/null.v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Serial = int
|
type (
|
||||||
type UUID = string
|
Serial = int
|
||||||
|
UUID = string
|
||||||
type LNAuth struct {
|
LNAuth struct {
|
||||||
K1 string
|
K1 string
|
||||||
LNURL string
|
LNURL string
|
||||||
CreatdAt time.Time
|
CreatedAt time.Time
|
||||||
SessionId string
|
SessionId string
|
||||||
}
|
}
|
||||||
|
User struct {
|
||||||
type User struct {
|
|
||||||
Pubkey string
|
Pubkey string
|
||||||
LastSeen time.Time
|
LastSeen time.Time
|
||||||
}
|
}
|
||||||
|
Session struct {
|
||||||
type Session struct {
|
|
||||||
Pubkey string
|
Pubkey string
|
||||||
SessionId string
|
SessionId string
|
||||||
}
|
}
|
||||||
|
Market struct {
|
||||||
type Market struct {
|
Id Serial `json:"id"`
|
||||||
Id Serial
|
Description string `json:"description"`
|
||||||
Description string
|
EndDate time.Time `json:"endDate"`
|
||||||
Active bool
|
InvoiceId UUID
|
||||||
}
|
}
|
||||||
|
Share struct {
|
||||||
type Share struct {
|
|
||||||
Id UUID
|
Id UUID
|
||||||
MarketId int
|
MarketId int
|
||||||
Description string
|
Description string
|
||||||
}
|
}
|
||||||
|
Invoice struct {
|
||||||
type Invoice struct {
|
|
||||||
Id UUID
|
Id UUID
|
||||||
Pubkey string
|
Pubkey string
|
||||||
Msats int64
|
Msats int64
|
||||||
@ -50,17 +46,18 @@ type Invoice struct {
|
|||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
ConfirmedAt null.Time
|
ConfirmedAt null.Time
|
||||||
HeldSince null.Time
|
HeldSince null.Time
|
||||||
|
Description string
|
||||||
}
|
}
|
||||||
|
Order struct {
|
||||||
type Order struct {
|
|
||||||
Id UUID
|
Id UUID
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
ShareId string `form:"share_id"`
|
ShareId string `json:"sid"`
|
||||||
Share
|
Share
|
||||||
Pubkey string
|
Pubkey string
|
||||||
Side string `form:"side"`
|
Side string `json:"side"`
|
||||||
Quantity int64 `form:"quantity"`
|
Quantity int64 `json:"quantity"`
|
||||||
Price int64 `form:"price"`
|
Price int64 `json:"price"`
|
||||||
InvoiceId UUID
|
InvoiceId UUID
|
||||||
Invoice
|
Invoice
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
func CreateUser(u *User) error {
|
func (db *DB) CreateUser(u *User) error {
|
||||||
_, err := db.Exec(
|
_, err := db.Exec(
|
||||||
"INSERT INTO users(pubkey) VALUES ($1) ON CONFLICT(pubkey) DO UPDATE SET last_seen = CURRENT_TIMESTAMP",
|
"INSERT INTO users(pubkey) VALUES ($1) ON CONFLICT(pubkey) DO UPDATE SET last_seen = CURRENT_TIMESTAMP",
|
||||||
u.Pubkey)
|
u.Pubkey)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateUser(u *User) error {
|
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)
|
_, err := db.Exec("UPDATE users SET last_seen = $1 WHERE pubkey = $2", u.LastSeen, u.Pubkey)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
10
env/env.go
vendored
@ -19,13 +19,17 @@ var (
|
|||||||
Version string
|
Version string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func Load(filenames ...string) error {
|
||||||
if err := godotenv.Load(); err != nil {
|
if err := godotenv.Load(); err != nil {
|
||||||
log.Fatalf("error loading env vars: %s", err)
|
return err
|
||||||
}
|
}
|
||||||
flag.StringVar(&PublicURL, "PUBLIC_URL", "delphi.market", "Public URL of website")
|
flag.StringVar(&PublicURL, "PUBLIC_URL", "delphi.market", "Public URL of website")
|
||||||
flag.IntVar(&Port, "PORT", 4321, "Server port")
|
flag.IntVar(&Port, "PORT", 4321, "Server port")
|
||||||
flag.StringVar(&Env, "ENV", "development", "Specify for which environment files should be built")
|
flag.StringVar(&Env, "ENV", "development", "Specify environment")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Parse() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
CommitLongSha = execCmd("git", "rev-parse", "HEAD")
|
CommitLongSha = execCmd("git", "rev-parse", "HEAD")
|
||||||
CommitShortSha = execCmd("git", "rev-parse", "--short", "HEAD")
|
CommitShortSha = execCmd("git", "rev-parse", "--short", "HEAD")
|
||||||
|
176
go.mod
@ -5,15 +5,16 @@ go 1.20
|
|||||||
replace git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e => gitlab.com/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e
|
replace git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e => gitlab.com/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.2.2
|
github.com/btcsuite/btcd/btcec/v2 v2.3.2
|
||||||
github.com/btcsuite/btcutil v1.0.2
|
github.com/btcsuite/btcutil v1.0.2
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/labstack/echo/v4 v4.11.1
|
github.com/labstack/echo/v4 v4.11.1
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
github.com/lightninglabs/lndclient v1.0.0
|
github.com/lightninglabs/lndclient v0.16.0-11
|
||||||
github.com/lightningnetwork/lnd v0.10.0-beta.rc6.0.20200615174244-103c59a4889f
|
github.com/lightningnetwork/lnd v0.16.0-beta
|
||||||
github.com/namsral/flag v1.7.4-pre
|
github.com/namsral/flag v1.7.4-pre
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
|
github.com/stretchr/testify v1.8.1
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
|
||||||
gopkg.in/guregu/null.v4 v4.0.0
|
gopkg.in/guregu/null.v4 v4.0.0
|
||||||
)
|
)
|
||||||
@ -21,82 +22,161 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
|
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
|
||||||
github.com/aead/siphash v1.0.1 // indirect
|
github.com/aead/siphash v1.0.1 // indirect
|
||||||
github.com/beorn7/perks v1.0.0 // indirect
|
github.com/andybalholm/brotli v1.0.3 // indirect
|
||||||
github.com/btcsuite/btcd v0.20.1-beta.0.20200515232429-9f0179fd2c46 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/btcsuite/btcd v0.23.5-0.20230125025938-be056b0a0b2f // indirect
|
||||||
|
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
|
||||||
|
github.com/btcsuite/btcd/btcutil/psbt v1.1.5 // indirect
|
||||||
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect
|
||||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
||||||
github.com/btcsuite/btcutil/psbt v1.0.2 // indirect
|
github.com/btcsuite/btcutil/psbt v1.0.2 // indirect
|
||||||
github.com/btcsuite/btcwallet v0.11.1-0.20200604005347-6390f167e5f8 // indirect
|
github.com/btcsuite/btcwallet v0.16.7 // indirect
|
||||||
github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0 // indirect
|
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2 // indirect
|
||||||
github.com/btcsuite/btcwallet/wallet/txrules v1.0.0 // indirect
|
github.com/btcsuite/btcwallet/wallet/txrules v1.2.0 // indirect
|
||||||
github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0 // indirect
|
github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3 // indirect
|
||||||
github.com/btcsuite/btcwallet/walletdb v1.3.1 // indirect
|
github.com/btcsuite/btcwallet/walletdb v1.4.0 // indirect
|
||||||
github.com/btcsuite/btcwallet/wtxmgr v1.1.1-0.20200604005347-6390f167e5f8 // indirect
|
github.com/btcsuite/btcwallet/wtxmgr v1.5.0 // indirect
|
||||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
|
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
|
||||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
|
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
|
||||||
|
github.com/btcsuite/winsvc v1.0.0 // indirect
|
||||||
|
github.com/cenkalti/backoff/v4 v4.1.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||||
github.com/coreos/bbolt v1.3.3 // indirect
|
github.com/coreos/bbolt v1.3.3 // indirect
|
||||||
github.com/coreos/etcd v3.3.22+incompatible // indirect
|
github.com/coreos/etcd v3.3.22+incompatible // indirect
|
||||||
github.com/coreos/go-semver v0.3.0 // indirect
|
github.com/coreos/go-semver v0.3.0 // indirect
|
||||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
|
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
|
||||||
|
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||||
|
github.com/decred/dcrd/lru v1.0.0 // indirect
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
||||||
|
github.com/dsnet/compress v0.0.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||||
|
github.com/dvyukov/go-fuzz v0.0.0-20210602112143-b1f3d6f4ef4e // indirect
|
||||||
|
github.com/fergusstrange/embedded-postgres v1.10.0 // indirect
|
||||||
|
github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
|
||||||
github.com/go-errors/errors v1.0.1 // indirect
|
github.com/go-errors/errors v1.0.1 // indirect
|
||||||
github.com/gogo/protobuf v1.1.1 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
github.com/golang/protobuf v1.3.2 // indirect
|
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
|
||||||
github.com/google/btree v1.0.0 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/google/uuid v1.1.1 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/gorilla/websocket v1.4.1 // indirect
|
github.com/google/btree v1.0.1 // indirect
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.4.2 // indirect
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.14.3 // indirect
|
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||||
github.com/jonboulle/clockwork v0.1.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0 // indirect
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||||
|
github.com/jackc/pgconn v1.10.0 // indirect
|
||||||
|
github.com/jackc/pgio v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgproto3/v2 v2.1.1 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||||
|
github.com/jackc/pgtype v1.8.1 // indirect
|
||||||
|
github.com/jackc/pgx/v4 v4.13.0 // indirect
|
||||||
|
github.com/jessevdk/go-flags v1.4.0 // indirect
|
||||||
|
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||||
github.com/jrick/logrotate v1.0.0 // indirect
|
github.com/jrick/logrotate v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.9 // indirect
|
github.com/json-iterator/go v1.1.11 // indirect
|
||||||
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 // indirect
|
github.com/juju/loggo v0.0.0-20210728185423-eebad3a902c4 // indirect
|
||||||
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec // indirect
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect
|
github.com/kkdai/bstream v1.0.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.13.6 // indirect
|
||||||
|
github.com/klauspost/pgzip v1.2.5 // indirect
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
|
||||||
github.com/labstack/gommon v0.4.0 // indirect
|
github.com/labstack/gommon v0.4.0 // indirect
|
||||||
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
|
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
|
||||||
github.com/lightninglabs/neutrino v0.11.1-0.20200316235139-bffc52e8f200 // indirect
|
github.com/lightninglabs/neutrino v0.15.0 // indirect
|
||||||
github.com/lightningnetwork/lightning-onion v1.0.2-0.20200501022730-3c8c8d0b89ea // indirect
|
github.com/lightninglabs/neutrino/cache v1.1.1 // indirect
|
||||||
github.com/lightningnetwork/lnd/clock v1.0.1 // indirect
|
github.com/lightningnetwork/lightning-onion v1.2.1-0.20221202012345-ca23184850a1 // indirect
|
||||||
github.com/lightningnetwork/lnd/queue v1.0.4 // indirect
|
github.com/lightningnetwork/lnd/clock v1.1.0 // indirect
|
||||||
github.com/lightningnetwork/lnd/ticker v1.0.0 // indirect
|
github.com/lightningnetwork/lnd/healthcheck v1.2.2 // indirect
|
||||||
|
github.com/lightningnetwork/lnd/kvdb v1.4.1 // indirect
|
||||||
|
github.com/lightningnetwork/lnd/queue v1.1.0 // indirect
|
||||||
|
github.com/lightningnetwork/lnd/ticker v1.1.0 // indirect
|
||||||
|
github.com/lightningnetwork/lnd/tlv v1.1.0 // indirect
|
||||||
|
github.com/lightningnetwork/lnd/tor v1.1.0 // indirect
|
||||||
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 // indirect
|
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||||
github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8 // indirect
|
github.com/mholt/archiver/v3 v3.5.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
github.com/miekg/dns v1.1.43 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||||
github.com/prometheus/client_golang v0.9.3 // indirect
|
github.com/nwaples/rardecode v1.1.2 // indirect
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 // indirect
|
github.com/pierrec/lz4/v4 v4.1.8 // indirect
|
||||||
github.com/prometheus/common v0.4.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 // indirect
|
github.com/prometheus/client_golang v1.11.1 // indirect
|
||||||
|
github.com/prometheus/client_model v0.2.0 // indirect
|
||||||
|
github.com/prometheus/common v0.26.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.6.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||||
github.com/rogpeppe/fastuuid v1.2.0 // indirect
|
github.com/rogpeppe/fastuuid v1.2.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.2.0 // indirect
|
github.com/sirupsen/logrus v1.7.0 // indirect
|
||||||
github.com/soheilhy/cmux v0.1.4 // indirect
|
github.com/soheilhy/cmux v0.1.5 // indirect
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.0 // indirect
|
||||||
|
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
|
||||||
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
|
||||||
|
github.com/ulikunitz/xz v0.5.10 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
|
||||||
go.uber.org/atomic v1.6.0 // indirect
|
go.etcd.io/bbolt v1.3.6 // indirect
|
||||||
go.uber.org/multierr v1.5.0 // indirect
|
go.etcd.io/etcd/api/v3 v3.5.7 // indirect
|
||||||
go.uber.org/zap v1.14.1 // indirect
|
go.etcd.io/etcd/client/pkg/v3 v3.5.7 // indirect
|
||||||
golang.org/x/crypto v0.11.0 // indirect
|
go.etcd.io/etcd/client/v2 v2.305.7 // indirect
|
||||||
golang.org/x/net v0.12.0 // indirect
|
go.etcd.io/etcd/client/v3 v3.5.7 // indirect
|
||||||
|
go.etcd.io/etcd/pkg/v3 v3.5.7 // indirect
|
||||||
|
go.etcd.io/etcd/raft/v3 v3.5.7 // indirect
|
||||||
|
go.etcd.io/etcd/server/v3 v3.5.7 // indirect
|
||||||
|
go.opentelemetry.io/contrib v0.20.0 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.25.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.0.1 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp v0.20.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.0.1 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.0.1 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v0.20.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk v1.0.1 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk/export/metric v0.20.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v0.20.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.0.1 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v0.9.0 // indirect
|
||||||
|
go.uber.org/atomic v1.7.0 // indirect
|
||||||
|
go.uber.org/multierr v1.6.0 // indirect
|
||||||
|
go.uber.org/zap v1.17.0 // indirect
|
||||||
|
golang.org/x/crypto v0.13.0 // indirect
|
||||||
|
golang.org/x/mod v0.12.0 // indirect
|
||||||
|
golang.org/x/net v0.15.0 // indirect
|
||||||
golang.org/x/sys v0.12.0 // indirect
|
golang.org/x/sys v0.12.0 // indirect
|
||||||
golang.org/x/term v0.10.0 // indirect
|
golang.org/x/term v0.12.0 // indirect
|
||||||
golang.org/x/text v0.11.0 // indirect
|
golang.org/x/text v0.13.0 // indirect
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/time v0.3.0 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c // indirect
|
golang.org/x/tools v0.13.0 // indirect
|
||||||
google.golang.org/grpc v1.24.0 // indirect
|
google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced // indirect
|
||||||
|
google.golang.org/grpc v1.41.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.27.1 // indirect
|
||||||
gopkg.in/errgo.v1 v1.0.1 // indirect
|
gopkg.in/errgo.v1 v1.0.1 // indirect
|
||||||
gopkg.in/macaroon-bakery.v2 v2.0.1 // indirect
|
gopkg.in/macaroon-bakery.v2 v2.0.1 // indirect
|
||||||
gopkg.in/macaroon.v2 v2.1.0 // indirect
|
gopkg.in/macaroon.v2 v2.1.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.2.3 // indirect
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||||
sigs.k8s.io/yaml v1.1.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
lukechampine.com/uint128 v1.2.0 // indirect
|
||||||
|
modernc.org/cc/v3 v3.40.0 // indirect
|
||||||
|
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||||
|
modernc.org/libc v1.22.2 // indirect
|
||||||
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
|
modernc.org/memory v1.4.0 // indirect
|
||||||
|
modernc.org/opt v0.1.3 // indirect
|
||||||
|
modernc.org/sqlite v1.20.3 // indirect
|
||||||
|
modernc.org/strutil v1.1.3 // indirect
|
||||||
|
modernc.org/token v1.0.1 // indirect
|
||||||
|
sigs.k8s.io/yaml v1.2.0 // indirect
|
||||||
)
|
)
|
||||||
|
0
hotreload.sh
Normal file → Executable file
@ -12,7 +12,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateInvoice(pubkey string, msats int64) (*db.Invoice, error) {
|
func (lnd *LNDClient) CreateInvoice(d *db.DB, pubkey string, msats int64, description string) (*db.Invoice, error) {
|
||||||
var (
|
var (
|
||||||
expiry time.Duration = time.Hour
|
expiry time.Duration = time.Hour
|
||||||
preimage lntypes.Preimage
|
preimage lntypes.Preimage
|
||||||
@ -44,14 +44,15 @@ func CreateInvoice(pubkey string, msats int64) (*db.Invoice, error) {
|
|||||||
Hash: hash.String(),
|
Hash: hash.String(),
|
||||||
CreatedAt: lnInvoice.CreationDate,
|
CreatedAt: lnInvoice.CreationDate,
|
||||||
ExpiresAt: lnInvoice.CreationDate.Add(expiry),
|
ExpiresAt: lnInvoice.CreationDate.Add(expiry),
|
||||||
|
Description: description,
|
||||||
}
|
}
|
||||||
if err := db.CreateInvoice(dbInvoice); err != nil {
|
if err := d.CreateInvoice(dbInvoice); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return dbInvoice, nil
|
return dbInvoice, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CheckInvoice(hash lntypes.Hash) {
|
func (lnd *LNDClient) CheckInvoice(d *db.DB, hash lntypes.Hash) {
|
||||||
var (
|
var (
|
||||||
pollInterval = 5 * time.Second
|
pollInterval = 5 * time.Second
|
||||||
invoice db.Invoice
|
invoice db.Invoice
|
||||||
@ -60,12 +61,7 @@ func CheckInvoice(hash lntypes.Hash) {
|
|||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if !Enabled {
|
if err = d.FetchInvoice(&db.FetchInvoiceWhere{Hash: hash.String()}, &invoice); err != nil {
|
||||||
log.Printf("LND disabled, skipping checking invoice: hash=%s", hash)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = db.FetchInvoice(&db.FetchInvoiceWhere{Hash: hash.String()}, &invoice); err != nil {
|
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -100,7 +96,7 @@ func CheckInvoice(hash lntypes.Hash) {
|
|||||||
handleLoopError(err)
|
handleLoopError(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err = db.ConfirmInvoice(hash.String(), time.Now(), int(lnInvoice.AmountPaid)); err != nil {
|
if err = d.ConfirmInvoice(hash.String(), time.Now(), int(lnInvoice.AmountPaid)); err != nil {
|
||||||
handleLoopError(err)
|
handleLoopError(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -110,3 +106,21 @@ func CheckInvoice(hash lntypes.Hash) {
|
|||||||
time.Sleep(pollInterval)
|
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
|
||||||
|
}
|
||||||
|
44
lnd/lnd.go
@ -3,47 +3,25 @@ package lnd
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
"github.com/lightninglabs/lndclient"
|
"github.com/lightninglabs/lndclient"
|
||||||
"github.com/namsral/flag"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
lnd *LNDClient
|
|
||||||
Enabled bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type LNDClient struct {
|
type LNDClient struct {
|
||||||
lndclient.GrpcLndServices
|
*lndclient.GrpcLndServices
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
type LNDConfig = lndclient.LndServicesConfig
|
||||||
|
|
||||||
|
func New(config *LNDConfig) (*LNDClient, error) {
|
||||||
var (
|
var (
|
||||||
lndCert string
|
rcpLndServices *lndclient.GrpcLndServices
|
||||||
lndMacaroonDir string
|
lnd *LNDClient
|
||||||
lndHost string
|
|
||||||
rpcLndServices *lndclient.GrpcLndServices
|
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if err = godotenv.Load(); err != nil {
|
if rcpLndServices, err = lndclient.NewLndServices(config); err != nil {
|
||||||
log.Fatalf("error loading env vars: %s", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
flag.StringVar(&lndCert, "LND_CERT", "", "Path to LND TLS certificate")
|
lnd = &LNDClient{GrpcLndServices: rcpLndServices}
|
||||||
flag.StringVar(&lndMacaroonDir, "LND_MACAROON_DIR", "", "LND macaroon directory")
|
log.Printf("Connected to %s running LND v%s", config.LndAddress, lnd.Version.Version)
|
||||||
flag.StringVar(&lndHost, "LND_HOST", "localhost:10001", "LND gRPC server address")
|
return lnd, nil
|
||||||
flag.Parse()
|
|
||||||
if rpcLndServices, err = lndclient.NewLndServices(&lndclient.LndServicesConfig{
|
|
||||||
LndAddress: lndHost,
|
|
||||||
MacaroonDir: lndMacaroonDir,
|
|
||||||
TLSPath: lndCert,
|
|
||||||
// TODO: make network configurable
|
|
||||||
Network: lndclient.NetworkRegtest,
|
|
||||||
}); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
Enabled = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lnd = &LNDClient{GrpcLndServices: *rpcLndServices}
|
|
||||||
log.Printf("Connected to %s running LND v%s", lndHost, lnd.Version.Version)
|
|
||||||
Enabled = true
|
|
||||||
}
|
}
|
||||||
|
65
main.go
@ -5,8 +5,13 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/db"
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/env"
|
"git.ekzyis.com/ekzyis/delphi.market/env"
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/lnd"
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server"
|
"git.ekzyis.com/ekzyis/delphi.market/server"
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/server/router"
|
||||||
|
"github.com/lightninglabs/lndclient"
|
||||||
|
"github.com/namsral/flag"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -14,11 +19,69 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
var (
|
||||||
|
dbUrl string
|
||||||
|
lndAddress string
|
||||||
|
lndCert string
|
||||||
|
lndMacaroonDir string
|
||||||
|
lndNetwork string
|
||||||
|
db_ *db.DB
|
||||||
|
lnd_ *lnd.LNDClient
|
||||||
|
ctx router.ServerContext
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if err = env.Load(); err != nil {
|
||||||
|
log.Fatalf("error loading env vars: %v", err)
|
||||||
|
}
|
||||||
|
flag.StringVar(&dbUrl, "DATABASE_URL", "delphi.market", "Public URL of website")
|
||||||
|
flag.StringVar(&lndAddress, "LND_ADDRESS", "localhost:10001", "LND gRPC server address")
|
||||||
|
flag.StringVar(&lndCert, "LND_CERT", "", "Path to LND TLS certificate")
|
||||||
|
flag.StringVar(&lndMacaroonDir, "LND_MACAROON_DIR", "", "LND macaroon directory")
|
||||||
|
flag.StringVar(&lndNetwork, "LND_NETWORK", "regtest", "LND network")
|
||||||
|
env.Parse()
|
||||||
|
figlet()
|
||||||
log.Printf("Commit: %s", env.CommitShortSha)
|
log.Printf("Commit: %s", env.CommitShortSha)
|
||||||
log.Printf("Public URL: %s", env.PublicURL)
|
log.Printf("Public URL: %s", env.PublicURL)
|
||||||
log.Printf("Environment: %s", env.Env)
|
log.Printf("Environment: %s", env.Env)
|
||||||
|
|
||||||
s = server.NewServer()
|
if db_, err = db.New(dbUrl); err != nil {
|
||||||
|
log.Fatalf("error connecting to database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if lnd_, err = lnd.New(&lnd.LNDConfig{
|
||||||
|
LndAddress: lndAddress,
|
||||||
|
TLSPath: lndCert,
|
||||||
|
MacaroonDir: lndMacaroonDir,
|
||||||
|
Network: lndclient.Network(lndNetwork),
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("[warn] error connecting to LND: %v\n", err)
|
||||||
|
lnd_ = nil
|
||||||
|
} else {
|
||||||
|
lnd_.CheckInvoices(db_)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = server.ServerContext{
|
||||||
|
PublicURL: env.PublicURL,
|
||||||
|
CommitShortSha: env.CommitShortSha,
|
||||||
|
CommitLongSha: env.CommitLongSha,
|
||||||
|
Version: env.Version,
|
||||||
|
Db: db_,
|
||||||
|
Lnd: lnd_,
|
||||||
|
}
|
||||||
|
s = server.New(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func figlet() {
|
||||||
|
log.Println(
|
||||||
|
"\n" +
|
||||||
|
" _ _ _ _ \n" +
|
||||||
|
" __| | ___| |_ __ | |__ (_)\n" +
|
||||||
|
" / _` |/ _ \\ | '_ \\| '_ \\| |\n" +
|
||||||
|
"| (_| | __/ | |_) | | | | |\n" +
|
||||||
|
" \\__,_|\\___|_| .__/|_| |_|_|\n" +
|
||||||
|
" |_| .market \n" +
|
||||||
|
"----------------------------",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
17
nginx.conf
@ -1,7 +1,11 @@
|
|||||||
upstream delphi {
|
upstream delphi-prod-backend {
|
||||||
server 127.0.0.1:4321;
|
server 127.0.0.1:4321;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
upstream delphi-prod-frontend {
|
||||||
|
server 127.0.0.1:4173;
|
||||||
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
server_name delphi.market;
|
server_name delphi.market;
|
||||||
listen 80;
|
listen 80;
|
||||||
@ -25,7 +29,16 @@ server {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header X-Forwarded-Host $host;
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
proxy_set_header X-Forwarded-Prefix /;
|
proxy_set_header X-Forwarded-Prefix /;
|
||||||
proxy_pass http://delphi$request_uri;
|
proxy_pass http://delphi-prod-frontend$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Prefix /;
|
||||||
|
proxy_pass http://delphi-prod-backend$request_uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
include letsencrypt.conf;
|
include letsencrypt.conf;
|
||||||
|
56
nginx.dev.conf
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
upstream delphi-dev-backend {
|
||||||
|
server 127.0.0.1:4322;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream delphi-dev-frontend {
|
||||||
|
server 127.0.0.1:4323;
|
||||||
|
}
|
||||||
|
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
server_name dev1.delphi.market;
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
|
||||||
|
return 301 https://dev1.delphi.market$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
server_name dev1.delphi.market;
|
||||||
|
listen 443;
|
||||||
|
listen [::]:443;
|
||||||
|
|
||||||
|
ssl on;
|
||||||
|
ssl_certificate /etc/letsencrypt/live/dev1.delphi.market/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/dev1.delphi.market/privkey.pem;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Prefix /;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_pass http://delphi-dev-frontend$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Prefix /;
|
||||||
|
proxy_pass http://delphi-dev-backend$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /hotreload {
|
||||||
|
root /var/www/dev1.delphi;
|
||||||
|
}
|
||||||
|
|
||||||
|
include letsencrypt.conf;
|
||||||
|
}
|
@ -30,7 +30,7 @@ func NewLNAuth() (*LNAuth, error) {
|
|||||||
return nil, fmt.Errorf("rand.Read error: %w", err)
|
return nil, fmt.Errorf("rand.Read error: %w", err)
|
||||||
}
|
}
|
||||||
k1hex := hex.EncodeToString(k1)
|
k1hex := hex.EncodeToString(k1)
|
||||||
url := []byte(fmt.Sprintf("https://%s/api/login?tag=login&k1=%s&action=login", env.PublicURL, k1hex))
|
url := []byte(fmt.Sprintf("https://%s/api/login/callback?tag=login&k1=%s&action=login", env.PublicURL, k1hex))
|
||||||
conv, err := bech32.ConvertBits(url, 8, 5, true)
|
conv, err := bech32.ConvertBits(url, 8, 5, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("bech32.ConvertBits error: %w", err)
|
return nil, fmt.Errorf("bech32.ConvertBits error: %w", err)
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"strings"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
@ -14,32 +13,8 @@ func httpErrorHandler(err error, c echo.Context) {
|
|||||||
if httpError, ok := err.(*echo.HTTPError); ok {
|
if httpError, ok := err.(*echo.HTTPError); ok {
|
||||||
code = httpError.Code
|
code = httpError.Code
|
||||||
}
|
}
|
||||||
filePath := fmt.Sprintf("public/%d.html", code)
|
if strings.Contains(err.Error(), "violates check constraint") {
|
||||||
var f *os.File
|
code = 400
|
||||||
if f, err = os.Open(filePath); err != nil {
|
|
||||||
c.Logger().Error(err)
|
|
||||||
serveError(c, 500)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if err = c.Stream(code, "text/html", f); err != nil {
|
c.JSON(code, map[string]any{"status": code})
|
||||||
c.Logger().Error(err)
|
|
||||||
serveError(c, 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveError(c echo.Context, code int) error {
|
|
||||||
var (
|
|
||||||
f *os.File
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if f, err = os.Open(fmt.Sprintf("public/%d.html", code)); err != nil {
|
|
||||||
c.Logger().Error(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = c.Stream(code, "text/html", f); err != nil {
|
|
||||||
c.Logger().Error(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
34
server/router/context/context.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/db"
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/lnd"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerContext struct {
|
||||||
|
Environment string
|
||||||
|
PublicURL string
|
||||||
|
CommitShortSha string
|
||||||
|
CommitLongSha string
|
||||||
|
Version string
|
||||||
|
Db *db.DB
|
||||||
|
Lnd *lnd.LNDClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *ServerContext) Render(c echo.Context, code int, name string, data map[string]any) error {
|
||||||
|
envVars := map[string]any{
|
||||||
|
"PUBLIC_URL": sc.PublicURL,
|
||||||
|
"COMMIT_SHORT_SHA": sc.CommitShortSha,
|
||||||
|
"COMMIT_LONG_SHA": sc.CommitLongSha,
|
||||||
|
"VERSION": sc.Version,
|
||||||
|
}
|
||||||
|
merge(&data, &envVars)
|
||||||
|
return c.Render(code, name, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func merge[T comparable](target *map[T]any, src *map[T]any) {
|
||||||
|
for k, v := range *src {
|
||||||
|
(*target)[k] = v
|
||||||
|
}
|
||||||
|
}
|
16
server/router/handler/handler_test.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package handler_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
db_ "git.ekzyis.com/ekzyis/delphi.market/db"
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
db *db_.DB
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
test.Main(m, db)
|
||||||
|
}
|
@ -4,25 +4,38 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
"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/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleIndex(envVars map[string]any) echo.HandlerFunc {
|
func HandleIndex(sc context.ServerContext) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
markets []db.Market
|
markets []db.Market
|
||||||
err error
|
err error
|
||||||
data map[string]any
|
data map[string]any
|
||||||
)
|
)
|
||||||
if err = db.FetchActiveMarkets(&markets); err != nil {
|
if err = sc.Db.FetchActiveMarkets(&markets); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
data = map[string]any{
|
data = map[string]any{
|
||||||
"session": c.Get("session"),
|
"session": c.Get("session"),
|
||||||
"markets": markets,
|
"markets": markets,
|
||||||
}
|
}
|
||||||
lib.Merge(&data, &envVars)
|
|
||||||
return c.Render(http.StatusOK, "index.html", data)
|
return sc.Render(c, http.StatusOK, "index.html", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleMarkets(sc context.ServerContext) 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,21 +7,22 @@ import (
|
|||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
"git.ekzyis.com/ekzyis/delphi.market/db"
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/lib"
|
"git.ekzyis.com/ekzyis/delphi.market/lib"
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/lnd"
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleInvoiceAPI(envVars map[string]any) echo.HandlerFunc {
|
func HandleInvoiceStatus(sc context.ServerContext) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
invoiceId string
|
invoiceId string
|
||||||
invoice db.Invoice
|
invoice db.Invoice
|
||||||
u db.User
|
u db.User
|
||||||
|
qr string
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
invoiceId = c.Param("id")
|
invoiceId = c.Param("id")
|
||||||
if err = db.FetchInvoice(&db.FetchInvoiceWhere{Id: invoiceId}, &invoice); err == sql.ErrNoRows {
|
if err = sc.Db.FetchInvoice(&db.FetchInvoiceWhere{Id: invoiceId}, &invoice); err == sql.ErrNoRows {
|
||||||
return echo.NewHTTPError(http.StatusNotFound)
|
return echo.NewHTTPError(http.StatusNotFound)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -29,12 +30,28 @@ func HandleInvoiceAPI(envVars map[string]any) echo.HandlerFunc {
|
|||||||
if u = c.Get("session").(db.User); invoice.Pubkey != u.Pubkey {
|
if u = c.Get("session").(db.User); invoice.Pubkey != u.Pubkey {
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized)
|
return echo.NewHTTPError(http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
|
if qr, err = lib.ToQR(invoice.PaymentRequest); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
invoice.Preimage = ""
|
invoice.Preimage = ""
|
||||||
return c.JSON(http.StatusOK, invoice)
|
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(envVars map[string]any) echo.HandlerFunc {
|
func HandleInvoice(sc context.ServerContext) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
invoiceId string
|
invoiceId string
|
||||||
@ -46,7 +63,7 @@ func HandleInvoice(envVars map[string]any) echo.HandlerFunc {
|
|||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
invoiceId = c.Param("id")
|
invoiceId = c.Param("id")
|
||||||
if err = db.FetchInvoice(&db.FetchInvoiceWhere{Id: invoiceId}, &invoice); err == sql.ErrNoRows {
|
if err = sc.Db.FetchInvoice(&db.FetchInvoiceWhere{Id: invoiceId}, &invoice); err == sql.ErrNoRows {
|
||||||
return echo.NewHTTPError(http.StatusNotFound)
|
return echo.NewHTTPError(http.StatusNotFound)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -57,7 +74,7 @@ func HandleInvoice(envVars map[string]any) echo.HandlerFunc {
|
|||||||
if hash, err = lntypes.MakeHashFromStr(invoice.Hash); err != nil {
|
if hash, err = lntypes.MakeHashFromStr(invoice.Hash); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
go lnd.CheckInvoice(hash)
|
go sc.Lnd.CheckInvoice(sc.Db, hash)
|
||||||
if qr, err = lib.ToQR(invoice.PaymentRequest); err != nil {
|
if qr, err = lib.ToQR(invoice.PaymentRequest); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -73,6 +90,6 @@ func HandleInvoice(envVars map[string]any) echo.HandlerFunc {
|
|||||||
"lnurl": invoice.PaymentRequest,
|
"lnurl": invoice.PaymentRequest,
|
||||||
"qr": qr,
|
"qr": qr,
|
||||||
}
|
}
|
||||||
return c.Render(http.StatusOK, "invoice.html", data)
|
return sc.Render(c, http.StatusOK, "invoice.html", data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,10 +8,11 @@ import (
|
|||||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
"git.ekzyis.com/ekzyis/delphi.market/db"
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/lib"
|
"git.ekzyis.com/ekzyis/delphi.market/lib"
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/auth"
|
"git.ekzyis.com/ekzyis/delphi.market/server/auth"
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleLogin(envVars map[string]any) echo.HandlerFunc {
|
func HandleLogin(sc context.ServerContext) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
lnAuth *auth.LNAuth
|
lnAuth *auth.LNAuth
|
||||||
@ -25,7 +26,7 @@ func HandleLogin(envVars map[string]any) echo.HandlerFunc {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dbLnAuth = db.LNAuth{K1: lnAuth.K1, LNURL: lnAuth.LNURL}
|
dbLnAuth = db.LNAuth{K1: lnAuth.K1, LNURL: lnAuth.LNURL}
|
||||||
if err = db.CreateLNAuth(&dbLnAuth); err != nil {
|
if err = sc.Db.CreateLNAuth(&dbLnAuth); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.SetCookie(&http.Cookie{Name: "session", HttpOnly: true, Path: "/", Value: dbLnAuth.SessionId, Secure: true, Expires: expires})
|
c.SetCookie(&http.Cookie{Name: "session", HttpOnly: true, Path: "/", Value: dbLnAuth.SessionId, Secure: true, Expires: expires})
|
||||||
@ -33,16 +34,14 @@ func HandleLogin(envVars map[string]any) echo.HandlerFunc {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
data = map[string]any{
|
data = map[string]any{
|
||||||
"session": c.Get("session"),
|
|
||||||
"lnurl": lnAuth.LNURL,
|
"lnurl": lnAuth.LNURL,
|
||||||
"qr": qr,
|
"qr": qr,
|
||||||
}
|
}
|
||||||
lib.Merge(&data, &envVars)
|
return c.JSON(http.StatusOK, data)
|
||||||
return c.Render(http.StatusOK, "login.html", data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleLoginCallback(envVars map[string]any) echo.HandlerFunc {
|
func HandleLoginCallback(sc context.ServerContext) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
query auth.LNAuthResponse
|
query auth.LNAuthResponse
|
||||||
@ -52,7 +51,7 @@ func HandleLoginCallback(envVars map[string]any) echo.HandlerFunc {
|
|||||||
if err := c.Bind(&query); err != nil {
|
if err := c.Bind(&query); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest)
|
return echo.NewHTTPError(http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
if err = db.FetchSessionId(query.K1, &sessionId); err == sql.ErrNoRows {
|
if err = sc.Db.FetchSessionId(query.K1, &sessionId); err == sql.ErrNoRows {
|
||||||
return echo.NewHTTPError(http.StatusNotFound, map[string]string{"reason": "session not found"})
|
return echo.NewHTTPError(http.StatusNotFound, map[string]string{"reason": "session not found"})
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -62,13 +61,13 @@ func HandleLoginCallback(envVars map[string]any) echo.HandlerFunc {
|
|||||||
} else if !ok {
|
} else if !ok {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, map[string]string{"reason": "bad signature"})
|
return echo.NewHTTPError(http.StatusBadRequest, map[string]string{"reason": "bad signature"})
|
||||||
}
|
}
|
||||||
if err = db.CreateUser(&db.User{Pubkey: query.Key}); err != nil {
|
if err = sc.Db.CreateUser(&db.User{Pubkey: query.Key}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = db.CreateSession(&db.Session{Pubkey: query.Key, SessionId: sessionId}); err != nil {
|
if err = sc.Db.CreateSession(&db.Session{Pubkey: query.Key, SessionId: sessionId}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = db.DeleteLNAuth(&db.LNAuth{K1: query.K1}); err != nil {
|
if err = sc.Db.DeleteLNAuth(&db.LNAuth{K1: query.K1}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return c.JSON(http.StatusOK, map[string]string{"status": "OK"})
|
return c.JSON(http.StatusOK, map[string]string{"status": "OK"})
|
||||||
|
123
server/router/handler/login_test.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
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.ServerContext
|
||||||
|
req *http.Request
|
||||||
|
rec *httptest.ResponseRecorder
|
||||||
|
cookies []*http.Cookie
|
||||||
|
sessionId string
|
||||||
|
dbSessionId string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
sc = context.ServerContext{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.ServerContext
|
||||||
|
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.ServerContext{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")
|
||||||
|
}
|
@ -5,10 +5,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
"git.ekzyis.com/ekzyis/delphi.market/db"
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleLogout(envVars map[string]any) echo.HandlerFunc {
|
func HandleLogout(sc context.ServerContext) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
cookie *http.Cookie
|
cookie *http.Cookie
|
||||||
@ -17,14 +18,14 @@ func HandleLogout(envVars map[string]any) echo.HandlerFunc {
|
|||||||
)
|
)
|
||||||
if cookie, err = c.Cookie("session"); err != nil {
|
if cookie, err = c.Cookie("session"); err != nil {
|
||||||
// cookie not found
|
// cookie not found
|
||||||
return c.Redirect(http.StatusSeeOther, "/")
|
return c.JSON(http.StatusNotFound, map[string]string{"reason": "session not found"})
|
||||||
}
|
}
|
||||||
sessionId = cookie.Value
|
sessionId = cookie.Value
|
||||||
if err = db.DeleteSession(&db.Session{SessionId: sessionId}); err != nil {
|
if err = sc.Db.DeleteSession(&db.Session{SessionId: sessionId}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// tell browser that cookie is expired and thus can be deleted
|
// 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()})
|
c.SetCookie(&http.Cookie{Name: "session", HttpOnly: true, Path: "/", Value: sessionId, Secure: true, Expires: time.Now()})
|
||||||
return c.Redirect(http.StatusSeeOther, "/")
|
return c.JSON(http.StatusSeeOther, map[string]string{"status": "OK"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
72
server/router/handler/logout_test.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
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/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 TestLogout(t *testing.T) {
|
||||||
|
var (
|
||||||
|
assert = assert.New(t)
|
||||||
|
e *echo.Echo
|
||||||
|
c echo.Context
|
||||||
|
sc context.ServerContext
|
||||||
|
req *http.Request
|
||||||
|
rec *httptest.ResponseRecorder
|
||||||
|
pk *secp256k1.PublicKey
|
||||||
|
s *db_.Session
|
||||||
|
key string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
sc = context.ServerContext{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")
|
||||||
|
|
||||||
|
}
|
@ -5,16 +5,16 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
"git.ekzyis.com/ekzyis/delphi.market/db"
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/env"
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/lib"
|
"git.ekzyis.com/ekzyis/delphi.market/lib"
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/lnd"
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleMarket(envVars map[string]any) echo.HandlerFunc {
|
func HandleMarket(sc context.ServerContext) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
marketId int64
|
marketId int64
|
||||||
@ -27,53 +27,93 @@ func HandleMarket(envVars map[string]any) echo.HandlerFunc {
|
|||||||
if marketId, err = strconv.ParseInt(c.Param("id"), 10, 64); err != nil {
|
if marketId, err = strconv.ParseInt(c.Param("id"), 10, 64); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
|
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
|
||||||
}
|
}
|
||||||
if err = db.FetchMarket(int(marketId), &market); err == sql.ErrNoRows {
|
if err = sc.Db.FetchMarket(int(marketId), &market); err == sql.ErrNoRows {
|
||||||
return echo.NewHTTPError(http.StatusNotFound, "Not Found")
|
return echo.NewHTTPError(http.StatusNotFound, "Not Found")
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = db.FetchShares(market.Id, &shares); err != nil {
|
if err = sc.Db.FetchShares(market.Id, &shares); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = db.FetchOrders(&db.FetchOrdersWhere{MarketId: market.Id, Confirmed: true}, &orders); err != nil {
|
if err = sc.Db.FetchOrders(&db.FetchOrdersWhere{MarketId: market.Id, Confirmed: true}, &orders); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
data = map[string]any{
|
data = map[string]any{
|
||||||
"session": c.Get("session"),
|
|
||||||
"Id": market.Id,
|
"Id": market.Id,
|
||||||
"Description": market.Description,
|
"Description": market.Description,
|
||||||
// shares are sorted by description in descending order
|
"Shares": shares,
|
||||||
// that's how we know that YES must be the first share
|
|
||||||
"YesShare": shares[0],
|
|
||||||
"NoShare": shares[1],
|
|
||||||
"Orders": orders,
|
|
||||||
}
|
}
|
||||||
lib.Merge(&data, &envVars)
|
return c.JSON(http.StatusOK, data)
|
||||||
return c.Render(http.StatusOK, "market.html", data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandlePostOrder(envVars map[string]any) echo.HandlerFunc {
|
func HandleCreateMarket(sc context.ServerContext) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
marketId string
|
|
||||||
u db.User
|
u db.User
|
||||||
o db.Order
|
m db.Market
|
||||||
invoice *db.Invoice
|
invoice *db.Invoice
|
||||||
msats int64
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
u = c.Get("session").(db.User)
|
||||||
|
msats = 1000
|
||||||
|
// TODO: add [market:<id>] for redirect after payment
|
||||||
|
invDescription = fmt.Sprintf("create market \"%s\" (%s)", m.Description, m.EndDate)
|
||||||
|
if invoice, err = sc.Lnd.CreateInvoice(sc.Db, u.Pubkey, msats, invDescription); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if qr, err = lib.ToQR(invoice.PaymentRequest); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if hash, err = lntypes.MakeHashFromStr(invoice.Hash); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go sc.Lnd.CheckInvoice(sc.Db, hash)
|
||||||
|
|
||||||
|
m.InvoiceId = invoice.Id
|
||||||
|
if err := sc.Db.CreateMarket(&m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data = map[string]any{
|
||||||
|
"id": invoice.Id,
|
||||||
|
"bolt11": invoice.PaymentRequest,
|
||||||
|
"amount": msats,
|
||||||
|
"qr": qr,
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusPaymentRequired, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleOrder(sc context.ServerContext) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
var (
|
||||||
|
u db.User
|
||||||
|
o db.Order
|
||||||
|
s db.Share
|
||||||
|
invoice *db.Invoice
|
||||||
|
msats int64
|
||||||
|
description string
|
||||||
data map[string]any
|
data map[string]any
|
||||||
qr string
|
qr string
|
||||||
hash lntypes.Hash
|
hash lntypes.Hash
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
marketId = c.Param("id")
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// [ ] Step 0: If SELL order, check share balance of user
|
// [ ] If SELL order, check share balance of user
|
||||||
// [x] Create HODL invoice
|
|
||||||
// [x] Create (unconfirmed) order
|
// [x] Create (unconfirmed) order
|
||||||
|
// [x] Create invoice
|
||||||
// [ ] Find matching orders
|
// [ ] Find matching orders
|
||||||
// [ ] Settle invoice when matching order was found,
|
// [ ] show invoice to user
|
||||||
// else cancel invoice if expired
|
|
||||||
|
|
||||||
// parse body
|
// parse body
|
||||||
if err := c.Bind(&o); err != nil {
|
if err := c.Bind(&o); err != nil {
|
||||||
@ -82,11 +122,15 @@ func HandlePostOrder(envVars map[string]any) echo.HandlerFunc {
|
|||||||
u = c.Get("session").(db.User)
|
u = c.Get("session").(db.User)
|
||||||
o.Pubkey = u.Pubkey
|
o.Pubkey = u.Pubkey
|
||||||
msats = o.Quantity * o.Price * 1000
|
msats = o.Quantity * o.Price * 1000
|
||||||
|
if err = sc.Db.FetchShare(o.ShareId, &s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
description = fmt.Sprintf("%s %d %s shares @ %d sats [market:%d]", strings.ToUpper(o.Side), o.Quantity, s.Description, o.Price, s.MarketId)
|
||||||
|
|
||||||
// TODO: if SELL order, check share balance of user
|
// TODO: if SELL order, check share balance of user
|
||||||
|
|
||||||
// Create HODL invoice
|
// Create HODL invoice
|
||||||
if invoice, err = lnd.CreateInvoice(o.Pubkey, msats); err != nil {
|
if invoice, err = sc.Lnd.CreateInvoice(sc.Db, o.Pubkey, msats, description); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Create QR code to pay HODL invoice
|
// Create QR code to pay HODL invoice
|
||||||
@ -97,25 +141,23 @@ func HandlePostOrder(envVars map[string]any) echo.HandlerFunc {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start goroutine to poll status and update invoice in background
|
|
||||||
go lnd.CheckInvoice(hash)
|
|
||||||
|
|
||||||
// Create (unconfirmed) order
|
// Create (unconfirmed) order
|
||||||
o.InvoiceId = invoice.Id
|
o.InvoiceId = invoice.Id
|
||||||
if err := db.CreateOrder(&o); err != nil {
|
if err := sc.Db.CreateOrder(&o); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start goroutine to poll status and update invoice in background
|
||||||
|
go sc.Lnd.CheckInvoice(sc.Db, hash)
|
||||||
|
|
||||||
// TODO: find matching orders
|
// TODO: find matching orders
|
||||||
|
|
||||||
data = map[string]any{
|
data = map[string]any{
|
||||||
"session": c.Get("session"),
|
"id": invoice.Id,
|
||||||
"lnurl": invoice.PaymentRequest,
|
"bolt11": invoice.PaymentRequest,
|
||||||
|
"amount": msats,
|
||||||
"qr": qr,
|
"qr": qr,
|
||||||
"invoice": *invoice,
|
|
||||||
"redirectURL": fmt.Sprintf("https://%s/market/%s", env.PublicURL, marketId),
|
|
||||||
}
|
}
|
||||||
lib.Merge(&data, &envVars)
|
return c.JSON(http.StatusPaymentRequired, data)
|
||||||
return c.Render(http.StatusPaymentRequired, "invoice.html", data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,11 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
"git.ekzyis.com/ekzyis/delphi.market/db"
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleCheckSession(envVars map[string]any) echo.HandlerFunc {
|
func HandleCheckSession(sc context.ServerContext) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
cookie *http.Cookie
|
cookie *http.Cookie
|
||||||
@ -16,13 +17,13 @@ func HandleCheckSession(envVars map[string]any) echo.HandlerFunc {
|
|||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if cookie, err = c.Cookie("session"); err != nil {
|
if cookie, err = c.Cookie("session"); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, map[string]string{"reason": "cookie required"})
|
return c.JSON(http.StatusBadRequest, map[string]string{"reason": "cookie required"})
|
||||||
}
|
}
|
||||||
s = db.Session{SessionId: cookie.Value}
|
s = db.Session{SessionId: cookie.Value}
|
||||||
if err = db.FetchSession(&s); err == sql.ErrNoRows {
|
if err = sc.Db.FetchSession(&s); err == sql.ErrNoRows {
|
||||||
return echo.NewHTTPError(http.StatusNotFound, map[string]string{"reason": "session not found"})
|
return c.JSON(http.StatusBadRequest, map[string]string{"reason": "session not found"})
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError)
|
return c.JSON(http.StatusInternalServerError, nil)
|
||||||
}
|
}
|
||||||
return c.JSON(http.StatusOK, map[string]string{"pubkey": s.Pubkey})
|
return c.JSON(http.StatusOK, map[string]string{"pubkey": s.Pubkey})
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,11 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
"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/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleUser(envVars map[string]any) echo.HandlerFunc {
|
func HandleUser(sc context.ServerContext) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
u db.User
|
u db.User
|
||||||
@ -17,7 +17,7 @@ func HandleUser(envVars map[string]any) echo.HandlerFunc {
|
|||||||
data map[string]any
|
data map[string]any
|
||||||
)
|
)
|
||||||
u = c.Get("session").(db.User)
|
u = c.Get("session").(db.User)
|
||||||
if err = db.FetchOrders(&db.FetchOrdersWhere{Pubkey: u.Pubkey}, &orders); err != nil {
|
if err = sc.Db.FetchOrders(&db.FetchOrdersWhere{Pubkey: u.Pubkey}, &orders); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
data = map[string]any{
|
data = map[string]any{
|
||||||
@ -25,7 +25,6 @@ func HandleUser(envVars map[string]any) echo.HandlerFunc {
|
|||||||
"user": u,
|
"user": u,
|
||||||
"Orders": orders,
|
"Orders": orders,
|
||||||
}
|
}
|
||||||
lib.Merge(&data, &envVars)
|
return sc.Render(c, http.StatusOK, "user.html", data)
|
||||||
return c.Render(http.StatusOK, "user.html", data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,14 +3,14 @@ package middleware
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/lnd"
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LNDGuard(envVars map[string]any) echo.MiddlewareFunc {
|
func LNDGuard(sc context.ServerContext) 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 {
|
||||||
if lnd.Enabled {
|
if sc.Lnd != nil {
|
||||||
return next(c)
|
return next(c)
|
||||||
}
|
}
|
||||||
return echo.NewHTTPError(http.StatusMethodNotAllowed)
|
return echo.NewHTTPError(http.StatusMethodNotAllowed)
|
||||||
|
@ -6,10 +6,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
"git.ekzyis.com/ekzyis/delphi.market/db"
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Session(envVars map[string]any) echo.MiddlewareFunc {
|
func Session(sc context.ServerContext) 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 (
|
var (
|
||||||
@ -23,10 +24,10 @@ func Session(envVars map[string]any) echo.MiddlewareFunc {
|
|||||||
return next(c)
|
return next(c)
|
||||||
}
|
}
|
||||||
s = &db.Session{SessionId: cookie.Value}
|
s = &db.Session{SessionId: cookie.Value}
|
||||||
if err = db.FetchSession(s); err == nil {
|
if err = sc.Db.FetchSession(s); err == nil {
|
||||||
// session found
|
// session found
|
||||||
u = &db.User{Pubkey: s.Pubkey, LastSeen: time.Now()}
|
u = &db.User{Pubkey: s.Pubkey, LastSeen: time.Now()}
|
||||||
if err = db.UpdateUser(u); err != nil {
|
if err = sc.Db.UpdateUser(u); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.Set("session", *u)
|
c.Set("session", *u)
|
||||||
@ -38,7 +39,7 @@ func Session(envVars map[string]any) echo.MiddlewareFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SessionGuard(envVars map[string]any) echo.MiddlewareFunc {
|
func SessionGuard(sc context.ServerContext) 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 {
|
||||||
session := c.Get("session")
|
session := c.Get("session")
|
||||||
|
@ -3,42 +3,78 @@ package router
|
|||||||
import (
|
import (
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/env"
|
"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"
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AddRoutes(e *echo.Echo) {
|
type ServerContext = context.ServerContext
|
||||||
envVars := map[string]any{
|
|
||||||
"PUBLIC_URL": env.PublicURL,
|
type MiddlewareFunc func(sc ServerContext) echo.MiddlewareFunc
|
||||||
"COMMIT_SHORT_SHA": env.CommitShortSha,
|
type HandlerFunc = func(sc ServerContext) echo.HandlerFunc
|
||||||
"COMMIT_LONG_SHA": env.CommitLongSha,
|
|
||||||
"VERSION": env.Version,
|
func AddRoutes(e *echo.Echo, sc ServerContext) {
|
||||||
|
mountMiddleware(e, sc)
|
||||||
|
addFrontendRoutes(e, sc)
|
||||||
|
addBackendRoutes(e, sc)
|
||||||
}
|
}
|
||||||
e.Use(middleware.Session(envVars))
|
|
||||||
e.GET("/", handler.HandleIndex(envVars))
|
func mountMiddleware(e *echo.Echo, sc ServerContext) {
|
||||||
e.GET("/login", handler.HandleLogin(envVars))
|
Use(e, sc, middleware.Session)
|
||||||
e.GET("/api/login", handler.HandleLoginCallback(envVars))
|
}
|
||||||
e.GET("/api/session", handler.HandleCheckSession(envVars))
|
|
||||||
e.POST("/logout", handler.HandleLogout(envVars))
|
func addFrontendRoutes(e *echo.Echo, sc ServerContext) {
|
||||||
e.GET("/user",
|
GET(e, sc, "/user",
|
||||||
handler.HandleUser(envVars),
|
handler.HandleUser,
|
||||||
middleware.SessionGuard(envVars))
|
middleware.SessionGuard)
|
||||||
e.GET("/market/:id",
|
GET(e, sc, "/market/:id",
|
||||||
handler.HandleMarket(envVars),
|
handler.HandleMarket,
|
||||||
middleware.SessionGuard(envVars))
|
middleware.SessionGuard)
|
||||||
e.POST("/market/:id/order",
|
POST(e, sc, "/market/:id/order",
|
||||||
handler.HandlePostOrder(envVars),
|
handler.HandleOrder,
|
||||||
middleware.SessionGuard(envVars),
|
middleware.SessionGuard,
|
||||||
middleware.LNDGuard(envVars))
|
middleware.LNDGuard)
|
||||||
e.GET("/invoice/:id",
|
GET(e, sc, "/invoice/:id",
|
||||||
handler.HandleInvoice(envVars),
|
handler.HandleInvoice,
|
||||||
middleware.SessionGuard(envVars),
|
middleware.SessionGuard)
|
||||||
middleware.LNDGuard(envVars),
|
}
|
||||||
)
|
|
||||||
e.GET("/api/invoice/:id",
|
func addBackendRoutes(e *echo.Echo, sc ServerContext) {
|
||||||
handler.HandleInvoiceAPI(envVars),
|
GET(e, sc, "/api/markets", handler.HandleMarkets)
|
||||||
middleware.SessionGuard(envVars),
|
POST(e, sc, "/api/market",
|
||||||
middleware.LNDGuard(envVars),
|
handler.HandleCreateMarket,
|
||||||
)
|
middleware.SessionGuard,
|
||||||
|
middleware.LNDGuard)
|
||||||
|
GET(e, sc, "/api/market/:id", handler.HandleMarket)
|
||||||
|
POST(e, sc, "/api/order",
|
||||||
|
handler.HandleOrder,
|
||||||
|
middleware.SessionGuard,
|
||||||
|
middleware.LNDGuard)
|
||||||
|
GET(e, sc, "/api/login", handler.HandleLogin)
|
||||||
|
GET(e, sc, "/api/login/callback", handler.HandleLoginCallback)
|
||||||
|
POST(e, sc, "/api/logout", handler.HandleLogout)
|
||||||
|
GET(e, sc, "/api/session", handler.HandleCheckSession)
|
||||||
|
GET(e, sc, "/api/invoice/:id",
|
||||||
|
handler.HandleInvoiceStatus,
|
||||||
|
middleware.SessionGuard)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GET(e *echo.Echo, sc ServerContext, path string, scF HandlerFunc, scM ...MiddlewareFunc) *echo.Route {
|
||||||
|
return e.GET(path, scF(sc), toMiddlewareFunc(sc, scM...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func POST(e *echo.Echo, sc ServerContext, path string, scF HandlerFunc, scM ...MiddlewareFunc) *echo.Route {
|
||||||
|
return e.POST(path, scF(sc), toMiddlewareFunc(sc, scM...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Use(e *echo.Echo, sc ServerContext, scM ...MiddlewareFunc) {
|
||||||
|
e.Use(toMiddlewareFunc(sc, scM...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toMiddlewareFunc(sc ServerContext, scM ...MiddlewareFunc) []echo.MiddlewareFunc {
|
||||||
|
var m []echo.MiddlewareFunc
|
||||||
|
for _, m_ := range scM {
|
||||||
|
m = append(m, m_(sc))
|
||||||
|
}
|
||||||
|
return m
|
||||||
}
|
}
|
||||||
|
@ -20,12 +20,8 @@ func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Con
|
|||||||
return t.templates.ExecuteTemplate(w, name, data)
|
return t.templates.ExecuteTemplate(w, name, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
func ParseTemplates(pattern string) *Template {
|
||||||
T *Template
|
return &Template{
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
T = &Template{
|
|
||||||
templates: template.Must(template.New("").Funcs(template.FuncMap{
|
templates: template.Must(template.New("").Funcs(template.FuncMap{
|
||||||
"add": add[int64],
|
"add": add[int64],
|
||||||
"sub": sub[int64],
|
"sub": sub[int64],
|
||||||
|
@ -11,21 +11,30 @@ type Server struct {
|
|||||||
*echo.Echo
|
*echo.Echo
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer() *Server {
|
type ServerContext = router.ServerContext
|
||||||
e := echo.New()
|
|
||||||
|
|
||||||
|
func New(ctx ServerContext) *Server {
|
||||||
|
var (
|
||||||
|
e *echo.Echo
|
||||||
|
s *Server
|
||||||
|
)
|
||||||
|
e = echo.New()
|
||||||
e.Static("/", "public")
|
e.Static("/", "public")
|
||||||
|
e.Renderer = router.ParseTemplates("pages/**.html")
|
||||||
e.Renderer = router.T
|
|
||||||
|
|
||||||
router.AddRoutes(e)
|
|
||||||
|
|
||||||
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
||||||
Format: "${time_custom} ${method} ${uri} ${status}\n",
|
Format: "${time_custom} ${method} ${uri} ${status}\n",
|
||||||
CustomTimeFormat: "2006-01-02 15:04:05.00000-0700",
|
CustomTimeFormat: "2006-01-02 15:04:05.00000-0700",
|
||||||
}))
|
}))
|
||||||
|
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
||||||
|
AllowOrigins: []string{"http://localhost:4224", "https://delphi.market", "https://dev1.delphi.market"},
|
||||||
|
AllowCredentials: true,
|
||||||
|
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept},
|
||||||
|
}))
|
||||||
e.HTTPErrorHandler = httpErrorHandler
|
e.HTTPErrorHandler = httpErrorHandler
|
||||||
|
|
||||||
return &Server{e}
|
s = &Server{e}
|
||||||
|
|
||||||
|
router.AddRoutes(e, ctx)
|
||||||
|
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
41
test/hooks.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
db_ "git.ekzyis.com/ekzyis/delphi.market/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dbName string = "delphi_test"
|
||||||
|
dbUrl string = fmt.Sprintf("postgres://delphi:delphi@localhost:5432/%s?sslmode=disable", dbName)
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init(db **db_.DB) {
|
||||||
|
// for ParseTemplates to work, cwd needs to be project root
|
||||||
|
_, filename, _, _ := runtime.Caller(0)
|
||||||
|
dir := path.Join(path.Dir(filename), "../")
|
||||||
|
err := os.Chdir(dir)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
*db, err = db_.New(dbUrl)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main(m *testing.M, db *db_.DB) {
|
||||||
|
if err := db.Reset(dbName); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
retCode := m.Run()
|
||||||
|
if err := db.Clear(dbName); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
os.Exit(retCode)
|
||||||
|
}
|
35
test/lnauth.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateKeyPair() (*secp256k1.PrivateKey, *secp256k1.PublicKey, error) {
|
||||||
|
var (
|
||||||
|
sk *secp256k1.PrivateKey
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if sk, err = secp256k1.GeneratePrivateKey(); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return sk, sk.PubKey(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sign(sk *secp256k1.PrivateKey, k1_ string) (string, error) {
|
||||||
|
var (
|
||||||
|
k1 []byte
|
||||||
|
sig []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if k1, err = hex.DecodeString(k1_); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if sig, err = ecdsa.SignASN1(rand.Reader, sk.ToECDSA(), k1); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(sig), nil
|
||||||
|
}
|
23
test/mocks.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/server/router"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HTTPMocks(method string, target string, body io.Reader) (*echo.Echo, *http.Request, *httptest.ResponseRecorder) {
|
||||||
|
var (
|
||||||
|
e *echo.Echo
|
||||||
|
req *http.Request
|
||||||
|
rec *httptest.ResponseRecorder
|
||||||
|
)
|
||||||
|
e = echo.New()
|
||||||
|
e.Renderer = router.ParseTemplates("pages/**.html")
|
||||||
|
req = httptest.NewRequest(method, target, body)
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
return e, req, rec
|
||||||
|
}
|
4
vue/.browserslistrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not dead
|
||||||
|
not ie 11
|
5
vue/.editorconfig
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[*.{js,jsx,ts,tsx,vue}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
18
vue/.eslintrc.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'plugin:vue/vue3-essential',
|
||||||
|
'@vue/standard'
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
parser: '@babel/eslint-parser'
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||||
|
'vue/multi-word-component-names': 'off'
|
||||||
|
}
|
||||||
|
}
|
23
vue/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
24
vue/README.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# delphi.market
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
```
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
yarn serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
```
|
||||||
|
yarn lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
5
vue/babel.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/cli-plugin-babel/preset'
|
||||||
|
]
|
||||||
|
}
|
27
vue/index.html
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
<title>delphi.market</title>
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
<meta name="theme-color" content="#8787a4">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but this site doesn't work properly without JavaScript enabled. Please enable it to
|
||||||
|
continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
19
vue/jsconfig.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"module": "esnext",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lib": [
|
||||||
|
"esnext",
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"scripthost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
33
vue/package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "delphi.market",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --port 4224",
|
||||||
|
"build": "vite build",
|
||||||
|
"serve": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^4.4.0",
|
||||||
|
"core-js": "^3.8.3",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"register-service-worker": "^1.7.2",
|
||||||
|
"vite": "^4.5.0",
|
||||||
|
"vue": "^3.2.13",
|
||||||
|
"vue-router": "4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.12.16",
|
||||||
|
"@babel/eslint-parser": "^7.12.16",
|
||||||
|
"@vue/cli-plugin-babel": "^5.0.8",
|
||||||
|
"@vue/eslint-config-standard": "^6.1.0",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"eslint": "^7.32.0",
|
||||||
|
"eslint-plugin-import": "^2.25.3",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"eslint-plugin-promise": "^5.1.0",
|
||||||
|
"eslint-plugin-vue": "^8.0.3",
|
||||||
|
"postcss": "^8.4.31",
|
||||||
|
"tailwindcss": "^3.3.5"
|
||||||
|
}
|
||||||
|
}
|
6
vue/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
}
|
BIN
vue/public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
vue/public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 449 KiB |
BIN
vue/public/app_screenshot_001.png
Normal file
After Width: | Height: | Size: 91 KiB |
BIN
vue/public/favicon.ico
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
vue/public/img/icons/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
vue/public/img/icons/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
vue/public/img/icons/android-chrome-maskable-192x192.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
vue/public/img/icons/android-chrome-maskable-512x512.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
vue/public/img/icons/apple-touch-icon-120x120.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
vue/public/img/icons/apple-touch-icon-152x152.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
vue/public/img/icons/apple-touch-icon-180x180.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
vue/public/img/icons/apple-touch-icon-60x60.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
vue/public/img/icons/apple-touch-icon-76x76.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
vue/public/img/icons/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
vue/public/img/icons/favicon-16x16.png
Normal file
After Width: | Height: | Size: 799 B |
BIN
vue/public/img/icons/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
vue/public/img/icons/msapplication-icon-144x144.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
vue/public/img/icons/mstile-150x150.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
3
vue/public/img/icons/safari-pinned-tab.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.00251 14.9297L0 1.07422H6.14651L8.00251 4.27503L9.84583 1.07422H16L8.00251 14.9297Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 215 B |
29
vue/public/manifest.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"short_name": "dm",
|
||||||
|
"name": "Delphi Market",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-192x192.png",
|
||||||
|
"type": "image/jpeg",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-512x512.png",
|
||||||
|
"type": "image/jpeg",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"background_color": "#091833",
|
||||||
|
"display": "standalone",
|
||||||
|
"scope": "/",
|
||||||
|
"theme_color": "#8787a4",
|
||||||
|
"description": "Prediction Market on Lightning",
|
||||||
|
"screenshots": [
|
||||||
|
{
|
||||||
|
"src": "/app_screenshot_001.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "750x1334",
|
||||||
|
"form_factor": "narrow"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
2
vue/public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow:
|
44
vue/src/App.vue
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<div id="container">
|
||||||
|
<NavBar />
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useSession } from './stores/session'
|
||||||
|
const session = useSession()
|
||||||
|
session.checkSession()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable -->
|
||||||
|
<!-- eslint wants to combine this <script> and <script setup> which breaks the code ... -->
|
||||||
|
<script>
|
||||||
|
import NavBar from './components/NavBar'
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
components: { NavBar }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
background-color: #091833;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-align: center;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#container {
|
||||||
|
margin: 1em auto;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
</style>
|
BIN
vue/src/assets/logo.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
147
vue/src/components/Invoice.vue
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<router-link v-if="success" :to="callbackUrl" class="label success font-mono">
|
||||||
|
<div>Paid</div>
|
||||||
|
<small v-if="redirectTimeout > 0">Redirecting in {{ redirectTimeout }} ...</small>
|
||||||
|
</router-link>
|
||||||
|
<div class="font-mono my-3">
|
||||||
|
Payment Required
|
||||||
|
</div>
|
||||||
|
<div v-if="error" class="label error font-mono">
|
||||||
|
<div>Error</div>
|
||||||
|
<small>{{ error }}</small>
|
||||||
|
</div>
|
||||||
|
<div v-if="invoice">
|
||||||
|
<figure class="flex flex-col m-auto">
|
||||||
|
<a class="m-auto" :href="'lightning:' + invoice.PaymentRequest">
|
||||||
|
<img :src="'data:image/png;base64,' + invoice.Qr" />
|
||||||
|
</a>
|
||||||
|
<figcaption class="flex flex-row my-3 font-mono text-xs">
|
||||||
|
<span class="w-[80%] text-ellipsis overflow-hidden">{{ invoice.PaymentRequest }}</span>
|
||||||
|
<button @click.prevent="copy">{{ label }}</button>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
<div class="grid text-muted text-xs">
|
||||||
|
<span class="mx-3 my-1">payment hash</span><span class="text-ellipsis overflow-hidden font-mono me-3 my-1">
|
||||||
|
{{ invoice.Hash }}
|
||||||
|
</span>
|
||||||
|
<span class="mx-3 my-1">created at</span><span class="text-ellipsis overflow-hidden font-mono me-3 my-1">
|
||||||
|
{{ invoice.CreatedAt }}
|
||||||
|
</span>
|
||||||
|
<span class="mx-3 my-1">expires at</span><span class="text-ellipsis overflow-hidden font-mono me-3 my-1">
|
||||||
|
{{ invoice.ExpiresAt }}
|
||||||
|
</span>
|
||||||
|
<span class="mx-3 my-1">sats</span><span class="text-ellipsis overflow-hidden font-mono me-3 my-1">
|
||||||
|
{{ invoice.Msats / 1000 }}
|
||||||
|
</span>
|
||||||
|
<span class="mx-3 my-1">description</span>
|
||||||
|
<span class="text-ellipsis overflow-hidden font-mono me-3 my-1">
|
||||||
|
<span v-if="invoice.DescriptionMarketId">
|
||||||
|
<span v-if="invoice.Description">
|
||||||
|
<span>{{ invoice.Description }}</span>
|
||||||
|
<router-link :to="'/market/' + invoice.DescriptionMarketId">[market]</router-link>
|
||||||
|
</span>
|
||||||
|
<span v-else><empty></span>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<span v-if="invoice.Description">{{ invoice.Description }}</span>
|
||||||
|
<span v-else><empty></span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
// TODO validate callback url
|
||||||
|
const callbackUrl = ref('/')
|
||||||
|
let pollCount = 0
|
||||||
|
const INVOICE_POLL = 2000
|
||||||
|
const poll = async () => {
|
||||||
|
pollCount++
|
||||||
|
const url = window.origin + '/api/invoice/' + route.params.id
|
||||||
|
const res = await fetch(url)
|
||||||
|
const body = await res.json()
|
||||||
|
if (body.ConfirmedAt) {
|
||||||
|
success.value = true
|
||||||
|
clearInterval(interval)
|
||||||
|
if (pollCount > 1) {
|
||||||
|
// only redirect if the invoice was not immediately paid
|
||||||
|
setInterval(() => {
|
||||||
|
if (--redirectTimeout.value === 0) {
|
||||||
|
router.push(callbackUrl.value)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
redirectTimeout.value = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let interval
|
||||||
|
const invoice = ref(undefined)
|
||||||
|
const redirectTimeout = ref(3)
|
||||||
|
const success = ref(null)
|
||||||
|
const error = ref(null)
|
||||||
|
const label = ref('copy')
|
||||||
|
let copyTimeout = null
|
||||||
|
|
||||||
|
const copy = () => {
|
||||||
|
navigator.clipboard?.writeText(invoice.value.PaymentRequest)
|
||||||
|
label.value = 'copied'
|
||||||
|
if (copyTimeout) clearTimeout(copyTimeout)
|
||||||
|
copyTimeout = setTimeout(() => { label.value = 'copy' }, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
await (async () => {
|
||||||
|
const url = window.origin + '/api/invoice/' + route.params.id
|
||||||
|
const res = await fetch(url)
|
||||||
|
if (res.status === 404) {
|
||||||
|
error.value = 'invoice not found'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const body = await res.json()
|
||||||
|
if (body.Description) {
|
||||||
|
const regexp = /\[market:(?<id>[0-9]+)\]/
|
||||||
|
const m = body.Description.match(regexp)
|
||||||
|
const marketId = m?.groups?.id
|
||||||
|
if (marketId) {
|
||||||
|
body.DescriptionMarketId = marketId
|
||||||
|
body.Description = body.Description.replace(regexp, '')
|
||||||
|
callbackUrl.value = '/market/' + marketId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
invoice.value = body
|
||||||
|
interval = setInterval(poll, INVOICE_POLL)
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
img {
|
||||||
|
width: 256px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
figcaption {
|
||||||
|
margin: 0.75em auto;
|
||||||
|
width: 256px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin: 1em auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.label {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid {
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
}
|
||||||
|
</style>
|
116
vue/src/components/LoginQRCode.vue
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<router-link v-if="success" to="/" class="label success font-mono">
|
||||||
|
<div>Authenticated</div>
|
||||||
|
<small>Redirecting in {{ redirectTimeout }} ...</small>
|
||||||
|
</router-link>
|
||||||
|
<div class="font-mono my-3">
|
||||||
|
LNURL-auth
|
||||||
|
</div>
|
||||||
|
<div v-if="error" class="label error font-mono">
|
||||||
|
<div>Authentication error</div>
|
||||||
|
<small>{{ error }}</small>
|
||||||
|
</div>
|
||||||
|
<figure v-if="lnurl && qr" class="flex flex-col m-auto">
|
||||||
|
<a class="m-auto" :href="'lightning:' + lnurl">
|
||||||
|
<img :src="'data:image/png;base64,' + qr" />
|
||||||
|
</a>
|
||||||
|
<figcaption class="flex flex-row my-3 font-mono text-xs">
|
||||||
|
<span class="w-[80%] text-ellipsis overflow-hidden">{{ lnurl }}</span>
|
||||||
|
<button @click.prevent="copy">{{ label }}</button>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useSession } from '@/stores/session'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const session = useSession()
|
||||||
|
|
||||||
|
const qr = ref(null)
|
||||||
|
const lnurl = ref(null)
|
||||||
|
let interval = null
|
||||||
|
const LOGIN_POLL = 2000
|
||||||
|
const redirectTimeout = ref(3)
|
||||||
|
const success = ref(null)
|
||||||
|
const error = ref(null)
|
||||||
|
const label = ref('copy')
|
||||||
|
let copyTimeout = null
|
||||||
|
|
||||||
|
const copy = () => {
|
||||||
|
navigator.clipboard?.writeText(lnurl.value)
|
||||||
|
label.value = 'copied'
|
||||||
|
if (copyTimeout) clearTimeout(copyTimeout)
|
||||||
|
copyTimeout = setTimeout(() => { label.value = 'copy' }, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
const poll = async () => {
|
||||||
|
try {
|
||||||
|
await session.checkSession()
|
||||||
|
if (session.isAuthenticated) {
|
||||||
|
success.value = true
|
||||||
|
clearInterval(interval)
|
||||||
|
setInterval(() => {
|
||||||
|
if (--redirectTimeout.value === 0) {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// ignore 404 errors
|
||||||
|
if (err.reason !== 'session not found') {
|
||||||
|
console.error(err)
|
||||||
|
error.value = err.reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = async () => {
|
||||||
|
const s = await session.login()
|
||||||
|
qr.value = s.qr
|
||||||
|
lnurl.value = s.lnurl
|
||||||
|
interval = setInterval(poll, LOGIN_POLL)
|
||||||
|
}
|
||||||
|
|
||||||
|
await (async () => {
|
||||||
|
// redirect to / if session already exists
|
||||||
|
if (session.initialized) {
|
||||||
|
if (session.isAuthenticated) return router.push('/')
|
||||||
|
return login()
|
||||||
|
}
|
||||||
|
// else subscribe to changes
|
||||||
|
return session.$subscribe(() => {
|
||||||
|
if (session.initialized) {
|
||||||
|
// for some reason, computed property is only updated when accessing the store directly
|
||||||
|
// it is not updated inside the second argument
|
||||||
|
if (session.isAuthenticated) return router.push('/')
|
||||||
|
return login()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
img {
|
||||||
|
width: 256px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
figcaption {
|
||||||
|
margin: 0.75em auto;
|
||||||
|
width: 256px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin: 1em auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.label {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
140
vue/src/components/Market.vue
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<template>
|
||||||
|
<!-- eslint-disable -->
|
||||||
|
<div class="my-3">
|
||||||
|
<pre>
|
||||||
|
_ _
|
||||||
|
_ __ ___ __ _ _ __| | _____| |_
|
||||||
|
| '_ ` _ \ / _` | '__| |/ / _ \ __|
|
||||||
|
| | | | | | (_| | | | ( __/ |_
|
||||||
|
|_| |_| |_|\__,_|_| |_|\_\___|\__|</pre>
|
||||||
|
</div>
|
||||||
|
<div class="font-mono">{{ market.Description }}</div>
|
||||||
|
<!-- eslint-enable -->
|
||||||
|
<button type="button" :class="yesClass" class="label success font-mono mx-1 my-3"
|
||||||
|
@click.prevent="toggleYes">YES</button>
|
||||||
|
<button type="button" :class="noClass" class="label error font-mono mx-1 my-3" @click.prevent="toggleNo">NO</button>
|
||||||
|
<form v-show="showForm" @submit.prevent="submitForm">
|
||||||
|
<label for="stake">how much?</label>
|
||||||
|
<input name="stake" v-model="stake" type="number" min="0" placeholder="sats" required />
|
||||||
|
<label for="certainty">how sure?</label>
|
||||||
|
<input name="certainty" v-model="certainty" type="number" min="0.01" max="0.99" step="0.01" required />
|
||||||
|
<label>you receive:</label>
|
||||||
|
<label>{{ format(shares) }} {{ selected }} shares @ {{ format(price) }} sats</label>
|
||||||
|
<label>you pay:</label>
|
||||||
|
<label>{{ format(cost) }} sats</label>
|
||||||
|
<label>if you win:</label>
|
||||||
|
<label>{{ format(profit) }} sats</label>
|
||||||
|
<button class="col-span-2" type="submit">submit order</button>
|
||||||
|
</form>
|
||||||
|
<div v-if="err" class="red text-center">{{ err }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const marketId = route.params.id
|
||||||
|
const selected = ref(null)
|
||||||
|
const showForm = computed(() => selected.value !== null)
|
||||||
|
const yesClass = computed(() => selected.value === 'YES' ? ['active'] : [])
|
||||||
|
const noClass = computed(() => selected.value === 'NO' ? ['active'] : [])
|
||||||
|
const err = ref(null)
|
||||||
|
|
||||||
|
// how much wants the user bet?
|
||||||
|
const stake = ref(null)
|
||||||
|
// how sure is the user he will win?
|
||||||
|
const certainty = ref(null)
|
||||||
|
// price per share: more risk, lower price, higher reward
|
||||||
|
const price = computed(() => certainty.value * 100)
|
||||||
|
// how many (full) shares can be bought?
|
||||||
|
const shares = computed(() => {
|
||||||
|
const val = price.value > 0 ? stake.value / price.value : null
|
||||||
|
// only full shares can be bought
|
||||||
|
return Math.round(val)
|
||||||
|
})
|
||||||
|
// how much does this order cost?
|
||||||
|
const cost = computed(() => {
|
||||||
|
return shares.value * price.value
|
||||||
|
})
|
||||||
|
// how high is the potential reward?
|
||||||
|
const profit = computed(() => {
|
||||||
|
// shares expire at 10 or 0 sats
|
||||||
|
const val = (100 * shares.value) - cost.value
|
||||||
|
return isNaN(val) ? 0 : val
|
||||||
|
})
|
||||||
|
|
||||||
|
const format = (x, i = 3) => x === null ? null : x >= 1 ? Math.round(x) : x === 0 ? x : x.toFixed(i)
|
||||||
|
|
||||||
|
const market = ref(null)
|
||||||
|
const url = '/api/market/' + marketId
|
||||||
|
await fetch(url)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(body => {
|
||||||
|
market.value = body
|
||||||
|
})
|
||||||
|
.catch(console.error)
|
||||||
|
// Currently, we only support binary markets.
|
||||||
|
// (only events which can be answered with YES and NO)
|
||||||
|
const yesShareId = computed(() => {
|
||||||
|
return market?.value.Shares.find(s => s.Description === 'YES').Id
|
||||||
|
})
|
||||||
|
const noShareId = computed(() => {
|
||||||
|
return market?.value.Shares.find(s => s.Description === 'NO').Id
|
||||||
|
})
|
||||||
|
const shareId = computed(() => {
|
||||||
|
return selected.value === 'YES' ? yesShareId.value : noShareId.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleYes = () => {
|
||||||
|
selected.value = selected.value === 'YES' ? null : 'YES'
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleNo = () => {
|
||||||
|
selected.value = selected.value === 'NO' ? null : 'NO'
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
// TODO validate form
|
||||||
|
const url = window.origin + '/api/order'
|
||||||
|
const body = JSON.stringify({
|
||||||
|
sid: shareId.value,
|
||||||
|
quantity: shares.value,
|
||||||
|
price: price.value,
|
||||||
|
// TODO support selling
|
||||||
|
side: 'BUY'
|
||||||
|
})
|
||||||
|
const res = await fetch(url, { method: 'POST', headers: { 'Content-type': 'application/json' }, body })
|
||||||
|
const resBody = await res.json()
|
||||||
|
if (res.status !== 402) {
|
||||||
|
err.value = `error: server responded with HTTP ${resBody.status}`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const invoiceId = resBody.id
|
||||||
|
router.push('/invoice/' + invoiceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.success.active {
|
||||||
|
background-color: #35df8d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error.active {
|
||||||
|
background-color: #ff7386;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
form>* {
|
||||||
|
margin: 0.5em 1em;
|
||||||
|
}
|
||||||
|
</style>
|
46
vue/src/components/MarketForm.vue
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<form ref="form" class="flex flex-col mx-auto text-left" method="post" action="/api/market"
|
||||||
|
@submit.prevent="submitForm">
|
||||||
|
<label for="desc">event description</label>
|
||||||
|
<textarea v-model="description" class="mb-1" id="desc" name="desc" type="text"></textarea>
|
||||||
|
<label for="endDate">end date</label>
|
||||||
|
<input v-model="endDate" class="mb-3" id="endDate" name="endDate" type="date" />
|
||||||
|
<div class="flex flex-row justify-center">
|
||||||
|
<button type="button" class="me-1" @click.prevent="$props.onCancel">cancel</button>
|
||||||
|
<button type="submit">submit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, defineProps } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
defineProps(['onCancel'])
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const form = ref(null)
|
||||||
|
const description = ref(null)
|
||||||
|
const endDate = ref(null)
|
||||||
|
|
||||||
|
const parseEndDate = endDate => {
|
||||||
|
const [yyyy, mm, dd] = endDate.split('-')
|
||||||
|
return `${yyyy}-${mm}-${dd}T00:00:00.000Z`
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
const url = window.origin + '/api/market'
|
||||||
|
const body = JSON.stringify({ description: description.value, endDate: parseEndDate(endDate.value) })
|
||||||
|
const res = await fetch(url, { method: 'post', headers: { 'Content-type': 'application/json' }, body })
|
||||||
|
const resBody = await res.json()
|
||||||
|
const invoiceId = resBody.id
|
||||||
|
router.push('/invoice/' + invoiceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
textarea {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
</style>
|
44
vue/src/components/MarketList.vue
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<ul>
|
||||||
|
<li class="my-3" v-for="market in markets" :key="market.id">
|
||||||
|
<router-link :to="'/market/' + market.id">{{ market.description }}</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<button v-if="!showForm" @click.prevent="toggleForm">+ create market</button>
|
||||||
|
<div v-else class="flex flex-col justify-center">
|
||||||
|
<MarketForm :onCancel="toggleForm" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import MarketForm from './MarketForm'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useSession } from '@/stores/session'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const session = useSession()
|
||||||
|
const router = useRouter()
|
||||||
|
const markets = ref([])
|
||||||
|
const showForm = ref(false)
|
||||||
|
|
||||||
|
// TODO only load markets once per session
|
||||||
|
const url = window.origin + '/api/markets'
|
||||||
|
await fetch(url).then(async r => {
|
||||||
|
const body = await r.json()
|
||||||
|
markets.value = body
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleForm = () => {
|
||||||
|
if (!session.isAuthenticated) {
|
||||||
|
return router.push('/login')
|
||||||
|
}
|
||||||
|
showForm.value = !showForm.value
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
a {
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
</style>
|
26
vue/src/components/NavBar.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<header class="flex flex-row text-center justify-center pt-1">
|
||||||
|
<nav>
|
||||||
|
<router-link to="/">market</router-link>
|
||||||
|
<router-link to="/user" v-if="session.isAuthenticated">user</router-link>
|
||||||
|
<router-link to="/login" v-else-if="session.isAuthenticated === false" href="/login">login</router-link>
|
||||||
|
<router-link disabled to="/" v-else>...</router-link>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useSession } from '@/stores/session'
|
||||||
|
const session = useSession()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav>a {
|
||||||
|
margin: 0 3px;
|
||||||
|
}
|
||||||
|
</style>
|
70
vue/src/index.css
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: #ffffff;
|
||||||
|
border: solid 1px #8787A4;
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
background: #8787A4;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #8787a4;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
background: #8787A4;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.selected {
|
||||||
|
color: #ffffff;
|
||||||
|
background: #8787A4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
border: none;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 0.5em 3em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
background-color: rgba(20, 158, 97, .24);
|
||||||
|
color: #35df8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success:hover {
|
||||||
|
background-color: #35df8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red {
|
||||||
|
color: #ff7386;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background-color: rgba(245, 57, 94, .24);
|
||||||
|
color: #ff7386;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error:hover {
|
||||||
|
background-color: #ff7386;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
opacity: 0.67
|
||||||
|
}
|
41
vue/src/main.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import * as VueRouter from 'vue-router'
|
||||||
|
import App from './App.vue'
|
||||||
|
import './registerServiceWorker'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
import HomeView from '@/views/HomeView'
|
||||||
|
import LoginView from '@/views/LoginView'
|
||||||
|
import UserView from '@/views/UserView'
|
||||||
|
import MarketView from '@/views/MarketView'
|
||||||
|
import InvoiceView from '@/views/InvoiceView'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/', component: HomeView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login', component: LoginView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/user', component: UserView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/market/:id', component: MarketView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/invoice/:id', component: InvoiceView
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const router = VueRouter.createRouter({
|
||||||
|
history: VueRouter.createWebHashHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.use(router)
|
||||||
|
app.use(pinia)
|
||||||
|
app.mount('#app')
|
32
vue/src/registerServiceWorker.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
import { register } from 'register-service-worker'
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||||
|
ready () {
|
||||||
|
console.log(
|
||||||
|
'App is being served from cache by a service worker.\n' +
|
||||||
|
'For more details, visit https://goo.gl/AFskqB'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
registered () {
|
||||||
|
console.log('Service worker has been registered.')
|
||||||
|
},
|
||||||
|
cached () {
|
||||||
|
console.log('Content has been cached for offline use.')
|
||||||
|
},
|
||||||
|
updatefound () {
|
||||||
|
console.log('New content is downloading.')
|
||||||
|
},
|
||||||
|
updated () {
|
||||||
|
console.log('New content is available; please refresh.')
|
||||||
|
},
|
||||||
|
offline () {
|
||||||
|
console.log('No internet connection found. App is running in offline mode.')
|
||||||
|
},
|
||||||
|
error (error) {
|
||||||
|
console.error('Error during service worker registration:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
44
vue/src/stores/session.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
export const useSession = defineStore('session', () => {
|
||||||
|
const pubkey = ref(null)
|
||||||
|
const initialized = ref(false)
|
||||||
|
const isAuthenticated = computed(() => initialized.value ? !!pubkey.value : undefined)
|
||||||
|
|
||||||
|
function checkSession () {
|
||||||
|
const url = window.origin + '/api/session'
|
||||||
|
return fetch(url, {
|
||||||
|
credentials: 'include'
|
||||||
|
})
|
||||||
|
.then(async r => {
|
||||||
|
const body = await r.json()
|
||||||
|
if (body.pubkey) {
|
||||||
|
pubkey.value = body.pubkey
|
||||||
|
console.log('authenticated as', body.pubkey)
|
||||||
|
} else console.log('unauthenticated')
|
||||||
|
initialized.value = true
|
||||||
|
return body
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('error:', err.reason || err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function login () {
|
||||||
|
const url = window.origin + '/api/login'
|
||||||
|
return fetch(url, { credentials: 'include' }).then(r => r.json())
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout () {
|
||||||
|
const url = window.origin + '/api/logout'
|
||||||
|
return fetch(url, { method: 'POST', credentials: 'include' })
|
||||||
|
.then(async r => {
|
||||||
|
const body = await r.json()
|
||||||
|
if (body.status === 'OK') {
|
||||||
|
pubkey.value = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { pubkey, isAuthenticated, initialized, checkSession, login, logout }
|
||||||
|
})
|
21
vue/src/views/HomeView.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<!-- eslint-disable -->
|
||||||
|
<div class="my-3">
|
||||||
|
<pre>
|
||||||
|
_ _ _ _
|
||||||
|
__| | ___| |_ __ | |__ (_)
|
||||||
|
/ _` |/ _ \ | '_ \| '_ \| |
|
||||||
|
| (_| | __/ | |_) | | | | |
|
||||||
|
\__,_|\___|_| .__/|_| |_|_|
|
||||||
|
|_|.market </pre>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- eslint-enable -->
|
||||||
|
<Suspense>
|
||||||
|
<MarketList />
|
||||||
|
</Suspense>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import MarketList from '@/components/MarketList'
|
||||||
|
</script>
|
19
vue/src/views/InvoiceView.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<!-- eslint-disable -->
|
||||||
|
<div class="my-3">
|
||||||
|
<pre>
|
||||||
|
_ _ ___ ____
|
||||||
|
| || | / _ \___ \
|
||||||
|
| || |_| | | |__) |
|
||||||
|
|__ _| |_| / __/
|
||||||
|
|_| \___/_____|</pre>
|
||||||
|
</div>
|
||||||
|
<!-- eslint-enable -->
|
||||||
|
<Suspense>
|
||||||
|
<Invoice />
|
||||||
|
</Suspense>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Invoice from '@/components/Invoice'
|
||||||
|
</script>
|
20
vue/src/views/LoginView.vue
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<!-- eslint-disable -->
|
||||||
|
<div class="my-3">
|
||||||
|
<pre>
|
||||||
|
_ _
|
||||||
|
| | ___ __ _(_)_ __
|
||||||
|
| |/ _ \ / _` | | '_ \
|
||||||
|
| | (_) | (_| | | | | |
|
||||||
|
|_|\___/ \__, |_|_| |_|
|
||||||
|
|___/ </pre>
|
||||||
|
</div>
|
||||||
|
<!-- eslint-enable -->
|
||||||
|
<Suspense>
|
||||||
|
<LoginQRCode class="flex justify-center m-3" />
|
||||||
|
</Suspense>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import LoginQRCode from '@/components/LoginQRCode'
|
||||||
|
</script>
|
9
vue/src/views/MarketView.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<Suspense>
|
||||||
|
<Market />
|
||||||
|
</Suspense>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Market from '@/components/Market'
|
||||||
|
</script>
|
29
vue/src/views/UserView.vue
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<!-- eslint-disable -->
|
||||||
|
<div class="my-3">
|
||||||
|
<pre>
|
||||||
|
|
||||||
|
_ _ ___ ___ _ __
|
||||||
|
| | | / __|/ _ \ '__|
|
||||||
|
| |_| \__ \ __/ |
|
||||||
|
\__,_|___/\___|_| </pre>
|
||||||
|
</div>
|
||||||
|
<!-- eslint-enable -->
|
||||||
|
<div v-if="session.pubkey">
|
||||||
|
<div>authenticated as {{ session.pubkey.slice(0, 8) }}</div>
|
||||||
|
<button class="my-3" @click="logout">logout</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useSession } from '@/stores/session'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
const session = useSession()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
await session.logout()
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
10
vue/tailwind.config.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./src/**/*.{html,js,vue}'
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {}
|
||||||
|
},
|
||||||
|
plugins: []
|
||||||
|
}
|
14
vue/vite.config.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
4
vue/vue.config.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
const { defineConfig } = require('@vue/cli-service')
|
||||||
|
module.exports = defineConfig({
|
||||||
|
transpileDependencies: true
|
||||||
|
})
|