Compare commits
2 Commits
e95bdff3b8
...
f195bee7e9
Author | SHA1 | Date | |
---|---|---|---|
f195bee7e9 | |||
698cc5c368 |
33
lnd/lnd.go
33
lnd/lnd.go
@ -34,20 +34,29 @@ func New(config *LNDConfig) (*LNDClient, error) {
|
|||||||
|
|
||||||
func (lnd *LNDClient) PollInvoices(db *db.DB) {
|
func (lnd *LNDClient) PollInvoices(db *db.DB) {
|
||||||
var (
|
var (
|
||||||
rows *sql.Rows
|
pending bool
|
||||||
hash lntypes.Hash
|
rows *sql.Rows
|
||||||
inv *lndclient.Invoice
|
hash lntypes.Hash
|
||||||
err error
|
inv *lndclient.Invoice
|
||||||
|
err error
|
||||||
)
|
)
|
||||||
for {
|
for {
|
||||||
// fetch all pending invoices
|
// fetch all pending invoices
|
||||||
if rows, err = db.Query("SELECT hash FROM invoices WHERE confirmed_at IS NULL AND expires_at > CURRENT_TIMESTAMP"); err != nil {
|
if rows, err = db.Query("SELECT hash, expires_at FROM invoices WHERE confirmed_at IS NULL AND expires_at > CURRENT_TIMESTAMP"); err != nil {
|
||||||
log.Printf("error checking invoices: %v", err)
|
log.Printf("error checking invoices: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pending = false
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var h string
|
pending = true
|
||||||
rows.Scan(&h)
|
|
||||||
|
var (
|
||||||
|
h string
|
||||||
|
expiresAt time.Time
|
||||||
|
)
|
||||||
|
rows.Scan(&h, &expiresAt)
|
||||||
|
|
||||||
if hash, err = lntypes.MakeHashFromStr(h); err != nil {
|
if hash, err = lntypes.MakeHashFromStr(h); err != nil {
|
||||||
log.Printf("error parsing hash: %v", err)
|
log.Printf("error parsing hash: %v", err)
|
||||||
continue
|
continue
|
||||||
@ -59,7 +68,7 @@ func (lnd *LNDClient) PollInvoices(db *db.DB) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !inv.State.IsFinal() {
|
if !inv.State.IsFinal() {
|
||||||
log.Printf("invoice pending: %s", h)
|
log.Printf("invoice pending: %s %s", h, expiresAt.Sub(time.Now()))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,6 +83,12 @@ func (lnd *LNDClient) PollInvoices(db *db.DB) {
|
|||||||
log.Printf("invoice expired: %s", h)
|
log.Printf("invoice expired: %s", h)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
|
// poll faster if there are pending invoices
|
||||||
|
if pending {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
} else {
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,16 @@
|
|||||||
transition: background-color 150ms ease-in;
|
transition: background-color 150ms ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
min-height: 85svh;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
#content {
|
||||||
|
min-height: 90svh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
@ -36,14 +46,16 @@
|
|||||||
a:not(.no-link),
|
a:not(.no-link),
|
||||||
button[hx-get],
|
button[hx-get],
|
||||||
button[hx-post],
|
button[hx-post],
|
||||||
button[type="submit"] {
|
button[type="submit"],
|
||||||
|
.button {
|
||||||
transition: background-color 150ms ease-in, color 150ms ease-in;
|
transition: background-color 150ms ease-in, color 150ms ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:not(.no-link):hover,
|
a:not(.no-link):hover,
|
||||||
button[hx-get]:hover,
|
button[hx-get]:hover,
|
||||||
button[hx-post]:hover,
|
button[hx-post]:hover,
|
||||||
button[type="submit"]:hover {
|
button[type="submit"]:hover,
|
||||||
|
.button:hover {
|
||||||
background-color: var(--color);
|
background-color: var(--color);
|
||||||
color: var(--background);
|
color: var(--background);
|
||||||
}
|
}
|
||||||
@ -54,7 +66,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
button[hx-post],
|
button[hx-post],
|
||||||
button[type="submit"] {
|
button[type="submit"],
|
||||||
|
.button {
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,6 +91,15 @@
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-reset {
|
||||||
|
color: var(--color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hitbox {
|
||||||
|
padding: 15px;
|
||||||
|
margin: -15px;
|
||||||
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0.5em 3em;
|
padding: 0.5em 3em;
|
||||||
@ -98,7 +120,7 @@
|
|||||||
color: var(--fg-error);
|
color: var(--fg-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
label.error:hover {
|
.label.error:hover {
|
||||||
background-color: var(--fg-error);
|
background-color: var(--fg-error);
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ func HandleInvoice(sc context.Context) echo.HandlerFunc {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if err = db.QueryRowContext(ctx, ""+
|
if err = db.QueryRowContext(ctx, ""+
|
||||||
"SELECT user_id, msats, COALESCE(msats_received, 0), expires_at, confirmed_at, bolt11, description "+
|
"SELECT user_id, msats, COALESCE(msats_received, 0), expires_at, confirmed_at, bolt11, COALESCE(description, '') "+
|
||||||
"FROM invoices "+
|
"FROM invoices "+
|
||||||
"WHERE hash = $1", hash).
|
"WHERE hash = $1", hash).
|
||||||
Scan(&inv.UserId, &inv.Msats, &inv.MsatsReceived, &inv.ExpiresAt, &inv.ConfirmedAt, &inv.Bolt11, &inv.Description); err != nil {
|
Scan(&inv.UserId, &inv.Msats, &inv.MsatsReceived, &inv.ExpiresAt, &inv.ConfirmedAt, &inv.Bolt11, &inv.Description); err != nil {
|
||||||
|
@ -3,9 +3,11 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages"
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components"
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components"
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/types"
|
"git.ekzyis.com/ekzyis/delphi.market/types"
|
||||||
"github.com/a-h/templ"
|
"github.com/a-h/templ"
|
||||||
@ -83,3 +85,27 @@ func HandleCreate(sc context.Context) echo.HandlerFunc {
|
|||||||
return components.Modal(qr).Render(context.RenderContext(sc, c), c.Response().Writer)
|
return components.Modal(qr).Render(context.RenderContext(sc, c), c.Response().Writer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HandleMarket(sc context.Context) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
var (
|
||||||
|
db = sc.Db
|
||||||
|
ctx = c.Request().Context()
|
||||||
|
id = c.Param("id")
|
||||||
|
market = types.Market{}
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if err = db.QueryRowContext(ctx, ""+
|
||||||
|
"SELECT id, question, description, end_date, user_id "+
|
||||||
|
"FROM markets "+
|
||||||
|
"WHERE id = $1", id).Scan(&market.Id, &market.Question, &market.Description, &market.EndDate, &market.UserId); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages.Market(market).Render(context.RenderContext(sc, c), c.Response().Writer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,20 +1,24 @@
|
|||||||
package components
|
package components
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
templ Invoice(hash string, bolt11 string, msats int, expiresIn int, paid bool, redirectUrl templ.SafeURL) {
|
templ Invoice(hash string, bolt11 string, msats int, expiresIn int, paid bool, redirectUrl templ.SafeURL) {
|
||||||
<div class="p-5 border border-muted bg-background text-center font-mono">
|
<div class="p-5 border border-muted bg-background text-center font-mono">
|
||||||
|
<div id="close" class="flex justify-end"><button class="w-fit text-muted hitbox hover:text-reset">X</button></div>
|
||||||
<div>Payment Required</div>
|
<div>Payment Required</div>
|
||||||
<div class="my-3">@Qr(bolt11, "lightning:"+bolt11)</div>
|
<div class="my-1">@Qr(bolt11, "lightning:"+bolt11)</div>
|
||||||
<div class="my-1">{ strconv.Itoa(msats/1000) } sats</div>
|
<div class="my-1">{ format(msats) }</div>
|
||||||
@InvoiceStatus(hash, expiresIn, paid, redirectUrl)
|
@InvoiceStatus(hash, expiresIn, paid, redirectUrl)
|
||||||
<div class="none" id="bolt11" bolt11-data={ templ.JSONString(bolt11) } hx-preserve></div>
|
<div class="none" id="bolt11-data" bolt11-data={ templ.JSONString(bolt11) } hx-preserve></div>
|
||||||
<script type="text/javascript" id="bolt11-js" hx-preserve>
|
<script type="text/javascript" id="bolt11-js" hx-preserve>
|
||||||
var $ = selector => document.querySelector(selector)
|
var $ = selector => document.querySelector(selector)
|
||||||
var bolt11 = JSON.parse($("#bolt11").getAttribute("bolt11-data"))
|
$("#close").addEventListener("click", function () {
|
||||||
console.log(bolt11)
|
// abort in-flight polls and prevent new polls
|
||||||
|
htmx.trigger("#poll", "htmx:abort")
|
||||||
|
$("#poll").addEventListener("htmx:beforeRequest", e => e.preventDefault())
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -23,11 +27,14 @@ templ InvoiceStatus(hash string, expiresIn int, paid bool, redirectUrl templ.Saf
|
|||||||
if paid {
|
if paid {
|
||||||
<div class="font-mono label success my-1">PAID</div>
|
<div class="font-mono label success my-1">PAID</div>
|
||||||
<div
|
<div
|
||||||
|
id="poll"
|
||||||
hx-get={ string(redirectUrl) }
|
hx-get={ string(redirectUrl) }
|
||||||
hx-trigger="load delay:3s"
|
hx-trigger="load delay:3s"
|
||||||
hx-target="#content"
|
hx-target="#content"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
hx-select="#content" />
|
hx-select="#content"
|
||||||
|
hx-push-url="true"
|
||||||
|
hx-select-oob="#modal" />
|
||||||
}
|
}
|
||||||
else if expiresIn <= 0 {
|
else if expiresIn <= 0 {
|
||||||
<div class="font-mono label error my-1">EXPIRED</div>
|
<div class="font-mono label error my-1">EXPIRED</div>
|
||||||
@ -59,10 +66,19 @@ templ InvoiceStatus(hash string, expiresIn int, paid bool, redirectUrl templ.Saf
|
|||||||
var interval = setInterval(_countdown, 1000)
|
var interval = setInterval(_countdown, 1000)
|
||||||
</script>
|
</script>
|
||||||
<div
|
<div
|
||||||
|
id="poll"
|
||||||
hx-get={ string(templ.SafeURL("/invoice/" + hash)) }
|
hx-get={ string(templ.SafeURL("/invoice/" + hash)) }
|
||||||
hx-trigger="load delay:1s"
|
hx-trigger="load delay:1s"
|
||||||
hx-target="#modal"
|
hx-target="#modal"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
hx-select="#modal" />
|
hx-select="#modal" />
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func format(msats int) string {
|
||||||
|
sats := msats / 1000
|
||||||
|
if sats == 1 {
|
||||||
|
return fmt.Sprintf("%d sat", sats)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d sats", sats)
|
||||||
}
|
}
|
@ -13,6 +13,14 @@ templ Modal(component templ.Component) {
|
|||||||
<div class="flex justify-center p-3 w-screen">
|
<div class="flex justify-center p-3 w-screen">
|
||||||
@component
|
@component
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var $ = selector => document.querySelector(selector)
|
||||||
|
$("#close").addEventListener("click", function () {
|
||||||
|
$("#modal").removeAttribute("class")
|
||||||
|
$("#modal").setAttribute("class", "hidden")
|
||||||
|
$("#modal").innerHTML = ""
|
||||||
|
})
|
||||||
|
</script>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
|
@ -13,11 +13,29 @@ templ Qr(value string, href string) {
|
|||||||
>
|
>
|
||||||
<img src={ "data:image/jpeg;base64," + qrEncode(value) }/>
|
<img src={ "data:image/jpeg;base64," + qrEncode(value) }/>
|
||||||
</a>
|
</a>
|
||||||
|
<small class="flex my-1 mx-auto">
|
||||||
|
<span class="block w-[188px] overflow-hidden">{ value }</span>
|
||||||
|
<button id="copy" class="ms-1 button w-[64px]" hx-preserve>copy</button>
|
||||||
|
</small>
|
||||||
|
@CopyButton(value)
|
||||||
} else {
|
} else {
|
||||||
<img src={ "data:image/jpeg;base64," + qrEncode(value) }/>
|
<img src={ "data:image/jpeg;base64," + qrEncode(value) }/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templ CopyButton(value string) {
|
||||||
|
<div class="none" id="copy-data" copy-data={ templ.JSONString(value) } hx-preserve></div>
|
||||||
|
<script type="text/javascript" id="copy-js" hx-preserve>
|
||||||
|
var $ = selector => document.querySelector(selector)
|
||||||
|
var value = JSON.parse($("#copy-data").getAttribute("copy-data"))
|
||||||
|
$("#copy").onclick = function () {
|
||||||
|
window.navigator.clipboard.writeText(value)
|
||||||
|
$("#copy").textContent = "copied"
|
||||||
|
setTimeout(() => $("#copy").textContent = "copy", 1000)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
|
||||||
func qrEncode(value string) string {
|
func qrEncode(value string) string {
|
||||||
png, err := qrcode.Encode(value, qrcode.Medium, 256)
|
png, err := qrcode.Encode(value, qrcode.Medium, 256)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -18,7 +18,6 @@ templ LnAuth(lnurl string, action string) {
|
|||||||
hx-push-url="true"
|
hx-push-url="true"
|
||||||
>
|
>
|
||||||
@components.Qr(lnurl, "lightning:"+lnurl)
|
@components.Qr(lnurl, "lightning:"+lnurl)
|
||||||
<small class="mx-auto w-[256px] my-1 break-words">{ lnurl }</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div hx-get="/session" hx-trigger="every 1s" hx-swap="none"></div>
|
<div hx-get="/session" hx-trigger="every 1s" hx-swap="none"></div>
|
||||||
</div>
|
</div>
|
||||||
|
28
server/router/pages/market.templ
Normal file
28
server/router/pages/market.templ
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package pages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components"
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ Market(m types.Market) {
|
||||||
|
<html>
|
||||||
|
@components.Head()
|
||||||
|
<body class="container">
|
||||||
|
@components.Nav()
|
||||||
|
<div id="content" class="flex flex-col">
|
||||||
|
<small>
|
||||||
|
@components.Figlet("random", "market")
|
||||||
|
</small>
|
||||||
|
<div class="text-center font-bold my-1">{ m.Question }</div>
|
||||||
|
<div class="my-1">{ m.Description }</div>
|
||||||
|
<div class="flex justify-center my-1">
|
||||||
|
<button class="label success mx-1">BET YES</button>
|
||||||
|
<button class="label error mx-1">BET NO</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@components.Modal(nil)
|
||||||
|
@components.Footer()
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
@ -16,6 +16,7 @@ func Init(e *echo.Echo, sc Context) {
|
|||||||
e.GET("/", handler.HandleIndex(sc))
|
e.GET("/", handler.HandleIndex(sc))
|
||||||
e.GET("/create", handler.HandleIndex(sc))
|
e.GET("/create", handler.HandleIndex(sc))
|
||||||
e.POST("/create", handler.HandleCreate(sc), middleware.SessionGuard(sc))
|
e.POST("/create", handler.HandleCreate(sc), middleware.SessionGuard(sc))
|
||||||
|
e.GET("/market/:id", handler.HandleMarket(sc))
|
||||||
e.GET("/about", handler.HandleAbout(sc))
|
e.GET("/about", handler.HandleAbout(sc))
|
||||||
|
|
||||||
e.GET("/login", handler.HandleAuth(sc, "login"))
|
e.GET("/login", handler.HandleAuth(sc, "login"))
|
||||||
|
@ -27,3 +27,11 @@ type Invoice struct {
|
|||||||
HeldSince bool
|
HeldSince bool
|
||||||
Description string
|
Description string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Market struct {
|
||||||
|
Id int
|
||||||
|
UserId int
|
||||||
|
Question string
|
||||||
|
Description string
|
||||||
|
EndDate time.Time
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user