refactor: Structure code into different packages
I have put too much code into the same files. Also, I put everything into the same package: main. This package is only meant for executables. Therefore, I have refactored my code to use multiple packages. These packages also guarantee separation of concerns since Golang doesn't allow cyclic imports.
This commit is contained in:
parent
b4a8adcb9a
commit
7558655458
10
Makefile
10
Makefile
@ -1,8 +1,12 @@
|
||||
.PHONY: build run
|
||||
|
||||
SOURCE := $(shell find db env lib lnd pages public server -type f)
|
||||
|
||||
build: delphi.market
|
||||
|
||||
delphi.market: src/*.go
|
||||
go build -o delphi.market ./src/
|
||||
delphi.market: $(SOURCE)
|
||||
go build -o delphi.market .
|
||||
|
||||
run:
|
||||
go run ./src/
|
||||
go run .
|
||||
|
||||
|
45
db/db.go
Normal file
45
db/db.go
Normal file
@ -0,0 +1,45 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/namsral/flag"
|
||||
)
|
||||
|
||||
var (
|
||||
db *DB
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
func init() {
|
||||
if err := godotenv.Load(); err != nil {
|
||||
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 {
|
||||
db, err := sql.Open("postgres", url)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// test connection
|
||||
_, err = db.Exec("SELECT 1")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// TODO: run migrations
|
||||
return &DB{DB: db}
|
||||
}
|
46
db/invoice.go
Normal file
46
db/invoice.go
Normal file
@ -0,0 +1,46 @@
|
||||
package db
|
||||
|
||||
import "time"
|
||||
|
||||
func CreateInvoice(invoice *Invoice) error {
|
||||
if err := db.QueryRow(""+
|
||||
"INSERT INTO invoices(pubkey, msats, preimage, hash, bolt11, created_at, expires_at) "+
|
||||
"VALUES($1, $2, $3, $4, $5, $6, $7) "+
|
||||
"RETURNING id",
|
||||
invoice.Pubkey, invoice.Msats, invoice.Preimage, invoice.Hash, invoice.PaymentRequest, invoice.CreatedAt, invoice.ExpiresAt).Scan(&invoice.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type FetchInvoiceWhere struct {
|
||||
Id string
|
||||
Hash string
|
||||
}
|
||||
|
||||
func FetchInvoice(where *FetchInvoiceWhere, invoice *Invoice) error {
|
||||
var (
|
||||
query = "SELECT id, pubkey, msats, preimage, hash, bolt11, created_at, expires_at, confirmed_at, held_since FROM invoices "
|
||||
args []any
|
||||
)
|
||||
if where.Id != "" {
|
||||
query += "WHERE id = $1"
|
||||
args = append(args, where.Id)
|
||||
} else if where.Hash != "" {
|
||||
query += "WHERE hash = $1"
|
||||
args = append(args, where.Hash)
|
||||
}
|
||||
if err := db.QueryRow(query, args...).Scan(
|
||||
&invoice.Id, &invoice.Pubkey, &invoice.Msats, &invoice.Preimage, &invoice.Hash,
|
||||
&invoice.PaymentRequest, &invoice.CreatedAt, &invoice.ExpiresAt, &invoice.ConfirmedAt, &invoice.HeldSince); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func 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 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
18
db/lnauth.go
Normal file
18
db/lnauth.go
Normal file
@ -0,0 +1,18 @@
|
||||
package db
|
||||
|
||||
func CreateLNAuth(lnAuth *LNAuth) error {
|
||||
err := db.QueryRow(
|
||||
"INSERT INTO lnauth(k1, lnurl) VALUES($1, $2) RETURNING session_id",
|
||||
lnAuth.K1, lnAuth.LNURL).Scan(&lnAuth.SessionId)
|
||||
return err
|
||||
}
|
||||
|
||||
func FetchSessionId(k1 string, sessionId *string) error {
|
||||
err := db.QueryRow("SELECT session_id FROM lnauth WHERE k1 = $1", k1).Scan(sessionId)
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteLNAuth(lnAuth *LNAuth) error {
|
||||
_, err := db.Exec("DELETE FROM lnauth WHERE k1 = $1", lnAuth.K1)
|
||||
return err
|
||||
}
|
90
db/market.go
Normal file
90
db/market.go
Normal file
@ -0,0 +1,90 @@
|
||||
package db
|
||||
|
||||
import "database/sql"
|
||||
|
||||
func FetchMarket(marketId int, market *Market) error {
|
||||
if err := db.QueryRow("SELECT id, description FROM markets WHERE id = $1", marketId).Scan(&market.Id, &market.Description); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FetchActiveMarkets(markets *[]Market) error {
|
||||
var (
|
||||
rows *sql.Rows
|
||||
market Market
|
||||
err error
|
||||
)
|
||||
if rows, err = db.Query("SELECT id, description, active FROM markets WHERE active = true"); err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
rows.Scan(&market.Id, &market.Description, &market.Active)
|
||||
*markets = append(*markets, market)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func 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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var share Share
|
||||
rows.Scan(&share.Id, &share.MarketId, &share.Description)
|
||||
*shares = append(*shares, share)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type FetchOrdersWhere struct {
|
||||
MarketId int
|
||||
Pubkey string
|
||||
Confirmed bool
|
||||
}
|
||||
|
||||
func FetchOrders(where *FetchOrdersWhere, orders *[]Order) error {
|
||||
query := "" +
|
||||
"SELECT o.id, share_id, o.pubkey, o.side, o.quantity, o.price, o.invoice_id, o.created_at, s.description, s.market_id, i.confirmed_at " +
|
||||
"FROM orders o " +
|
||||
"JOIN invoices i ON o.invoice_id = i.id " +
|
||||
"JOIN shares s ON o.share_id = s.id " +
|
||||
"WHERE "
|
||||
var args []any
|
||||
if where.MarketId > 0 {
|
||||
query += "share_id = ANY(SELECT id FROM shares WHERE market_id = $1) "
|
||||
args = append(args, where.MarketId)
|
||||
} else if where.Pubkey != "" {
|
||||
query += "o.pubkey = $1 "
|
||||
args = append(args, where.Pubkey)
|
||||
}
|
||||
if where.Confirmed {
|
||||
query += "AND i.confirmed_at IS NOT NULL "
|
||||
}
|
||||
query += "AND (i.confirmed_at IS NOT NULL OR i.expires_at > CURRENT_TIMESTAMP) "
|
||||
query += "ORDER BY price DESC"
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var order Order
|
||||
rows.Scan(&order.Id, &order.ShareId, &order.Pubkey, &order.Side, &order.Quantity, &order.Price, &order.InvoiceId, &order.CreatedAt, &order.Share.Description, &order.Share.MarketId, &order.Invoice.ConfirmedAt)
|
||||
*orders = append(*orders, order)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateOrder(order *Order) error {
|
||||
if _, err := db.Exec(""+
|
||||
"INSERT INTO orders(share_id, pubkey, side, quantity, price, invoice_id) "+
|
||||
"VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
order.ShareId, order.Pubkey, order.Side, order.Quantity, order.Price, order.InvoiceId); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
16
db/session.go
Normal file
16
db/session.go
Normal file
@ -0,0 +1,16 @@
|
||||
package db
|
||||
|
||||
func CreateSession(s *Session) error {
|
||||
_, err := db.Exec("INSERT INTO sessions(pubkey, session_id) VALUES($1, $2)", s.Pubkey, s.SessionId)
|
||||
return err
|
||||
}
|
||||
|
||||
func FetchSession(s *Session) error {
|
||||
err := db.QueryRow("SELECT pubkey FROM sessions WHERE session_id = $1", s.SessionId).Scan(&s.Pubkey)
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteSession(s *Session) error {
|
||||
_, err := db.Exec("DELETE FROM sessions where session_id = $1", s.SessionId)
|
||||
return err
|
||||
}
|
66
db/types.go
Normal file
66
db/types.go
Normal file
@ -0,0 +1,66 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gopkg.in/guregu/null.v4"
|
||||
)
|
||||
|
||||
type Serial = int
|
||||
type UUID = string
|
||||
|
||||
type LNAuth struct {
|
||||
K1 string
|
||||
LNURL string
|
||||
CreatdAt time.Time
|
||||
SessionId string
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Pubkey string
|
||||
LastSeen time.Time
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
Pubkey string
|
||||
SessionId string
|
||||
}
|
||||
|
||||
type Market struct {
|
||||
Id Serial
|
||||
Description string
|
||||
Active bool
|
||||
}
|
||||
|
||||
type Share struct {
|
||||
Id UUID
|
||||
MarketId int
|
||||
Description string
|
||||
}
|
||||
|
||||
type Invoice struct {
|
||||
Id UUID
|
||||
Pubkey string
|
||||
Msats int64
|
||||
MsatsReceived int64
|
||||
Preimage string
|
||||
Hash string
|
||||
PaymentRequest string
|
||||
CreatedAt time.Time
|
||||
ExpiresAt time.Time
|
||||
ConfirmedAt null.Time
|
||||
HeldSince null.Time
|
||||
}
|
||||
|
||||
type Order struct {
|
||||
Id UUID
|
||||
CreatedAt time.Time
|
||||
ShareId string `form:"share_id"`
|
||||
Share
|
||||
Pubkey string
|
||||
Side string `form:"side"`
|
||||
Quantity int64 `form:"quantity"`
|
||||
Price int64 `form:"price"`
|
||||
InvoiceId UUID
|
||||
Invoice
|
||||
}
|
13
db/user.go
Normal file
13
db/user.go
Normal file
@ -0,0 +1,13 @@
|
||||
package db
|
||||
|
||||
func CreateUser(u *User) error {
|
||||
_, err := db.Exec(
|
||||
"INSERT INTO users(pubkey) VALUES ($1) ON CONFLICT(pubkey) DO UPDATE SET last_seen = CURRENT_TIMESTAMP",
|
||||
u.Pubkey)
|
||||
return err
|
||||
}
|
||||
|
||||
func UpdateUser(u *User) error {
|
||||
_, err := db.Exec("UPDATE users SET last_seen = $1 WHERE pubkey = $2", u.LastSeen, u.Pubkey)
|
||||
return err
|
||||
}
|
42
env/env.go
vendored
Normal file
42
env/env.go
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/namsral/flag"
|
||||
)
|
||||
|
||||
var (
|
||||
Port int
|
||||
PublicURL string
|
||||
Env string
|
||||
CommitLongSha string
|
||||
CommitShortSha string
|
||||
Version string
|
||||
)
|
||||
|
||||
func init() {
|
||||
if err := godotenv.Load(); err != nil {
|
||||
log.Fatalf("error loading env vars: %s", err)
|
||||
}
|
||||
flag.StringVar(&PublicURL, "PUBLIC_URL", "delphi.market", "Public URL of website")
|
||||
flag.IntVar(&Port, "PORT", 4321, "Server port")
|
||||
flag.StringVar(&Env, "ENV", "development", "Specify for which environment files should be built")
|
||||
flag.Parse()
|
||||
CommitLongSha = execCmd("git", "rev-parse", "HEAD")
|
||||
CommitShortSha = execCmd("git", "rev-parse", "--short", "HEAD")
|
||||
Version = fmt.Sprintf("v0.0.0+%s", CommitShortSha)
|
||||
}
|
||||
|
||||
func execCmd(name string, args ...string) string {
|
||||
cmd := exec.Command(name, args...)
|
||||
stdout, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return strings.TrimSpace(string(stdout))
|
||||
}
|
14
go.mod
14
go.mod
@ -1,16 +1,21 @@
|
||||
module delphi.market
|
||||
module git.ekzyis.com/ekzyis/delphi.market
|
||||
|
||||
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
|
||||
|
||||
require (
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.2.2
|
||||
github.com/btcsuite/btcutil v1.0.2
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/labstack/echo/v4 v4.11.1
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/lightninglabs/lndclient v1.0.0
|
||||
github.com/lightningnetwork/lnd v0.10.0-beta.rc6.0.20200615174244-103c59a4889f
|
||||
github.com/namsral/flag v1.7.4-pre
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
|
||||
gopkg.in/guregu/null.v4 v4.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
@ -55,10 +60,8 @@ require (
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect
|
||||
github.com/labstack/gommon v0.4.0 // indirect
|
||||
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
|
||||
github.com/lightninglabs/lndclient v1.0.0 // indirect
|
||||
github.com/lightninglabs/neutrino v0.11.1-0.20200316235139-bffc52e8f200 // indirect
|
||||
github.com/lightningnetwork/lightning-onion v1.0.2-0.20200501022730-3c8c8d0b89ea // indirect
|
||||
github.com/lightningnetwork/lnd v0.10.0-beta.rc6.0.20200615174244-103c59a4889f // indirect
|
||||
github.com/lightningnetwork/lnd/clock v1.0.1 // indirect
|
||||
github.com/lightningnetwork/lnd/queue v1.0.4 // indirect
|
||||
github.com/lightningnetwork/lnd/ticker v1.0.0 // indirect
|
||||
@ -85,14 +88,13 @@ require (
|
||||
go.uber.org/zap v1.14.1 // indirect
|
||||
golang.org/x/crypto v0.11.0 // indirect
|
||||
golang.org/x/net v0.12.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/term v0.10.0 // indirect
|
||||
golang.org/x/text v0.11.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c // indirect
|
||||
google.golang.org/grpc v1.24.0 // indirect
|
||||
gopkg.in/errgo.v1 v1.0.1 // indirect
|
||||
gopkg.in/guregu/null.v4 v4.0.0 // indirect
|
||||
gopkg.in/macaroon-bakery.v2 v2.0.1 // indirect
|
||||
gopkg.in/macaroon.v2 v2.1.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.3 // indirect
|
||||
|
35
go.sum
35
go.sum
@ -1,5 +1,5 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e/go.mod h1:BWqTsj8PgcPriQJGl7el20J/7TuT1d/hSyFDXMEpoEo=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ=
|
||||
github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82/go.mod h1:GbuBk21JqF+driLX3XtJYNZjGa45YDoa9IqCTzNSfEc=
|
||||
@ -24,7 +24,6 @@ github.com/btcsuite/btcd v0.20.1-beta.0.20200515232429-9f0179fd2c46 h1:QyTpiR5nQ
|
||||
github.com/btcsuite/btcd v0.20.1-beta.0.20200515232429-9f0179fd2c46/go.mod h1:Yktc19YNjh/Iz2//CX0vfRTS4IJKM/RKO5YZ9Fn+Pgo=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.2.2 h1:5uxe5YjoCq+JeOpg0gZSNHuFgeogrocBYxvg6w9sAgc=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.2.2/go.mod h1:9/CSmJxmuvqzX9Wh2fXMWToLOHhPd11lSPuIupwTkI8=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U=
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||
@ -51,8 +50,10 @@ github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JG
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
||||
github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8/go.mod h1:tYvUd8KLhm/oXvUeSEs2VlLghFjQt9+ZaF9ghH0JNjc=
|
||||
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
|
||||
github.com/btcsuite/goleveldb v1.0.0 h1:Tvd0BfvqX9o823q1j2UZ/epQo09eJh6dTcRp79ilIN4=
|
||||
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
|
||||
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJGQE=
|
||||
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||
@ -81,6 +82,7 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k=
|
||||
github.com/frankban/quicktest v1.2.2 h1:xfmOhhoH5fGPgbEAlhLpJH9p0z/0Qizio9osmvn9IUY=
|
||||
github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
@ -96,7 +98,9 @@ github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
@ -109,6 +113,7 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
@ -135,13 +140,19 @@ github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c h1:3UvYABOQRhJAApj9MdCN+Ydv841ETSoy6xLzdmmr/9A=
|
||||
github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA=
|
||||
github.com/juju/errors v0.0.0-20190806202954-0232dcc7464d h1:hJXjZMxj0SWlMoQkzeZDLi2cmeiWKa7y1B8Rg+qaoEc=
|
||||
github.com/juju/errors v0.0.0-20190806202954-0232dcc7464d/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
|
||||
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 h1:UUHMLvzt/31azWTN/ifGWef4WUqvXk0iRqdhdy/2uzI=
|
||||
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
|
||||
github.com/juju/retry v0.0.0-20180821225755-9058e192b216 h1:/eQL7EJQKFHByJe3DeE8Z36yqManj9UY5zppDoQi4FU=
|
||||
github.com/juju/retry v0.0.0-20180821225755-9058e192b216/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4=
|
||||
github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2 h1:Pp8RxiF4rSoXP9SED26WCfNB28/dwTDpPXS8XMJR8rc=
|
||||
github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
|
||||
github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d h1:irPlN9z5VCe6BTsqVsxheCZH99OFSmqSVyTigW4mEoY=
|
||||
github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk=
|
||||
github.com/juju/version v0.0.0-20180108022336-b64dbd566305 h1:lQxPJ1URr2fjsKnJRt/BxiIxjLt9IKGvS+0injMHbag=
|
||||
github.com/juju/version v0.0.0-20180108022336-b64dbd566305/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
@ -151,8 +162,10 @@ github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec/go.mod h1:J+Gs4SYgM6
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4=
|
||||
github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ=
|
||||
@ -210,6 +223,7 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
|
||||
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@ -257,12 +271,15 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
gitlab.com/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e/go.mod h1:PIe5G60AUtC0u4HgbjMVEeRR1EUGMMBl6a0gB1tRdDk=
|
||||
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
|
||||
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.14.1 h1:nYDKopTbvAPq/NrUVZwT15y2lpROBiLLyoRTbXOYWOo=
|
||||
go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
|
||||
@ -277,14 +294,16 @@ golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
|
||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -320,8 +339,8 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
|
||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -339,6 +358,7 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@ -353,6 +373,7 @@ google.golang.org/grpc v1.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s=
|
||||
google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v1 v1.0.1 h1:oQFRXzZ7CkBGdm1XZm/EbQYaYNNEElNBOd09M6cqNso=
|
||||
gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo=
|
||||
@ -365,6 +386,7 @@ gopkg.in/macaroon-bakery.v2 v2.0.1/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETf
|
||||
gopkg.in/macaroon.v2 v2.0.0/go.mod h1:+I6LnTMkm/uV5ew/0nsulNjL16SK4+C8yDmRUzHR17I=
|
||||
gopkg.in/macaroon.v2 v2.1.0 h1:HZcsjBCzq9t0eBPMKqTN/uSN6JOm78ZJ2INbqcBQOUI=
|
||||
gopkg.in/macaroon.v2 v2.1.0/go.mod h1:OUb+TQP/OP0WOerC2Jp/3CwhIKyIa9kQjuc7H24e6/o=
|
||||
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw=
|
||||
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@ -377,6 +399,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
|
@ -13,10 +13,9 @@ function restart_server() {
|
||||
PID=$(pidof delphi.market)
|
||||
}
|
||||
|
||||
function sync() {
|
||||
function restart() {
|
||||
restart_server
|
||||
date +%s.%N > public/hotreload
|
||||
rsync -avh public/ dev1.delphi.market:/var/www/dev1.delphi --delete
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
@ -25,9 +24,9 @@ function cleanup() {
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
sync
|
||||
restart
|
||||
tail -f server.log &
|
||||
|
||||
while inotifywait -r -e modify src/ pages/; do
|
||||
sync
|
||||
while inotifywait -r -e modify db/ env/ lib/ lnd/ pages/ public/ server/; do
|
||||
restart
|
||||
done
|
||||
|
8
lib/max.go
Normal file
8
lib/max.go
Normal file
@ -0,0 +1,8 @@
|
||||
package lib
|
||||
|
||||
func Max(x, y int) int {
|
||||
if x < y {
|
||||
return y
|
||||
}
|
||||
return x
|
||||
}
|
7
lib/merge.go
Normal file
7
lib/merge.go
Normal file
@ -0,0 +1,7 @@
|
||||
package lib
|
||||
|
||||
func Merge[T comparable](target *map[T]any, src *map[T]any) {
|
||||
for k, v := range *src {
|
||||
(*target)[k] = v
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package lib
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
@ -6,13 +6,6 @@ import (
|
||||
"github.com/skip2/go-qrcode"
|
||||
)
|
||||
|
||||
func Max(x, y int) int {
|
||||
if x < y {
|
||||
return y
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func ToQR(s string) (string, error) {
|
||||
png, err := qrcode.Encode(s, qrcode.Medium, 256)
|
||||
if err != nil {
|
112
lnd/invoice.go
Normal file
112
lnd/invoice.go
Normal file
@ -0,0 +1,112 @@
|
||||
package lnd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
func CreateInvoice(pubkey string, msats int64) (*db.Invoice, error) {
|
||||
var (
|
||||
expiry time.Duration = time.Hour
|
||||
preimage lntypes.Preimage
|
||||
hash lntypes.Hash
|
||||
paymentRequest string
|
||||
lnInvoice *lndclient.Invoice
|
||||
dbInvoice *db.Invoice
|
||||
err error
|
||||
)
|
||||
if preimage, err = generateNewPreimage(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hash = preimage.Hash()
|
||||
if paymentRequest, err = lnd.Invoices.AddHoldInvoice(context.TODO(), &invoicesrpc.AddInvoiceData{
|
||||
Hash: &hash,
|
||||
Value: lnwire.MilliSatoshi(msats),
|
||||
Expiry: int64(expiry / time.Millisecond),
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if lnInvoice, err = lnd.Client.LookupInvoice(context.TODO(), hash); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbInvoice = &db.Invoice{
|
||||
Pubkey: pubkey,
|
||||
Msats: msats,
|
||||
Preimage: preimage.String(),
|
||||
PaymentRequest: paymentRequest,
|
||||
Hash: hash.String(),
|
||||
CreatedAt: lnInvoice.CreationDate,
|
||||
ExpiresAt: lnInvoice.CreationDate.Add(expiry),
|
||||
}
|
||||
if err := db.CreateInvoice(dbInvoice); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dbInvoice, nil
|
||||
}
|
||||
|
||||
func CheckInvoice(hash lntypes.Hash) {
|
||||
var (
|
||||
pollInterval = 5 * time.Second
|
||||
invoice db.Invoice
|
||||
lnInvoice *lndclient.Invoice
|
||||
preimage lntypes.Preimage
|
||||
err error
|
||||
)
|
||||
|
||||
if !Enabled {
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
handleLoopError := func(err error) {
|
||||
log.Println(err)
|
||||
time.Sleep(pollInterval)
|
||||
}
|
||||
|
||||
for {
|
||||
log.Printf("lookup invoice: hash=%s", hash)
|
||||
if lnInvoice, err = lnd.Client.LookupInvoice(context.TODO(), hash); err != nil {
|
||||
handleLoopError(err)
|
||||
continue
|
||||
}
|
||||
if time.Now().After(invoice.ExpiresAt) {
|
||||
// cancel invoices after expiration if no matching order found yet
|
||||
if err = lnd.Invoices.CancelInvoice(context.TODO(), hash); err != nil {
|
||||
handleLoopError(err)
|
||||
continue
|
||||
}
|
||||
log.Printf("invoice expired: hash=%s", hash)
|
||||
break
|
||||
}
|
||||
if lnInvoice.AmountPaid > 0 {
|
||||
if preimage, err = lntypes.MakePreimageFromStr(invoice.Preimage); err != nil {
|
||||
handleLoopError(err)
|
||||
continue
|
||||
}
|
||||
// TODO settle invoice after matching order was found
|
||||
if err = lnd.Invoices.SettleInvoice(context.TODO(), preimage); err != nil {
|
||||
handleLoopError(err)
|
||||
continue
|
||||
}
|
||||
if err = db.ConfirmInvoice(hash.String(), time.Now(), int(lnInvoice.AmountPaid)); err != nil {
|
||||
handleLoopError(err)
|
||||
continue
|
||||
}
|
||||
log.Printf("invoice confirmed: hash=%s", hash)
|
||||
break
|
||||
}
|
||||
time.Sleep(pollInterval)
|
||||
}
|
||||
}
|
21
lnd/lib.go
Normal file
21
lnd/lib.go
Normal file
@ -0,0 +1,21 @@
|
||||
package lnd
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"io"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
)
|
||||
|
||||
func generateNewPreimage() (lntypes.Preimage, error) {
|
||||
randomBytes := make([]byte, 32)
|
||||
_, err := io.ReadFull(rand.Reader, randomBytes)
|
||||
if err != nil {
|
||||
return lntypes.Preimage{}, err
|
||||
}
|
||||
preimage, err := lntypes.MakePreimage(randomBytes)
|
||||
if err != nil {
|
||||
return lntypes.Preimage{}, err
|
||||
}
|
||||
return preimage, nil
|
||||
}
|
49
lnd/lnd.go
Normal file
49
lnd/lnd.go
Normal file
@ -0,0 +1,49 @@
|
||||
package lnd
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/namsral/flag"
|
||||
)
|
||||
|
||||
var (
|
||||
lnd *LNDClient
|
||||
Enabled bool
|
||||
)
|
||||
|
||||
type LNDClient struct {
|
||||
lndclient.GrpcLndServices
|
||||
}
|
||||
|
||||
func init() {
|
||||
var (
|
||||
lndCert string
|
||||
lndMacaroonDir string
|
||||
lndHost string
|
||||
rpcLndServices *lndclient.GrpcLndServices
|
||||
err error
|
||||
)
|
||||
if err = godotenv.Load(); err != nil {
|
||||
log.Fatalf("error loading env vars: %s", err)
|
||||
}
|
||||
flag.StringVar(&lndCert, "LND_CERT", "", "Path to LND TLS certificate")
|
||||
flag.StringVar(&lndMacaroonDir, "LND_MACAROON_DIR", "", "LND macaroon directory")
|
||||
flag.StringVar(&lndHost, "LND_HOST", "localhost:10001", "LND gRPC server address")
|
||||
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
|
||||
}
|
1
lnd/types.go
Normal file
1
lnd/types.go
Normal file
@ -0,0 +1 @@
|
||||
package lnd
|
28
main.go
Normal file
28
main.go
Normal file
@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"git.ekzyis.com/ekzyis/delphi.market/env"
|
||||
"git.ekzyis.com/ekzyis/delphi.market/server"
|
||||
)
|
||||
|
||||
var (
|
||||
s *server.Server
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.Printf("Commit: %s", env.CommitShortSha)
|
||||
log.Printf("Public URL: %s", env.PublicURL)
|
||||
log.Printf("Environment: %s", env.Env)
|
||||
|
||||
s = server.NewServer()
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := s.Start(fmt.Sprintf("%s:%d", "127.0.0.1", env.Port)); err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
@ -51,11 +51,11 @@
|
||||
<div class="font-mono word-wrap mb-1">{{.lnurl}}</div>
|
||||
<details class="font-mono mb-1 align-left">
|
||||
<summary>details</summary>
|
||||
<div>id: {{.Invoice.Id}}</div>
|
||||
<div>amount: {{div .Invoice.Msats 1000}} sats</div>
|
||||
<div>created: {{.Invoice.CreatedAt}}</div>
|
||||
<div>expiry : {{.Invoice.ExpiresAt}}</div>
|
||||
<div class="word-wrap">hash: {{.Invoice.PaymentHash}}</div>
|
||||
<div>id: {{.invoice.Id}}</div>
|
||||
<div>amount: {{div .invoice.Msats 1000}} sats</div>
|
||||
<div>created: {{.invoice.CreatedAt}}</div>
|
||||
<div>expiry : {{.invoice.ExpiresAt}}</div>
|
||||
<div class="word-wrap">hash: {{.invoice.Hash}}</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
@ -63,10 +63,10 @@
|
||||
<script>
|
||||
const statusElement = document.querySelector("#status")
|
||||
const label = document.querySelector("#status-label")
|
||||
const status = "{{.Status}}"
|
||||
const redirectUrl = "{{.RedirectURL}}"
|
||||
const status = "{{.status}}"
|
||||
const redirectUrl = "{{.redirectURL}}"
|
||||
function poll() {
|
||||
const invoiceId = "{{.Invoice.Id}}"
|
||||
const invoiceId = "{{.invoice.Id}}"
|
||||
const countdown = document.querySelector("#countdown")
|
||||
const redirect = () => {
|
||||
clearInterval(interval)
|
||||
|
65
server/auth/lnauth.go
Normal file
65
server/auth/lnauth.go
Normal file
@ -0,0 +1,65 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcutil/bech32"
|
||||
|
||||
"git.ekzyis.com/ekzyis/delphi.market/env"
|
||||
)
|
||||
|
||||
type LNAuth struct {
|
||||
K1 string
|
||||
LNURL string
|
||||
}
|
||||
|
||||
type LNAuthResponse struct {
|
||||
K1 string `query:"k1"`
|
||||
Sig string `query:"sig"`
|
||||
Key string `query:"key"`
|
||||
}
|
||||
|
||||
func NewLNAuth() (*LNAuth, error) {
|
||||
k1 := make([]byte, 32)
|
||||
_, err := rand.Read(k1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rand.Read error: %w", err)
|
||||
}
|
||||
k1hex := hex.EncodeToString(k1)
|
||||
url := []byte(fmt.Sprintf("https://%s/api/login?tag=login&k1=%s&action=login", env.PublicURL, k1hex))
|
||||
conv, err := bech32.ConvertBits(url, 8, 5, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bech32.ConvertBits error: %w", err)
|
||||
}
|
||||
lnurl, err := bech32.Encode("lnurl", conv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bech32.Encode error: %w", err)
|
||||
}
|
||||
return &LNAuth{k1hex, lnurl}, nil
|
||||
}
|
||||
|
||||
func VerifyLNAuth(r *LNAuthResponse) (bool, error) {
|
||||
var k1Bytes, sigBytes, keyBytes []byte
|
||||
k1Bytes, err := hex.DecodeString(r.K1)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("k1 decode error: %w", err)
|
||||
}
|
||||
sigBytes, err = hex.DecodeString(r.Sig)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("sig decode error: %w", err)
|
||||
}
|
||||
keyBytes, err = hex.DecodeString(r.Key)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("key decode error: %w", err)
|
||||
}
|
||||
key, err := btcec.ParsePubKey(keyBytes)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("key parse error: %w", err)
|
||||
}
|
||||
ecdsaKey := ecdsa.PublicKey{Curve: btcec.S256(), X: key.X(), Y: key.Y()}
|
||||
return ecdsa.VerifyASN1(&ecdsaKey, k1Bytes, sigBytes), nil
|
||||
}
|
45
server/error.go
Normal file
45
server/error.go
Normal file
@ -0,0 +1,45 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func httpErrorHandler(err error, c echo.Context) {
|
||||
c.Logger().Error(err)
|
||||
code := http.StatusInternalServerError
|
||||
if httpError, ok := err.(*echo.HTTPError); ok {
|
||||
code = httpError.Code
|
||||
}
|
||||
filePath := fmt.Sprintf("public/%d.html", code)
|
||||
var f *os.File
|
||||
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.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
|
||||
}
|
28
server/router/handler/index.go
Normal file
28
server/router/handler/index.go
Normal file
@ -0,0 +1,28 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
||||
"git.ekzyis.com/ekzyis/delphi.market/lib"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func HandleIndex(envVars map[string]any) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var (
|
||||
markets []db.Market
|
||||
err error
|
||||
data map[string]any
|
||||
)
|
||||
if err = db.FetchActiveMarkets(&markets); err != nil {
|
||||
return err
|
||||
}
|
||||
data = map[string]any{
|
||||
"session": c.Get("session"),
|
||||
"markets": markets,
|
||||
}
|
||||
lib.Merge(&data, &envVars)
|
||||
return c.Render(http.StatusOK, "index.html", data)
|
||||
}
|
||||
}
|
78
server/router/handler/invoice.go
Normal file
78
server/router/handler/invoice.go
Normal file
@ -0,0 +1,78 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
||||
"git.ekzyis.com/ekzyis/delphi.market/lib"
|
||||
"git.ekzyis.com/ekzyis/delphi.market/lnd"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
)
|
||||
|
||||
func HandleInvoiceAPI(envVars map[string]any) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var (
|
||||
invoiceId string
|
||||
invoice db.Invoice
|
||||
u db.User
|
||||
err error
|
||||
)
|
||||
invoiceId = c.Param("id")
|
||||
if err = db.FetchInvoice(&db.FetchInvoiceWhere{Id: invoiceId}, &invoice); err == sql.ErrNoRows {
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if u = c.Get("session").(db.User); invoice.Pubkey != u.Pubkey {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized)
|
||||
}
|
||||
invoice.Preimage = ""
|
||||
return c.JSON(http.StatusOK, invoice)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleInvoice(envVars map[string]any) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var (
|
||||
invoiceId string
|
||||
invoice db.Invoice
|
||||
u db.User
|
||||
hash lntypes.Hash
|
||||
qr string
|
||||
status string
|
||||
err error
|
||||
)
|
||||
invoiceId = c.Param("id")
|
||||
if err = db.FetchInvoice(&db.FetchInvoiceWhere{Id: invoiceId}, &invoice); err == sql.ErrNoRows {
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if u = c.Get("session").(db.User); invoice.Pubkey != u.Pubkey {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized)
|
||||
}
|
||||
if hash, err = lntypes.MakeHashFromStr(invoice.Hash); err != nil {
|
||||
return err
|
||||
}
|
||||
go lnd.CheckInvoice(hash)
|
||||
if qr, err = lib.ToQR(invoice.PaymentRequest); err != nil {
|
||||
return err
|
||||
}
|
||||
if invoice.ConfirmedAt.Valid {
|
||||
status = "Paid"
|
||||
} else if time.Now().After(invoice.ExpiresAt) {
|
||||
status = "Expired"
|
||||
}
|
||||
data := map[string]any{
|
||||
"session": c.Get("session"),
|
||||
"invoice": invoice,
|
||||
"status": status,
|
||||
"lnurl": invoice.PaymentRequest,
|
||||
"qr": qr,
|
||||
}
|
||||
return c.Render(http.StatusOK, "invoice.html", data)
|
||||
}
|
||||
}
|
76
server/router/handler/login.go
Normal file
76
server/router/handler/login.go
Normal file
@ -0,0 +1,76 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
||||
"git.ekzyis.com/ekzyis/delphi.market/lib"
|
||||
"git.ekzyis.com/ekzyis/delphi.market/server/auth"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func HandleLogin(envVars map[string]any) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var (
|
||||
lnAuth *auth.LNAuth
|
||||
dbLnAuth db.LNAuth
|
||||
err error
|
||||
expires time.Time = time.Now().Add(60 * 60 * 24 * 365 * time.Second)
|
||||
qr string
|
||||
data map[string]any
|
||||
)
|
||||
if lnAuth, err = auth.NewLNAuth(); err != nil {
|
||||
return err
|
||||
}
|
||||
dbLnAuth = db.LNAuth{K1: lnAuth.K1, LNURL: lnAuth.LNURL}
|
||||
if err = db.CreateLNAuth(&dbLnAuth); err != nil {
|
||||
return err
|
||||
}
|
||||
c.SetCookie(&http.Cookie{Name: "session", HttpOnly: true, Path: "/", Value: dbLnAuth.SessionId, Secure: true, Expires: expires})
|
||||
if qr, err = lib.ToQR(lnAuth.LNURL); err != nil {
|
||||
return err
|
||||
}
|
||||
data = map[string]any{
|
||||
"session": c.Get("session"),
|
||||
"lnurl": lnAuth.LNURL,
|
||||
"qr": qr,
|
||||
}
|
||||
lib.Merge(&data, &envVars)
|
||||
return c.Render(http.StatusOK, "login.html", data)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleLoginCallback(envVars map[string]any) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var (
|
||||
query auth.LNAuthResponse
|
||||
sessionId string
|
||||
err error
|
||||
)
|
||||
if err := c.Bind(&query); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest)
|
||||
}
|
||||
if err = db.FetchSessionId(query.K1, &sessionId); err == sql.ErrNoRows {
|
||||
return echo.NewHTTPError(http.StatusNotFound, map[string]string{"reason": "session not found"})
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok, err := auth.VerifyLNAuth(&query); err != nil {
|
||||
return err
|
||||
} else if !ok {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, map[string]string{"reason": "bad signature"})
|
||||
}
|
||||
if err = db.CreateUser(&db.User{Pubkey: query.Key}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = db.CreateSession(&db.Session{Pubkey: query.Key, SessionId: sessionId}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = db.DeleteLNAuth(&db.LNAuth{K1: query.K1}); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]string{"status": "OK"})
|
||||
}
|
||||
}
|
30
server/router/handler/logout.go
Normal file
30
server/router/handler/logout.go
Normal file
@ -0,0 +1,30 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func HandleLogout(envVars map[string]any) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var (
|
||||
cookie *http.Cookie
|
||||
sessionId string
|
||||
err error
|
||||
)
|
||||
if cookie, err = c.Cookie("session"); err != nil {
|
||||
// cookie not found
|
||||
return c.Redirect(http.StatusSeeOther, "/")
|
||||
}
|
||||
sessionId = cookie.Value
|
||||
if err = db.DeleteSession(&db.Session{SessionId: sessionId}); err != nil {
|
||||
return err
|
||||
}
|
||||
// tell browser that cookie is expired and thus can be deleted
|
||||
c.SetCookie(&http.Cookie{Name: "session", HttpOnly: true, Path: "/", Value: sessionId, Secure: true, Expires: time.Now()})
|
||||
return c.Redirect(http.StatusSeeOther, "/")
|
||||
}
|
||||
}
|
121
server/router/handler/market.go
Normal file
121
server/router/handler/market.go
Normal file
@ -0,0 +1,121 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"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/lnd"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
)
|
||||
|
||||
func HandleMarket(envVars map[string]any) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var (
|
||||
marketId int64
|
||||
market db.Market
|
||||
shares []db.Share
|
||||
orders []db.Order
|
||||
err error
|
||||
data map[string]any
|
||||
)
|
||||
if marketId, err = strconv.ParseInt(c.Param("id"), 10, 64); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
|
||||
}
|
||||
if err = db.FetchMarket(int(marketId), &market); err == sql.ErrNoRows {
|
||||
return echo.NewHTTPError(http.StatusNotFound, "Not Found")
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = db.FetchShares(market.Id, &shares); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = db.FetchOrders(&db.FetchOrdersWhere{MarketId: market.Id, Confirmed: true}, &orders); err != nil {
|
||||
return err
|
||||
}
|
||||
data = map[string]any{
|
||||
"session": c.Get("session"),
|
||||
"Id": market.Id,
|
||||
"Description": market.Description,
|
||||
// shares are sorted by description in descending order
|
||||
// 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.Render(http.StatusOK, "market.html", data)
|
||||
}
|
||||
}
|
||||
|
||||
func HandlePostOrder(envVars map[string]any) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var (
|
||||
marketId string
|
||||
u db.User
|
||||
o db.Order
|
||||
invoice *db.Invoice
|
||||
msats int64
|
||||
data map[string]any
|
||||
qr string
|
||||
hash lntypes.Hash
|
||||
err error
|
||||
)
|
||||
marketId = c.Param("id")
|
||||
// TODO:
|
||||
// [ ] Step 0: If SELL order, check share balance of user
|
||||
// [x] Create HODL invoice
|
||||
// [x] Create (unconfirmed) order
|
||||
// [ ] Find matching orders
|
||||
// [ ] Settle invoice when matching order was found,
|
||||
// else cancel invoice if expired
|
||||
|
||||
// parse body
|
||||
if err := c.Bind(&o); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest)
|
||||
}
|
||||
u = c.Get("session").(db.User)
|
||||
o.Pubkey = u.Pubkey
|
||||
msats = o.Quantity * o.Price * 1000
|
||||
|
||||
// TODO: if SELL order, check share balance of user
|
||||
|
||||
// Create HODL invoice
|
||||
if invoice, err = lnd.CreateInvoice(o.Pubkey, msats); err != nil {
|
||||
return err
|
||||
}
|
||||
// Create QR code to pay HODL invoice
|
||||
if qr, err = lib.ToQR(invoice.PaymentRequest); err != nil {
|
||||
return err
|
||||
}
|
||||
if hash, err = lntypes.MakeHashFromStr(invoice.Hash); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start goroutine to poll status and update invoice in background
|
||||
go lnd.CheckInvoice(hash)
|
||||
|
||||
// Create (unconfirmed) order
|
||||
o.InvoiceId = invoice.Id
|
||||
if err := db.CreateOrder(&o); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: find matching orders
|
||||
|
||||
data = map[string]any{
|
||||
"session": c.Get("session"),
|
||||
"lnurl": invoice.PaymentRequest,
|
||||
"qr": qr,
|
||||
"invoice": *invoice,
|
||||
"redirectURL": fmt.Sprintf("https://%s/market/%s", env.PublicURL, marketId),
|
||||
}
|
||||
lib.Merge(&data, &envVars)
|
||||
return c.Render(http.StatusPaymentRequired, "invoice.html", data)
|
||||
}
|
||||
}
|
29
server/router/handler/session.go
Normal file
29
server/router/handler/session.go
Normal file
@ -0,0 +1,29 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func HandleCheckSession(envVars map[string]any) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var (
|
||||
cookie *http.Cookie
|
||||
s db.Session
|
||||
err error
|
||||
)
|
||||
if cookie, err = c.Cookie("session"); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, map[string]string{"reason": "cookie required"})
|
||||
}
|
||||
s = db.Session{SessionId: cookie.Value}
|
||||
if err = db.FetchSession(&s); err == sql.ErrNoRows {
|
||||
return echo.NewHTTPError(http.StatusNotFound, map[string]string{"reason": "session not found"})
|
||||
} else if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError)
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]string{"pubkey": s.Pubkey})
|
||||
}
|
||||
}
|
31
server/router/handler/user.go
Normal file
31
server/router/handler/user.go
Normal file
@ -0,0 +1,31 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
||||
"git.ekzyis.com/ekzyis/delphi.market/lib"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func HandleUser(envVars map[string]any) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var (
|
||||
u db.User
|
||||
orders []db.Order
|
||||
err error
|
||||
data map[string]any
|
||||
)
|
||||
u = c.Get("session").(db.User)
|
||||
if err = db.FetchOrders(&db.FetchOrdersWhere{Pubkey: u.Pubkey}, &orders); err != nil {
|
||||
return err
|
||||
}
|
||||
data = map[string]any{
|
||||
"session": c.Get("session"),
|
||||
"user": u,
|
||||
"Orders": orders,
|
||||
}
|
||||
lib.Merge(&data, &envVars)
|
||||
return c.Render(http.StatusOK, "user.html", data)
|
||||
}
|
||||
}
|
19
server/router/middleware/lnd.go
Normal file
19
server/router/middleware/lnd.go
Normal file
@ -0,0 +1,19 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.ekzyis.com/ekzyis/delphi.market/lnd"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func LNDGuard(envVars map[string]any) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
if lnd.Enabled {
|
||||
return next(c)
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
}
|
51
server/router/middleware/session.go
Normal file
51
server/router/middleware/session.go
Normal file
@ -0,0 +1,51 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func Session(envVars map[string]any) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var (
|
||||
cookie *http.Cookie
|
||||
err error
|
||||
s *db.Session
|
||||
u *db.User
|
||||
)
|
||||
if cookie, err = c.Cookie("session"); err != nil {
|
||||
// cookie not found
|
||||
return next(c)
|
||||
}
|
||||
s = &db.Session{SessionId: cookie.Value}
|
||||
if err = db.FetchSession(s); err == nil {
|
||||
// session found
|
||||
u = &db.User{Pubkey: s.Pubkey, LastSeen: time.Now()}
|
||||
if err = db.UpdateUser(u); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Set("session", *u)
|
||||
} else if err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SessionGuard(envVars map[string]any) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
session := c.Get("session")
|
||||
if session == nil {
|
||||
return c.Redirect(http.StatusTemporaryRedirect, "/login")
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
44
server/router/router.go
Normal file
44
server/router/router.go
Normal file
@ -0,0 +1,44 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"git.ekzyis.com/ekzyis/delphi.market/env"
|
||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/handler"
|
||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/middleware"
|
||||
)
|
||||
|
||||
func AddRoutes(e *echo.Echo) {
|
||||
envVars := map[string]any{
|
||||
"PUBLIC_URL": env.PublicURL,
|
||||
"COMMIT_SHORT_SHA": env.CommitShortSha,
|
||||
"COMMIT_LONG_SHA": env.CommitLongSha,
|
||||
"VERSION": env.Version,
|
||||
}
|
||||
e.Use(middleware.Session(envVars))
|
||||
e.GET("/", handler.HandleIndex(envVars))
|
||||
e.GET("/login", handler.HandleLogin(envVars))
|
||||
e.GET("/api/login", handler.HandleLoginCallback(envVars))
|
||||
e.GET("/api/session", handler.HandleCheckSession(envVars))
|
||||
e.POST("/logout", handler.HandleLogout(envVars))
|
||||
e.GET("/user",
|
||||
handler.HandleUser(envVars),
|
||||
middleware.SessionGuard(envVars))
|
||||
e.GET("/market/:id",
|
||||
handler.HandleMarket(envVars),
|
||||
middleware.SessionGuard(envVars))
|
||||
e.POST("/market/:id/order",
|
||||
handler.HandlePostOrder(envVars),
|
||||
middleware.SessionGuard(envVars),
|
||||
middleware.LNDGuard(envVars))
|
||||
e.GET("/invoice/:id",
|
||||
handler.HandleInvoice(envVars),
|
||||
middleware.SessionGuard(envVars),
|
||||
middleware.LNDGuard(envVars),
|
||||
)
|
||||
e.GET("/api/invoice/:id",
|
||||
handler.HandleInvoiceAPI(envVars),
|
||||
middleware.SessionGuard(envVars),
|
||||
middleware.LNDGuard(envVars),
|
||||
)
|
||||
}
|
59
server/router/template.go
Normal file
59
server/router/template.go
Normal file
@ -0,0 +1,59 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
|
||||
type Template struct {
|
||||
templates *template.Template
|
||||
}
|
||||
|
||||
type Number interface {
|
||||
constraints.Integer | constraints.Float
|
||||
}
|
||||
|
||||
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
|
||||
return t.templates.ExecuteTemplate(w, name, data)
|
||||
}
|
||||
|
||||
var (
|
||||
T *Template
|
||||
)
|
||||
|
||||
func init() {
|
||||
T = &Template{
|
||||
templates: template.Must(template.New("").Funcs(template.FuncMap{
|
||||
"add": add[int64],
|
||||
"sub": sub[int64],
|
||||
"div": div[int64],
|
||||
"substr": substr,
|
||||
}).ParseGlob("pages/**.html")),
|
||||
}
|
||||
}
|
||||
|
||||
func add[T Number](arg1 T, arg2 T) T {
|
||||
return arg1 + arg2
|
||||
}
|
||||
|
||||
func sub[T Number](arg1 T, arg2 T) T {
|
||||
return arg1 - arg2
|
||||
}
|
||||
|
||||
func div[T Number](arg1 T, arg2 T) T {
|
||||
return arg1 / arg2
|
||||
}
|
||||
|
||||
func substr(s string, start, length int) string {
|
||||
if start < 0 || start >= len(s) {
|
||||
return ""
|
||||
}
|
||||
end := start + length
|
||||
if end > len(s) {
|
||||
end = len(s)
|
||||
}
|
||||
return s[start:end]
|
||||
}
|
31
server/server.go
Normal file
31
server/server.go
Normal file
@ -0,0 +1,31 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
|
||||
"git.ekzyis.com/ekzyis/delphi.market/server/router"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
*echo.Echo
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
e := echo.New()
|
||||
|
||||
e.Static("/", "public")
|
||||
|
||||
e.Renderer = router.T
|
||||
|
||||
router.AddRoutes(e)
|
||||
|
||||
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
||||
Format: "${time_custom} ${method} ${uri} ${status}\n",
|
||||
CustomTimeFormat: "2006-01-02 15:04:05.00000-0700",
|
||||
}))
|
||||
|
||||
e.HTTPErrorHandler = httpErrorHandler
|
||||
|
||||
return &Server{e}
|
||||
}
|
202
src/auth.go
202
src/auth.go
@ -1,202 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcutil/bech32"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type LnAuth struct {
|
||||
k1 string
|
||||
lnurl string
|
||||
}
|
||||
|
||||
type LnAuthResponse struct {
|
||||
K1 string `query:"k1"`
|
||||
Sig string `query:"sig"`
|
||||
Key string `query:"key"`
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
Pubkey string
|
||||
}
|
||||
|
||||
func lnAuth() (*LnAuth, error) {
|
||||
k1 := make([]byte, 32)
|
||||
_, err := rand.Read(k1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rand.Read error: %w", err)
|
||||
}
|
||||
k1hex := hex.EncodeToString(k1)
|
||||
url := []byte(fmt.Sprintf("https://%s/api/login?tag=login&k1=%s&action=login", PUBLIC_URL, k1hex))
|
||||
conv, err := bech32.ConvertBits(url, 8, 5, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bech32.ConvertBits error: %w", err)
|
||||
}
|
||||
lnurl, err := bech32.Encode("lnurl", conv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bech32.Encode error: %w", err)
|
||||
}
|
||||
return &LnAuth{k1hex, lnurl}, nil
|
||||
}
|
||||
|
||||
func lnAuthVerify(r *LnAuthResponse) (bool, error) {
|
||||
var k1Bytes, sigBytes, keyBytes []byte
|
||||
k1Bytes, err := hex.DecodeString(r.K1)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("k1 decode error: %w", err)
|
||||
}
|
||||
sigBytes, err = hex.DecodeString(r.Sig)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("sig decode error: %w", err)
|
||||
}
|
||||
keyBytes, err = hex.DecodeString(r.Key)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("key decode error: %w", err)
|
||||
}
|
||||
key, err := btcec.ParsePubKey(keyBytes)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("key parse error: %w", err)
|
||||
}
|
||||
ecdsaKey := ecdsa.PublicKey{Curve: btcec.S256(), X: key.X(), Y: key.Y()}
|
||||
return ecdsa.VerifyASN1(&ecdsaKey, k1Bytes, sigBytes), nil
|
||||
}
|
||||
|
||||
func login(c echo.Context) error {
|
||||
lnauth, err := lnAuth()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var sessionId string
|
||||
err = db.QueryRow("INSERT INTO lnauth(k1, lnurl) VALUES($1, $2) RETURNING session_id", lnauth.k1, lnauth.lnurl).Scan(&sessionId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expires := time.Now().Add(60 * 60 * 24 * 365 * time.Second)
|
||||
c.SetCookie(&http.Cookie{Name: "session", HttpOnly: true, Path: "/", Value: sessionId, Secure: true, Expires: expires})
|
||||
qr, err := ToQR(lnauth.lnurl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Render(http.StatusOK, "login.html", map[string]any{"session": c.Get("session"), "PUBLIC_URL": PUBLIC_URL, "lnurl": lnauth.lnurl, "qr": qr})
|
||||
}
|
||||
|
||||
func verifyLogin(c echo.Context) error {
|
||||
var query LnAuthResponse
|
||||
if err := c.Bind(&query); err != nil {
|
||||
c.Logger().Error(err)
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"status": "ERROR", "reason": "bad request"})
|
||||
}
|
||||
var sessionId string
|
||||
err := db.QueryRow("SELECT session_id FROM lnauth WHERE k1 = $1", query.K1).Scan(&sessionId)
|
||||
if err == sql.ErrNoRows {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"status": "ERROR", "reason": "unknown k1"})
|
||||
} else if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"})
|
||||
}
|
||||
ok, err := lnAuthVerify(&query)
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"})
|
||||
}
|
||||
if !ok {
|
||||
c.Logger().Error("bad signature")
|
||||
return c.JSON(http.StatusUnauthorized, map[string]string{"status": "ERROR", "reason": "bad signature"})
|
||||
}
|
||||
_, err = db.Exec("INSERT INTO users(pubkey) VALUES ($1) ON CONFLICT(pubkey) DO UPDATE SET last_seen = CURRENT_TIMESTAMP", query.Key)
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"})
|
||||
}
|
||||
_, err = db.Exec("INSERT INTO sessions(pubkey, session_id) VALUES($1, $2)", query.Key, sessionId)
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"})
|
||||
}
|
||||
_, err = db.Exec("DELETE FROM lnauth WHERE k1 = $1", query.K1)
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]string{"status": "OK"})
|
||||
}
|
||||
|
||||
func checkSession(c echo.Context) error {
|
||||
cookie, err := c.Cookie("session")
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"})
|
||||
}
|
||||
sessionId := cookie.Value
|
||||
var pubkey string
|
||||
err = db.QueryRow("SELECT pubkey FROM sessions WHERE session_id = $1", sessionId).Scan(&pubkey)
|
||||
if err == sql.ErrNoRows {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"status": "Not Found", "message": "session not found"})
|
||||
} else if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]string{"pubkey": pubkey})
|
||||
}
|
||||
|
||||
func sessionHandler(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
cookie, err := c.Cookie("session")
|
||||
if err != nil {
|
||||
// cookie not found
|
||||
return next(c)
|
||||
}
|
||||
sessionId := cookie.Value
|
||||
var pubkey string
|
||||
err = db.QueryRow("SELECT pubkey FROM sessions WHERE session_id = $1", sessionId).Scan(&pubkey)
|
||||
if err == nil {
|
||||
// session found
|
||||
_, err = db.Exec("UPDATE users SET last_seen = CURRENT_TIMESTAMP WHERE pubkey = $1", pubkey)
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"})
|
||||
}
|
||||
c.Set("session", Session{pubkey})
|
||||
} else if err != sql.ErrNoRows {
|
||||
c.Logger().Error(err)
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"})
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
func sessionGuard(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
session := c.Get("session")
|
||||
if session == nil {
|
||||
return c.Redirect(http.StatusTemporaryRedirect, "/login")
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
func logout(c echo.Context) error {
|
||||
cookie, err := c.Cookie("session")
|
||||
if err != nil {
|
||||
// cookie not found
|
||||
return c.Redirect(http.StatusSeeOther, "/")
|
||||
}
|
||||
sessionId := cookie.Value
|
||||
_, err = db.Exec("DELETE FROM sessions where session_id = $1", sessionId)
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return err
|
||||
}
|
||||
// tell browser that cookie is expired and thus can be deleted
|
||||
c.SetCookie(&http.Cookie{Name: "session", HttpOnly: true, Path: "/", Value: sessionId, Secure: true, Expires: time.Now()})
|
||||
return c.Redirect(http.StatusSeeOther, "/")
|
||||
}
|
188
src/db.go
188
src/db.go
@ -1,188 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/namsral/flag"
|
||||
)
|
||||
|
||||
var (
|
||||
DbUrl string
|
||||
db *DB
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
func init() {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Fatal("Error loading .env file")
|
||||
}
|
||||
flag.StringVar(&DbUrl, "DATABASE_URL", "", "Database URL")
|
||||
flag.Parse()
|
||||
validateFlags()
|
||||
db = initDb()
|
||||
_, err = db.Exec("SELECT 1")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func initDb() *DB {
|
||||
db, err := sql.Open("postgres", DbUrl)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return &DB{DB: db}
|
||||
}
|
||||
|
||||
func validateFlags() {
|
||||
if DbUrl == "" {
|
||||
log.Fatal("DATABASE_URL not set")
|
||||
}
|
||||
}
|
||||
|
||||
func (db *DB) FetchMarket(marketId int, market *Market) error {
|
||||
if err := db.QueryRow("SELECT id, description FROM markets WHERE id = $1", marketId).Scan(&market.Id, &market.Description); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var share Share
|
||||
rows.Scan(&share.Id, &share.MarketId, &share.Description)
|
||||
*shares = append(*shares, share)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type FetchOrdersWhere struct {
|
||||
MarketId int
|
||||
Pubkey string
|
||||
Confirmed bool
|
||||
}
|
||||
|
||||
func (db *DB) FetchOrders(where *FetchOrdersWhere, orders *[]Order) error {
|
||||
query := "" +
|
||||
"SELECT o.id, share_id, o.pubkey, o.side, o.quantity, o.price, o.invoice_id, o.created_at, s.description, s.market_id, i.confirmed_at " +
|
||||
"FROM orders o " +
|
||||
"JOIN invoices i ON o.invoice_id = i.id " +
|
||||
"JOIN shares s ON o.share_id = s.id " +
|
||||
"WHERE "
|
||||
var args []any
|
||||
if where.MarketId > 0 {
|
||||
query += "share_id = ANY(SELECT id FROM shares WHERE market_id = $1) "
|
||||
args = append(args, where.MarketId)
|
||||
} else if where.Pubkey != "" {
|
||||
query += "o.pubkey = $1 "
|
||||
args = append(args, where.Pubkey)
|
||||
}
|
||||
if where.Confirmed {
|
||||
query += "AND i.confirmed_at IS NOT NULL "
|
||||
}
|
||||
query += "AND (i.confirmed_at IS NOT NULL OR i.expires_at > CURRENT_TIMESTAMP) "
|
||||
query += "ORDER BY price DESC"
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var order Order
|
||||
rows.Scan(&order.Id, &order.ShareId, &order.Pubkey, &order.Side, &order.Quantity, &order.Price, &order.InvoiceId, &order.CreatedAt, &order.Share.Description, &order.Share.MarketId, &order.Invoice.ConfirmedAt)
|
||||
*orders = append(*orders, order)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) CreateOrder(order *Order) error {
|
||||
if _, err := db.Exec(""+
|
||||
"INSERT INTO orders(share_id, pubkey, side, quantity, price, invoice_id) "+
|
||||
"VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
order.ShareId, order.Pubkey, order.Side, order.Quantity, order.Price, order.InvoiceId); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) CreateInvoice(invoice *Invoice) error {
|
||||
if err := db.QueryRow(""+
|
||||
"INSERT INTO invoices(pubkey, msats, preimage, hash, bolt11, created_at, expires_at) "+
|
||||
"VALUES($1, $2, $3, $4, $5, $6, $7) "+
|
||||
"RETURNING id",
|
||||
invoice.Pubkey, invoice.Msats, invoice.Preimage, invoice.PaymentHash, invoice.PaymentRequest, invoice.CreatedAt, invoice.ExpiresAt).Scan(&invoice.Id); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type FetchInvoiceWhere struct {
|
||||
Id string
|
||||
Hash string
|
||||
}
|
||||
|
||||
func (db *DB) FetchInvoice(where *FetchInvoiceWhere, invoice *Invoice) error {
|
||||
query := "SELECT id, pubkey, msats, preimage, hash, bolt11, created_at, expires_at, confirmed_at, held_since FROM invoices "
|
||||
var args []any
|
||||
if where.Id != "" {
|
||||
query += "WHERE id = $1"
|
||||
args = append(args, where.Id)
|
||||
} else if where.Hash != "" {
|
||||
query += "WHERE hash = $1"
|
||||
args = append(args, where.Hash)
|
||||
}
|
||||
if err := db.QueryRow(query, args...).Scan(&invoice.Id, &invoice.Pubkey, &invoice.Msats, &invoice.Preimage, &invoice.PaymentHash, &invoice.PaymentRequest, &invoice.CreatedAt, &invoice.ExpiresAt, &invoice.ConfirmedAt, &invoice.HeldSince); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type FetchInvoicesWhere struct {
|
||||
Expired bool
|
||||
}
|
||||
|
||||
func (db *DB) FetchInvoices(where *FetchInvoicesWhere, invoices *[]Invoice) error {
|
||||
query := "" +
|
||||
"SELECT id, msats, msats_received, preimage, hash, bolt11, created_at, expires_at, confirmed_at, held_since " +
|
||||
"FROM invoices i "
|
||||
if where.Expired {
|
||||
query += "WHERE i.expires_at <= CURRENT_TIMESTAMP"
|
||||
} else {
|
||||
query += "WHERE i.expires_at > CURRENT_TIMESTAMP"
|
||||
}
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var inv Invoice
|
||||
rows.Scan(&inv.Id, &inv.Msats, &inv.ReceivedMsats, &inv.Preimage, &inv.PaymentHash, &inv.PaymentRequest, &inv.CreatedAt, &inv.ExpiresAt, &inv.ConfirmedAt, &inv.HeldSince)
|
||||
*invoices = append(*invoices, inv)
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) FetchUser(pubkey string, user *User) error {
|
||||
return db.QueryRow("SELECT pubkey FROM users WHERE pubkey = $1", pubkey).Scan(&user.Pubkey)
|
||||
}
|
222
src/lnd.go
222
src/lnd.go
@ -1,222 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/namsral/flag"
|
||||
)
|
||||
|
||||
var (
|
||||
LndCert string
|
||||
LndMacaroonDir string
|
||||
LndHost string
|
||||
lnd *LndClient
|
||||
lndEnabled bool
|
||||
)
|
||||
|
||||
type LndClient struct {
|
||||
lndclient.GrpcLndServices
|
||||
}
|
||||
|
||||
func init() {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Println("Error loading .env file")
|
||||
}
|
||||
flag.StringVar(&LndCert, "LND_CERT", "", "Path to LND TLS certificate")
|
||||
flag.StringVar(&LndMacaroonDir, "LND_MACAROON_DIR", "", "LND macaroon directory")
|
||||
flag.StringVar(&LndHost, "LND_HOST", "localhost:10001", "LND gRPC server address")
|
||||
flag.Parse()
|
||||
lndEnabled = false
|
||||
rpcLndServices, err := lndclient.NewLndServices(&lndclient.LndServicesConfig{
|
||||
LndAddress: LndHost,
|
||||
MacaroonDir: LndMacaroonDir,
|
||||
TLSPath: LndCert,
|
||||
Network: lndclient.NetworkRegtest,
|
||||
})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
lnd = &LndClient{GrpcLndServices: *rpcLndServices}
|
||||
ver := lnd.Version
|
||||
log.Printf("Connected to %s running LND v%s", LndHost, ver.Version)
|
||||
lndEnabled = true
|
||||
|
||||
}
|
||||
|
||||
func lndGuard(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
if lndEnabled {
|
||||
return next(c)
|
||||
}
|
||||
return serveError(c, 405)
|
||||
}
|
||||
}
|
||||
|
||||
func (lnd *LndClient) GenerateNewPreimage() (lntypes.Preimage, error) {
|
||||
randomBytes := make([]byte, 32)
|
||||
_, err := io.ReadFull(rand.Reader, randomBytes)
|
||||
if err != nil {
|
||||
return lntypes.Preimage{}, err
|
||||
}
|
||||
preimage, err := lntypes.MakePreimage(randomBytes)
|
||||
if err != nil {
|
||||
return lntypes.Preimage{}, err
|
||||
}
|
||||
return preimage, nil
|
||||
}
|
||||
|
||||
func (lnd *LndClient) CreateInvoice(pubkey string, msats int) (*Invoice, error) {
|
||||
expiry := time.Hour
|
||||
preimage, err := lnd.GenerateNewPreimage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hash := preimage.Hash()
|
||||
paymentRequest, err := lnd.Invoices.AddHoldInvoice(context.TODO(), &invoicesrpc.AddInvoiceData{
|
||||
Hash: &hash,
|
||||
Value: lnwire.MilliSatoshi(msats),
|
||||
Expiry: int64(expiry / time.Millisecond),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lnInvoice, err := lnd.Client.LookupInvoice(context.TODO(), hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbInvoice := Invoice{
|
||||
Session: Session{pubkey},
|
||||
Msats: msats,
|
||||
Preimage: preimage.String(),
|
||||
PaymentRequest: paymentRequest,
|
||||
PaymentHash: hash.String(),
|
||||
CreatedAt: lnInvoice.CreationDate,
|
||||
ExpiresAt: lnInvoice.CreationDate.Add(expiry),
|
||||
}
|
||||
if err := db.CreateInvoice(&dbInvoice); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dbInvoice, nil
|
||||
}
|
||||
|
||||
func (lnd *LndClient) CheckInvoice(hash lntypes.Hash) {
|
||||
if !lndEnabled {
|
||||
log.Printf("LND disabled, skipping checking invoice: hash=%s", hash)
|
||||
return
|
||||
}
|
||||
|
||||
var invoice Invoice
|
||||
if err := db.FetchInvoice(&FetchInvoiceWhere{Hash: hash.String()}, &invoice); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
loopPause := 5 * time.Second
|
||||
handleLoopError := func(err error) {
|
||||
log.Println(err)
|
||||
time.Sleep(loopPause)
|
||||
}
|
||||
|
||||
for {
|
||||
log.Printf("lookup invoice: hash=%s", hash)
|
||||
lnInvoice, err := lnd.Client.LookupInvoice(context.TODO(), hash)
|
||||
if err != nil {
|
||||
handleLoopError(err)
|
||||
continue
|
||||
}
|
||||
if time.Now().After(invoice.ExpiresAt) {
|
||||
if err := lnd.Invoices.CancelInvoice(context.TODO(), hash); err != nil {
|
||||
handleLoopError(err)
|
||||
continue
|
||||
}
|
||||
log.Printf("invoice expired: hash=%s", hash)
|
||||
break
|
||||
}
|
||||
if lnInvoice.AmountPaid > 0 {
|
||||
preimage, err := lntypes.MakePreimageFromStr(invoice.Preimage)
|
||||
if err != nil {
|
||||
handleLoopError(err)
|
||||
continue
|
||||
}
|
||||
// TODO settle invoice after matching order was found
|
||||
if err := lnd.Invoices.SettleInvoice(context.TODO(), preimage); err != nil {
|
||||
handleLoopError(err)
|
||||
continue
|
||||
}
|
||||
if err := db.ConfirmInvoice(hash.String(), time.Now(), int(lnInvoice.AmountPaid)); err != nil {
|
||||
handleLoopError(err)
|
||||
continue
|
||||
}
|
||||
log.Printf("invoice confirmed: hash=%s", hash)
|
||||
break
|
||||
}
|
||||
time.Sleep(loopPause)
|
||||
}
|
||||
}
|
||||
|
||||
func invoice(c echo.Context) error {
|
||||
invoiceId := c.Param("id")
|
||||
var invoice Invoice
|
||||
if err := db.FetchInvoice(&FetchInvoiceWhere{Id: invoiceId}, &invoice); err == sql.ErrNoRows {
|
||||
return echo.NewHTTPError(http.StatusNotFound, "Not Found")
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
session := c.Get("session").(Session)
|
||||
if invoice.Pubkey != session.Pubkey {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
hash, err := lntypes.MakeHashFromStr(invoice.PaymentHash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go lnd.CheckInvoice(hash)
|
||||
qr, err := ToQR(invoice.PaymentRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status := ""
|
||||
if invoice.ConfirmedAt.Valid {
|
||||
status = "Paid"
|
||||
} else if time.Now().After(invoice.ExpiresAt) {
|
||||
status = "Expired"
|
||||
}
|
||||
data := map[string]any{
|
||||
"session": c.Get("session"),
|
||||
"Invoice": invoice,
|
||||
"Status": status,
|
||||
"lnurl": invoice.PaymentRequest,
|
||||
"qr": qr,
|
||||
}
|
||||
return c.Render(http.StatusOK, "invoice.html", data)
|
||||
}
|
||||
|
||||
func invoiceStatus(c echo.Context) error {
|
||||
invoiceId := c.Param("id")
|
||||
var invoice Invoice
|
||||
if err := db.FetchInvoice(&FetchInvoiceWhere{Id: invoiceId}, &invoice); err == sql.ErrNoRows {
|
||||
return echo.NewHTTPError(http.StatusNotFound, "Not Found")
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
session := c.Get("session").(Session)
|
||||
if invoice.Pubkey != session.Pubkey {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
invoice.Preimage = ""
|
||||
return c.JSON(http.StatusOK, invoice)
|
||||
}
|
155
src/market.go
155
src/market.go
@ -1,155 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"gopkg.in/guregu/null.v4"
|
||||
)
|
||||
|
||||
type Market struct {
|
||||
Id int
|
||||
Description string
|
||||
Active bool
|
||||
}
|
||||
|
||||
type Share struct {
|
||||
Id string
|
||||
MarketId int
|
||||
Description string
|
||||
}
|
||||
|
||||
type Order struct {
|
||||
Session
|
||||
Share
|
||||
Market
|
||||
Invoice
|
||||
Id string
|
||||
ShareId string `form:"share_id"`
|
||||
Side string `form:"side"`
|
||||
Price int `form:"price"`
|
||||
Quantity int `form:"quantity"`
|
||||
InvoiceId string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type Invoice struct {
|
||||
Session
|
||||
Id string
|
||||
Msats int
|
||||
ReceivedMsats int
|
||||
Preimage string
|
||||
PaymentRequest string
|
||||
PaymentHash string
|
||||
CreatedAt time.Time
|
||||
ExpiresAt time.Time
|
||||
ConfirmedAt null.Time
|
||||
HeldSince null.Time
|
||||
}
|
||||
|
||||
func costFunction(b float64, q1 float64, q2 float64) float64 {
|
||||
// reference: http://blog.oddhead.com/2006/10/30/implementing-hansons-market-maker/
|
||||
return b * math.Log(math.Pow(math.E, q1/b)+math.Pow(math.E, q2/b))
|
||||
}
|
||||
|
||||
// logarithmic market scoring rule (LMSR) market maker from Robin Hanson:
|
||||
// https://mason.gmu.edu/~rhanson/mktscore.pdf1
|
||||
func BinaryLMSR(invariant int, funding int, q1 int, q2 int, dq1 int) float64 {
|
||||
b := float64(funding)
|
||||
fq1 := float64(q1)
|
||||
fq2 := float64(q2)
|
||||
fdq1 := float64(dq1)
|
||||
return costFunction(b, fq1+fdq1, fq2) - costFunction(b, fq1, fq2)
|
||||
}
|
||||
|
||||
func order(c echo.Context) error {
|
||||
marketId := c.Param("id")
|
||||
// (TBD) Step 0: If SELL order, check share balance of user
|
||||
// (TBD) Step 1: respond with HODL invoice
|
||||
o := new(Order)
|
||||
if err := c.Bind(o); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "bad request")
|
||||
}
|
||||
session := c.Get("session").(Session)
|
||||
o.Pubkey = session.Pubkey
|
||||
amount := o.Quantity * o.Price
|
||||
invoice, err := lnd.CreateInvoice(session.Pubkey, amount*1000)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.InvoiceId = invoice.Id
|
||||
if err := db.CreateOrder(o); err != nil {
|
||||
return err
|
||||
}
|
||||
qr, err := ToQR(invoice.PaymentRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash, err := lntypes.MakeHashFromStr(invoice.PaymentHash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go lnd.CheckInvoice(hash)
|
||||
data := map[string]any{
|
||||
"session": c.Get("session"),
|
||||
"ENV": ENV,
|
||||
"lnurl": invoice.PaymentRequest,
|
||||
"qr": qr,
|
||||
"Invoice": invoice,
|
||||
"RedirectURL": fmt.Sprintf("https://%s/market/%s", PUBLIC_URL, marketId),
|
||||
}
|
||||
return c.Render(http.StatusPaymentRequired, "invoice.html", data)
|
||||
// Step 2: After payment, confirm order if no matching order was found
|
||||
// if err := db.CreateOrder(o); err != nil {
|
||||
// if strings.Contains(err.Error(), "violates check constraint") {
|
||||
// return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
|
||||
// }
|
||||
// return err
|
||||
// }
|
||||
// (TBD) Step 3:
|
||||
// Settle hodl invoice when matching order was found
|
||||
// else cancel hodl invoice if expired
|
||||
// ...
|
||||
// return market(c)
|
||||
}
|
||||
|
||||
func market(c echo.Context) error {
|
||||
marketId, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
|
||||
}
|
||||
|
||||
var market Market
|
||||
if err = db.FetchMarket(int(marketId), &market); err == sql.ErrNoRows {
|
||||
return echo.NewHTTPError(http.StatusNotFound, "Not Found")
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var shares []Share
|
||||
if err = db.FetchShares(market.Id, &shares); err != nil {
|
||||
return err
|
||||
}
|
||||
var orders []Order
|
||||
if err = db.FetchOrders(&FetchOrdersWhere{MarketId: market.Id, Confirmed: true}, &orders); err != nil {
|
||||
return err
|
||||
}
|
||||
data := map[string]any{
|
||||
"session": c.Get("session"),
|
||||
"ENV": ENV,
|
||||
"Id": market.Id,
|
||||
"Description": market.Description,
|
||||
// shares are sorted by description in descending order
|
||||
// that's how we know that YES must be the first share
|
||||
"YesShare": shares[0],
|
||||
"NoShare": shares[1],
|
||||
"Orders": orders,
|
||||
}
|
||||
return c.Render(http.StatusOK, "market.html", data)
|
||||
}
|
108
src/router.go
108
src/router.go
@ -1,108 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type Template struct {
|
||||
templates *template.Template
|
||||
}
|
||||
|
||||
func add(arg1 int, arg2 int) int {
|
||||
return arg1 + arg2
|
||||
}
|
||||
|
||||
func sub(arg1 int, arg2 int) int {
|
||||
return arg1 - arg2
|
||||
}
|
||||
|
||||
func div(arg1 int, arg2 int) int {
|
||||
return arg1 / arg2
|
||||
}
|
||||
|
||||
func substr(s string, start, length int) string {
|
||||
if start < 0 || start >= len(s) {
|
||||
return ""
|
||||
}
|
||||
end := start + length
|
||||
if end > len(s) {
|
||||
end = len(s)
|
||||
}
|
||||
return s[start:end]
|
||||
}
|
||||
|
||||
var (
|
||||
FuncMap template.FuncMap = template.FuncMap{
|
||||
"add": add,
|
||||
"sub": sub,
|
||||
"div": div,
|
||||
"substr": substr,
|
||||
}
|
||||
)
|
||||
|
||||
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
|
||||
return t.templates.ExecuteTemplate(w, name, data)
|
||||
}
|
||||
|
||||
func index(c echo.Context) error {
|
||||
rows, err := db.Query("SELECT id, description, active FROM markets WHERE active = true")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
var markets []Market
|
||||
for rows.Next() {
|
||||
var market Market
|
||||
rows.Scan(&market.Id, &market.Description, &market.Active)
|
||||
markets = append(markets, market)
|
||||
}
|
||||
data := map[string]any{
|
||||
"session": c.Get("session"),
|
||||
"ENV": ENV,
|
||||
"markets": markets,
|
||||
"VERSION": VERSION,
|
||||
"COMMIT_LONG_SHA": COMMIT_LONG_SHA}
|
||||
return c.Render(http.StatusOK, "index.html", data)
|
||||
}
|
||||
|
||||
func serveError(c echo.Context, code int) error {
|
||||
f, err := os.Open(fmt.Sprintf("public/%d.html", code))
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return err
|
||||
}
|
||||
err = c.Stream(code, "text/html", f)
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func httpErrorHandler(err error, c echo.Context) {
|
||||
c.Logger().Error(err)
|
||||
code := http.StatusInternalServerError
|
||||
httpError, ok := err.(*echo.HTTPError)
|
||||
if ok {
|
||||
code = httpError.Code
|
||||
}
|
||||
filePath := fmt.Sprintf("public/%d.html", code)
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
serveError(c, 500)
|
||||
return
|
||||
}
|
||||
err = c.Stream(code, "text/html", f)
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
serveError(c, 500)
|
||||
return
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"github.com/namsral/flag"
|
||||
)
|
||||
|
||||
var (
|
||||
e *echo.Echo
|
||||
t *Template
|
||||
COMMIT_LONG_SHA string
|
||||
COMMIT_SHORT_SHA string
|
||||
VERSION string
|
||||
PORT int
|
||||
PUBLIC_URL string
|
||||
ENV string
|
||||
)
|
||||
|
||||
func execCmd(name string, args ...string) string {
|
||||
cmd := exec.Command(name, args...)
|
||||
stdout, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return strings.TrimSpace(string(stdout))
|
||||
}
|
||||
|
||||
func init() {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Fatal("Error loading .env file")
|
||||
}
|
||||
flag.StringVar(&PUBLIC_URL, "PUBLIC_URL", "delphi.market", "Public URL of website")
|
||||
flag.IntVar(&PORT, "PORT", 4321, "Server port")
|
||||
flag.StringVar(&ENV, "ENV", "development", "Specify for which environment files should be built")
|
||||
flag.Parse()
|
||||
e = echo.New()
|
||||
t = &Template{
|
||||
templates: template.Must(template.New("").Funcs(FuncMap).ParseGlob("pages/**.html")),
|
||||
}
|
||||
COMMIT_LONG_SHA = execCmd("git", "rev-parse", "HEAD")
|
||||
COMMIT_SHORT_SHA = execCmd("git", "rev-parse", "--short", "HEAD")
|
||||
VERSION = fmt.Sprintf("v0.0.0+%s", COMMIT_SHORT_SHA)
|
||||
log.Printf("Running commit %s", COMMIT_SHORT_SHA)
|
||||
log.Printf("Public URL: %s", PUBLIC_URL)
|
||||
log.Printf("Environment: %s", ENV)
|
||||
}
|
||||
|
||||
func main() {
|
||||
e.Static("/", "public")
|
||||
e.Renderer = t
|
||||
e.GET("/", index)
|
||||
e.GET("/login", login)
|
||||
e.GET("/api/login", verifyLogin)
|
||||
e.GET("/api/session", checkSession)
|
||||
e.POST("/logout", logout)
|
||||
e.GET("/user", sessionGuard(user))
|
||||
e.GET("/market/:id", sessionGuard(market))
|
||||
e.POST("/market/:id/order", sessionGuard(lndGuard(order)))
|
||||
e.GET("/invoice/:id", sessionGuard(lndGuard(invoice)))
|
||||
e.GET("/api/invoice/:id", sessionGuard(lndGuard(invoiceStatus)))
|
||||
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
||||
Format: "${time_custom} ${method} ${uri} ${status}\n",
|
||||
CustomTimeFormat: "2006-01-02 15:04:05.00000-0700",
|
||||
}))
|
||||
e.Use(sessionHandler)
|
||||
e.HTTPErrorHandler = httpErrorHandler
|
||||
if err := RunJobs(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err := e.Start(fmt.Sprintf("%s:%d", "127.0.0.1", PORT))
|
||||
if err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
29
src/user.go
29
src/user.go
@ -1,29 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Session
|
||||
}
|
||||
|
||||
func user(c echo.Context) error {
|
||||
session := c.Get("session").(Session)
|
||||
u := User{}
|
||||
if err := db.FetchUser(session.Pubkey, &u); err != nil {
|
||||
return err
|
||||
}
|
||||
var orders []Order
|
||||
if err := db.FetchOrders(&FetchOrdersWhere{Pubkey: session.Pubkey}, &orders); err != nil {
|
||||
return err
|
||||
}
|
||||
data := map[string]any{
|
||||
"session": c.Get("session"),
|
||||
"user": u,
|
||||
"Orders": orders,
|
||||
}
|
||||
return c.Render(http.StatusOK, "user.html", data)
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
)
|
||||
|
||||
func RunJobs() error {
|
||||
var invoices []Invoice
|
||||
if err := db.FetchInvoices(&FetchInvoicesWhere{Expired: false}, &invoices); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, inv := range invoices {
|
||||
hash, err := lntypes.MakeHashFromStr(inv.PaymentHash)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
go lnd.CheckInvoice(hash)
|
||||
}
|
||||
return nil
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user