Compare commits
2 Commits
4727f18958
...
d322a8599a
Author | SHA1 | Date | |
---|---|---|---|
d322a8599a | |||
07648c30f6 |
@ -24,10 +24,9 @@ CREATE TABLE invoices(
|
|||||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||||
msats BIGINT NOT NULL,
|
msats BIGINT NOT NULL,
|
||||||
msats_received BIGINT,
|
msats_received BIGINT,
|
||||||
preimage TEXT NOT NULL UNIQUE,
|
|
||||||
hash TEXT NOT NULL UNIQUE,
|
hash TEXT NOT NULL UNIQUE,
|
||||||
bolt11 TEXT NOT NULL,
|
bolt11 TEXT NOT NULL,
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
confirmed_at TIMESTAMP WITH TIME ZONE,
|
confirmed_at TIMESTAMP WITH TIME ZONE,
|
||||||
held_since TIMESTAMP WITH TIME ZONE,
|
held_since TIMESTAMP WITH TIME ZONE,
|
||||||
@ -37,7 +36,8 @@ CREATE TABLE invoices(
|
|||||||
CREATE TABLE markets(
|
CREATE TABLE markets(
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
description TEXT NOT NULL,
|
question TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
end_date TIMESTAMP WITH TIME ZONE NOT NULL,
|
end_date TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
settled_at TIMESTAMP WITH TIME ZONE,
|
settled_at TIMESTAMP WITH TIME ZONE,
|
||||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||||
|
@ -10,6 +10,10 @@
|
|||||||
--nostr: #8d45dd;
|
--nostr: #8d45dd;
|
||||||
--black: #000;
|
--black: #000;
|
||||||
--white: #fff;
|
--white: #fff;
|
||||||
|
--bg-success: #149e613d;
|
||||||
|
--fg-success: #35df8d;
|
||||||
|
--bg-error: #f5395e3d;
|
||||||
|
--fg-error: #ff7386;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@ -31,13 +35,15 @@
|
|||||||
|
|
||||||
a:not(.no-link),
|
a:not(.no-link),
|
||||||
button[hx-get],
|
button[hx-get],
|
||||||
button[hx-post] {
|
button[hx-post],
|
||||||
|
button[type="submit"] {
|
||||||
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 {
|
||||||
background-color: var(--color);
|
background-color: var(--color);
|
||||||
color: var(--background);
|
color: var(--background);
|
||||||
}
|
}
|
||||||
@ -47,13 +53,15 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[hx-post] {
|
button[hx-post],
|
||||||
|
button[type="submit"] {
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav a,
|
nav a,
|
||||||
button[hx-get],
|
button[hx-get],
|
||||||
button[hx-post] {
|
button[hx-post],
|
||||||
|
button[type="submit"] {
|
||||||
padding: 0 0.25em;
|
padding: 0 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,6 +78,31 @@
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
border: none;
|
||||||
|
padding: 0.5em 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label.success {
|
||||||
|
background-color: var(--bg-success);
|
||||||
|
color: var(--fg-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label.success:hover {
|
||||||
|
background-color: var(--fg-success);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label.error {
|
||||||
|
background-color: var(--bg-error);
|
||||||
|
color: var(--fg-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
label.error:hover {
|
||||||
|
background-color: var(--fg-error);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
.figlet {
|
.figlet {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -105,4 +138,8 @@
|
|||||||
.nostr:hover {
|
.nostr:hover {
|
||||||
filter: brightness(125%) drop-shadow(0 0 0.33rem var(--nostr));
|
filter: brightness(125%) drop-shadow(0 0 0.33rem var(--nostr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#modal {
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
}
|
}
|
52
server/router/handler/invoice.go
Normal file
52
server/router/handler/invoice.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components"
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/types"
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleInvoice(sc context.Context) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
var (
|
||||||
|
db = sc.Db
|
||||||
|
ctx = c.Request().Context()
|
||||||
|
hash = c.Param("hash")
|
||||||
|
u = c.Get("session").(types.User)
|
||||||
|
inv = types.Invoice{}
|
||||||
|
expiresIn int
|
||||||
|
paid bool
|
||||||
|
qr templ.Component
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if err = db.QueryRowContext(ctx, ""+
|
||||||
|
"SELECT user_id, msats, COALESCE(msats_received, 0), expires_at, confirmed_at, bolt11 "+
|
||||||
|
"FROM invoices "+
|
||||||
|
"WHERE hash = $1", hash).
|
||||||
|
Scan(&inv.UserId, &inv.Msats, &inv.MsatsReceived, &inv.ExpiresAt, &inv.ConfirmedAt, &inv.Bolt11); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
c.Logger().Error(err)
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Id != inv.UserId {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
expiresIn = int(time.Until(inv.ExpiresAt).Seconds())
|
||||||
|
paid = inv.MsatsReceived >= inv.Msats
|
||||||
|
|
||||||
|
qr = components.Invoice(hash, inv.Bolt11, int(inv.Msats), expiresIn, paid)
|
||||||
|
|
||||||
|
return components.Modal(qr).Render(context.RenderContext(sc, c), c.Response().Writer)
|
||||||
|
}
|
||||||
|
}
|
74
server/router/handler/market.go
Normal file
74
server/router/handler/market.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components"
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/types"
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleCreate(sc context.Context) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
var (
|
||||||
|
db = sc.Db
|
||||||
|
lnd = sc.Lnd
|
||||||
|
tx *sql.Tx
|
||||||
|
ctx = c.Request().Context()
|
||||||
|
u = c.Get("session").(types.User)
|
||||||
|
question = c.FormValue("question")
|
||||||
|
description = c.FormValue("description")
|
||||||
|
endDate = c.FormValue("end_date")
|
||||||
|
hash lntypes.Hash
|
||||||
|
paymentRequest string
|
||||||
|
amount = lnwire.MilliSatoshi(1000)
|
||||||
|
expiry = int64(600)
|
||||||
|
expiresAt = time.Now().Add(time.Second * time.Duration(expiry))
|
||||||
|
invoiceId int
|
||||||
|
qr templ.Component
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
// TODO: validation
|
||||||
|
|
||||||
|
if tx, err = db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if hash, paymentRequest, err = lnd.Client.AddInvoice(ctx,
|
||||||
|
&invoicesrpc.AddInvoiceData{
|
||||||
|
Value: amount,
|
||||||
|
Expiry: expiry,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.QueryRowContext(ctx, ""+
|
||||||
|
"INSERT INTO invoices (user_id, msats, hash, bolt11, expires_at) "+
|
||||||
|
"VALUES ($1, $2, $3, $4, $5) "+
|
||||||
|
"RETURNING id",
|
||||||
|
u.Id, amount, hash.String(), paymentRequest, expiresAt).Scan(&invoiceId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = tx.ExecContext(ctx, ""+
|
||||||
|
"INSERT INTO markets (question, description, end_date, user_id, invoice_id) "+
|
||||||
|
"VALUES ($1, $2, $3, $4, $5)",
|
||||||
|
question, description, endDate, u.Id, invoiceId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
qr = components.Invoice(hash.String(), paymentRequest, int(amount), int(expiry), false)
|
||||||
|
|
||||||
|
return components.Modal(qr).Render(context.RenderContext(sc, c), c.Response().Writer)
|
||||||
|
}
|
||||||
|
}
|
62
server/router/pages/components/invoice.templ
Normal file
62
server/router/pages/components/invoice.templ
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ Invoice(hash string, bolt11 string, msats int, expiresIn int, paid bool) {
|
||||||
|
<div class="p-5 border border-muted bg-background text-center font-mono">
|
||||||
|
<div>Payment Required</div>
|
||||||
|
<div class="my-3">@Qr(bolt11, "lightning:"+bolt11)</div>
|
||||||
|
<div class="my-1">{ strconv.Itoa(msats/1000) } sats</div>
|
||||||
|
@InvoiceStatus(hash, expiresIn, paid)
|
||||||
|
<div class="none" id="bolt11" bolt11-data={ templ.JSONString(bolt11) } hx-preserve></div>
|
||||||
|
<script type="text/javascript" id="bolt11-js" hx-preserve>
|
||||||
|
var $ = selector => document.querySelector(selector)
|
||||||
|
var bolt11 = JSON.parse($("#bolt11").getAttribute("bolt11-data"))
|
||||||
|
console.log(bolt11)
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ InvoiceStatus(hash string, expiresIn int, paid bool) {
|
||||||
|
if paid {
|
||||||
|
<div class="font-mono label success my-1">PAID</div>
|
||||||
|
}
|
||||||
|
else if expiresIn <= 0 {
|
||||||
|
<div class="font-mono label error my-1">EXPIRED</div>
|
||||||
|
} else {
|
||||||
|
<!-- invoice is pending -->
|
||||||
|
<div class="font-mono my-1" id="countdown" countdown-data={ templ.JSONString(expiresIn) } hx-preserve></div>
|
||||||
|
<script type="text/javascript" id="countdown-js" hx-preserve>
|
||||||
|
var $ = selector => document.querySelector(selector)
|
||||||
|
var expiresIn = JSON.parse($("#countdown").getAttribute("countdown-data"))
|
||||||
|
|
||||||
|
function pad(num, places) {
|
||||||
|
return String(num).padStart(places, "0")
|
||||||
|
}
|
||||||
|
|
||||||
|
function _countdown() {
|
||||||
|
var minutes = Math.floor(expiresIn / 60)
|
||||||
|
var seconds = expiresIn % 60
|
||||||
|
var text = `${pad(minutes, 2)}:${pad(seconds, 2)}`
|
||||||
|
try {
|
||||||
|
$("#countdown").innerText = text
|
||||||
|
expiresIn--
|
||||||
|
} catch {
|
||||||
|
// countdown element disappeared
|
||||||
|
clearInterval(interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_countdown()
|
||||||
|
var interval = setInterval(_countdown, 1000)
|
||||||
|
</script>
|
||||||
|
<div
|
||||||
|
hx-get={ string(templ.SafeURL("/invoice/" + hash)) }
|
||||||
|
hx-trigger="load delay:1s"
|
||||||
|
hx-target="#modal"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-select="#modal" />
|
||||||
|
}
|
||||||
|
}
|
21
server/router/pages/components/modal.templ
Normal file
21
server/router/pages/components/modal.templ
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
templ Modal(component templ.Component) {
|
||||||
|
if component != nil {
|
||||||
|
<div
|
||||||
|
id="modal"
|
||||||
|
class="fixed left-0 top-0 w-screen h-screen"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
|
||||||
|
>
|
||||||
|
<!-- TODO: add background -->
|
||||||
|
<div class="flex justify-center p-3 w-screen">
|
||||||
|
@component
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div id="modal" class="hidden"></div>
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
package pages
|
package pages
|
||||||
|
|
||||||
import "git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components"
|
import (
|
||||||
|
c "git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
||||||
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components"
|
||||||
|
)
|
||||||
|
|
||||||
templ Index() {
|
templ Index() {
|
||||||
<html>
|
<html>
|
||||||
@ -9,9 +12,81 @@ templ Index() {
|
|||||||
@components.Nav()
|
@components.Nav()
|
||||||
<div id="content" class="flex flex-col text-center">
|
<div id="content" class="flex flex-col text-center">
|
||||||
@components.Figlet("random", "delphi")
|
@components.Figlet("random", "delphi")
|
||||||
<div class="font-mono my-3">A prediction market using the lightning network</div>
|
<div class="font-mono my-3"><small>A prediction market using the lightning network</small></div>
|
||||||
|
<div
|
||||||
|
id="grid-container"
|
||||||
|
class="border border-[var(--muted)] text-start"
|
||||||
|
hx-target="#grid-container"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-select="#grid-container"
|
||||||
|
hx-push-url="true"
|
||||||
|
>
|
||||||
|
<div class="border-b border-[var(--muted)]">
|
||||||
|
<button hx-get="/" class={ tabStyle(ctx.Value(c.ReqPathContextKey).(string), "/") }>markets</button>
|
||||||
|
<button hx-get="/create" class={ tabStyle(ctx.Value(c.ReqPathContextKey).(string), "/create") }>create</button>
|
||||||
</div>
|
</div>
|
||||||
|
if ctx.Value(c.ReqPathContextKey).(string) == "/" {
|
||||||
|
<div class="grid grid-cols-[auto_fit-content(10%)_fit-content(10%)] gap-1">
|
||||||
|
<!--
|
||||||
|
<span class="mt-1 ms-3">Will X happen?</span>
|
||||||
|
<span class="mt-1">51%</span>
|
||||||
|
<span class="mt-1 me-3 text-nowrap">1504 sats</span>
|
||||||
|
<span class="mb-1 ms-3">Will X happen?</span>
|
||||||
|
<span class="mb-1">51%</span>
|
||||||
|
<span class="mb-1 me-3 text-nowrap">1504 sats</span>
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<form
|
||||||
|
hx-post="/create"
|
||||||
|
hx-target="#modal"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-select="#modal"
|
||||||
|
class="flex flex-col mx-3"
|
||||||
|
>
|
||||||
|
<label class="my-1" for="question">question</label>
|
||||||
|
<input
|
||||||
|
id="question"
|
||||||
|
name="question"
|
||||||
|
type="text"
|
||||||
|
class="my-1 p-1 text-black"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div class="mt-3 mb-1">
|
||||||
|
<label for="description">description</label>
|
||||||
|
<span class="px-1 text-small text-muted">optional</span>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
type="text"
|
||||||
|
class="my-1 p-1 text-black"
|
||||||
|
></textarea>
|
||||||
|
<label class="mt-3" for="end_date">end date</label>
|
||||||
|
<input
|
||||||
|
id="end_date"
|
||||||
|
name="end_date"
|
||||||
|
type="date"
|
||||||
|
class="my-1 p-1 text-black"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button type="submit" class="mt-3">submit</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@components.Modal(nil)
|
||||||
@components.Footer()
|
@components.Footer()
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tabStyle(path string, tab string) string {
|
||||||
|
class := "!no-underline"
|
||||||
|
if path == tab {
|
||||||
|
class += " font-bold border-b-none"
|
||||||
|
}
|
||||||
|
return class
|
||||||
|
}
|
||||||
|
@ -14,6 +14,8 @@ func Init(e *echo.Echo, sc Context) {
|
|||||||
e.Use(middleware.Session(sc))
|
e.Use(middleware.Session(sc))
|
||||||
|
|
||||||
e.GET("/", handler.HandleIndex(sc))
|
e.GET("/", handler.HandleIndex(sc))
|
||||||
|
e.GET("/create", handler.HandleIndex(sc))
|
||||||
|
e.POST("/create", handler.HandleCreate(sc), middleware.SessionGuard(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"))
|
||||||
@ -25,4 +27,6 @@ func Init(e *echo.Echo, sc Context) {
|
|||||||
|
|
||||||
e.GET("/user", handler.HandleUser(sc), middleware.SessionGuard(sc))
|
e.GET("/user", handler.HandleUser(sc), middleware.SessionGuard(sc))
|
||||||
e.POST("/logout", handler.HandleLogout(sc), middleware.SessionGuard(sc))
|
e.POST("/logout", handler.HandleLogout(sc), middleware.SessionGuard(sc))
|
||||||
|
|
||||||
|
e.GET("/invoice/:hash", handler.HandleInvoice(sc), middleware.SessionGuard(sc))
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ pkgs.mkShell {
|
|||||||
gnumake
|
gnumake
|
||||||
inotify-tools
|
inotify-tools
|
||||||
figlet
|
figlet
|
||||||
|
postgresql
|
||||||
];
|
];
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
# install templ if not already installed
|
# install templ if not already installed
|
||||||
|
@ -6,7 +6,12 @@ module.exports = {
|
|||||||
center: true,
|
center: true,
|
||||||
padding: '1rem'
|
padding: '1rem'
|
||||||
},
|
},
|
||||||
extend: {},
|
extend: {
|
||||||
|
colors: {
|
||||||
|
'background': '191d21',
|
||||||
|
'muted': '#6c757d',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
function ({ addComponents }) {
|
function ({ addComponents }) {
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/guregu/null.v4"
|
||||||
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Id int
|
Id int
|
||||||
@ -9,3 +13,17 @@ type User struct {
|
|||||||
NostrPubkey string
|
NostrPubkey string
|
||||||
Msats int64
|
Msats int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Invoice struct {
|
||||||
|
Id int
|
||||||
|
UserId int
|
||||||
|
Msats int64
|
||||||
|
MsatsReceived int64
|
||||||
|
Hash string
|
||||||
|
Bolt11 string
|
||||||
|
CreatedAt time.Time
|
||||||
|
ExpiresAt time.Time
|
||||||
|
ConfirmedAt null.Time
|
||||||
|
HeldSince bool
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user