Implement withdrawal
This commit is contained in:
parent
2af9ac29df
commit
c7ae559777
@ -54,3 +54,11 @@ CREATE TABLE orders(
|
||||
);
|
||||
ALTER TABLE orders ADD CONSTRAINT order_price CHECK(price > 0 AND price < 100);
|
||||
ALTER TABLE orders ADD CONSTRAINT order_quantity CHECK(quantity > 0);
|
||||
CREATE TABLE withdrawals(
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
pubkey TEXT NOT NULL REFERENCES users(pubkey),
|
||||
bolt11 TEXT NOT NULL UNIQUE,
|
||||
paid_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
@ -66,4 +66,12 @@ type (
|
||||
Invoice
|
||||
OrderId null.String
|
||||
}
|
||||
Withdrawal struct {
|
||||
Id UUID
|
||||
CreatedAt time.Time
|
||||
DeletedAt null.Time
|
||||
Pubkey string
|
||||
Bolt11 string `json:"bolt11"`
|
||||
PaidAt null.Time
|
||||
}
|
||||
)
|
||||
|
13
db/withdrawal.go
Normal file
13
db/withdrawal.go
Normal file
@ -0,0 +1,13 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func (db *DB) CreateWithdrawal(tx *sql.Tx, c context.Context, w *Withdrawal) error {
|
||||
if _, err := tx.ExecContext(c, "INSERT INTO withdrawals(pubkey, bolt11) VALUES ($1, $2)", w.Pubkey, w.Bolt11); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
30
lnd/withdrawal.go
Normal file
30
lnd/withdrawal.go
Normal file
@ -0,0 +1,30 @@
|
||||
package lnd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
)
|
||||
|
||||
func (lnd *LNDClient) PayInvoice(tx *sql.Tx, bolt11 string) error {
|
||||
maxFeeSats := btcutil.Amount(10)
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second)
|
||||
defer cancel()
|
||||
log.Printf("attempting to pay bolt11 %s ...\n", bolt11)
|
||||
payChan := lnd.Client.PayInvoice(ctx, bolt11, maxFeeSats, nil)
|
||||
res := <-payChan
|
||||
if res.Err != nil {
|
||||
log.Printf("error paying bolt11: %s -- %s\n", bolt11, res.Err)
|
||||
tx.Rollback()
|
||||
return res.Err
|
||||
}
|
||||
log.Printf("successfully paid bolt11: %s\n", bolt11)
|
||||
if _, err := tx.ExecContext(ctx, "UPDATE withdrawals SET paid_at = CURRENT_TIMESTAMP WHERE bolt11 = $1", bolt11); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
return res.Err
|
||||
}
|
@ -13,7 +13,7 @@ func httpErrorHandler(err error, c echo.Context) {
|
||||
if httpError, ok := err.(*echo.HTTPError); ok {
|
||||
code = httpError.Code
|
||||
}
|
||||
if strings.Contains(err.Error(), "violates check constraint") {
|
||||
if strings.Contains(err.Error(), "violates check constraint") || strings.Contains(err.Error(), "violates unique constraint") {
|
||||
code = 400
|
||||
}
|
||||
c.JSON(code, map[string]any{"status": code})
|
||||
|
79
server/router/handler/withdrawal.go
Normal file
79
server/router/handler/withdrawal.go
Normal file
@ -0,0 +1,79 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
context_ "context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.ekzyis.com/ekzyis/delphi.market/db"
|
||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
)
|
||||
|
||||
func HandleWithdrawal(sc context.ServerContext) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var (
|
||||
w db.Withdrawal
|
||||
u db.User
|
||||
inv *zpay32.Invoice
|
||||
tx *sql.Tx
|
||||
err error
|
||||
)
|
||||
if err := c.Bind(&w); err != nil {
|
||||
code := http.StatusBadRequest
|
||||
return c.JSON(code, map[string]any{"status": code, "reason": "bolt11 required"})
|
||||
}
|
||||
|
||||
if inv, err = zpay32.Decode(w.Bolt11, sc.Lnd.ChainParams); err != nil {
|
||||
code := http.StatusBadRequest
|
||||
return c.JSON(code, map[string]any{"status": code, "reason": "zpay32 decode error"})
|
||||
}
|
||||
|
||||
// transaction start
|
||||
ctx, cancel := context_.WithTimeout(c.Request().Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
if tx, err = sc.Db.BeginTx(ctx, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Commit()
|
||||
|
||||
u = c.Get("session").(db.User)
|
||||
w.Pubkey = u.Pubkey
|
||||
|
||||
// TODO deduct network fee from user balance
|
||||
if u.Msats < int64(*inv.MilliSat) {
|
||||
tx.Rollback()
|
||||
code := http.StatusBadRequest
|
||||
return c.JSON(code, map[string]any{"status": code, "reason": "insufficient balance"})
|
||||
}
|
||||
|
||||
// create withdrawal
|
||||
if err = sc.Db.CreateWithdrawal(tx, ctx, &w); err != nil {
|
||||
tx.Rollback()
|
||||
if strings.Contains(err.Error(), "violates unique constraint") {
|
||||
code := http.StatusBadRequest
|
||||
return c.JSON(code, map[string]any{"status": code, "reason": "bolt11 already submitted"})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// pay invoice via LND
|
||||
if err = sc.Lnd.PayInvoice(tx, w.Bolt11); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
// deduct balance from user
|
||||
if _, err = tx.ExecContext(ctx, "UPDATE users SET msats = msats - $1 WHERE pubkey = $2", int64(*inv.MilliSat), u.Pubkey); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
return c.JSON(http.StatusOK, nil)
|
||||
}
|
||||
}
|
@ -68,6 +68,10 @@ func addBackendRoutes(e *echo.Echo, sc ServerContext) {
|
||||
GET(e, sc, "/api/invoices",
|
||||
handler.HandleInvoices,
|
||||
middleware.SessionGuard)
|
||||
POST(e, sc, "/api/withdrawal",
|
||||
handler.HandleWithdrawal,
|
||||
middleware.SessionGuard,
|
||||
middleware.LNDGuard)
|
||||
}
|
||||
|
||||
func GET(e *echo.Echo, sc ServerContext, path string, scF HandlerFunc, scM ...MiddlewareFunc) *echo.Route {
|
||||
|
@ -1,18 +0,0 @@
|
||||
<template>
|
||||
<div v-if="session.pubkey" class="flex flex-row items-center">
|
||||
<div>authenticated as {{ session.pubkey.slice(0, 8) }}</div>
|
||||
<button class="ms-2 my-3" @click="logout">logout</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useSession } from '@/stores/session'
|
||||
import { useRouter } from 'vue-router'
|
||||
const session = useSession()
|
||||
const router = useRouter()
|
||||
|
||||
const logout = async () => {
|
||||
await session.logout()
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
80
vue/src/components/UserWallet.vue
Normal file
80
vue/src/components/UserWallet.vue
Normal file
@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="session.pubkey" class="grid flex-row items-center">
|
||||
<div>authenticated as {{ session.pubkey.slice(0, 8) }}</div>
|
||||
<button class="ms-2 my-3" @click="logout">logout</button>
|
||||
<div>you have {{ session.msats / 1000 }} sats</div>
|
||||
<button class="ms-2 my-3" @click="toggleWithdrawalForm">
|
||||
<span v-if="showWithdrawalForm">cancel</span>
|
||||
<span v-else>withdraw</span></button>
|
||||
</div>
|
||||
<form v-show="showWithdrawalForm" @submit.prevent="submitWithdrawal">
|
||||
<label for="bolt11">bolt11</label>
|
||||
<input name="bolt11" id="bolt11" type="text" required v-model="bolt11" />
|
||||
<button type="submit" class="col-span-2">submit withdrawal</button>
|
||||
</form>
|
||||
<div v-if="err" class="red text-center">{{ err }}</div>
|
||||
<div v-if="success" class="green text-center">{{ success }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useSession } from '@/stores/session'
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
const session = useSession()
|
||||
const router = useRouter()
|
||||
|
||||
const logout = async () => {
|
||||
await session.logout()
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
const showWithdrawalForm = ref(false)
|
||||
const bolt11 = ref(null)
|
||||
const toggleWithdrawalForm = () => {
|
||||
showWithdrawalForm.value = !showWithdrawalForm.value
|
||||
}
|
||||
const err = ref(null)
|
||||
const success = ref(null)
|
||||
const submitWithdrawal = async () => {
|
||||
success.value = null
|
||||
err.value = null
|
||||
const url = '/api/withdrawal'
|
||||
const body = JSON.stringify({ bolt11: bolt11.value })
|
||||
try {
|
||||
const res = await fetch(url, { method: 'POST', headers: { 'Content-type': 'application/json' }, body })
|
||||
if (res.status === 200) {
|
||||
success.value = 'invoice paid'
|
||||
return session.checkSession()
|
||||
}
|
||||
const resBody = await res.json()
|
||||
err.value = resBody.reason || `error: server responded with HTTP ${res.status}`
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.grid {
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
|
||||
form>* {
|
||||
margin: 0.5em 1em;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
@ -11,7 +11,7 @@ import LoginView from '@/views/LoginView'
|
||||
import UserView from '@/views/UserView'
|
||||
import MarketView from '@/views/MarketView'
|
||||
import InvoiceView from '@/views/InvoiceView'
|
||||
import UserSettings from '@/components/UserSettings'
|
||||
import UserWallet from '@/components/UserWallet'
|
||||
import UserInvoices from '@/components/UserInvoices'
|
||||
import UserOrders from '@/components/UserOrders'
|
||||
import OrderForm from '@/components/OrderForm'
|
||||
@ -32,7 +32,7 @@ const routes = [
|
||||
path: '/user',
|
||||
component: UserView,
|
||||
children: [
|
||||
{ path: 'settings', name: 'user', component: UserSettings },
|
||||
{ path: 'wallet', name: 'user', component: UserWallet },
|
||||
{ path: 'invoices', name: 'invoices', component: UserInvoices },
|
||||
{ path: 'orders', name: 'orders', component: UserOrders }
|
||||
]
|
||||
|
@ -11,7 +11,7 @@
|
||||
<!-- eslint-enable -->
|
||||
<header class="flex flex-row text-center justify-center pt-1">
|
||||
<nav>
|
||||
<StyledLink to="/user/settings">settings</StyledLink>
|
||||
<StyledLink to="/user/wallet">wallet</StyledLink>
|
||||
<StyledLink to="/user/invoices">invoices</StyledLink>
|
||||
<StyledLink to="/user/orders">orders</StyledLink>
|
||||
</nav>
|
||||
|
Loading…
x
Reference in New Issue
Block a user