mirror of
https://github.com/fiatjaf/khatru.git
synced 2026-06-06 02:31:15 +02:00
move example relay implementations to examples/ folder.
This commit is contained in:
1
examples/expensive/.gitignore
vendored
Normal file
1
examples/expensive/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
relayer-expensive
|
||||
2
examples/expensive/Makefile
Normal file
2
examples/expensive/Makefile
Normal file
@@ -0,0 +1,2 @@
|
||||
relayer-expensive: $(shell find .. -name "*.go")
|
||||
CC=$$(which musl-gcc) go build -ldflags='-s -w -linkmode external -extldflags "-static"' -o ./relayer-expensive
|
||||
31
examples/expensive/README.md
Normal file
31
examples/expensive/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
expensive-relay, a sybil-free corner of nostr
|
||||
=============================================
|
||||
|
||||
- a nostr relay implementation based on relayer.
|
||||
- uses postgres, which I think must be over version 12 since it uses generated columns.
|
||||
- requires users to manually register themselves to be able to publish events and pay a fee. this should prevent spam.
|
||||
- aside from that it's basically the same thing as relayer basic.
|
||||
|
||||
running
|
||||
-------
|
||||
|
||||
this requires a recent CLN version with Commando.
|
||||
|
||||
grab a binary from the releases page and run it with the following environment variables:
|
||||
|
||||
POSTGRESQL_DATABASE=postgresql://...
|
||||
CLN_NODE_ID=02fed8723...
|
||||
CLN_HOST=127.0.0.1:9735
|
||||
CLN_RUNE=...
|
||||
TICKET_PRICE_SATS=500
|
||||
|
||||
adjust the values above accordingly.
|
||||
|
||||
compiling
|
||||
---------
|
||||
|
||||
if you know Go you already know this:
|
||||
|
||||
go install github.com/fiatjaf/relayer/expensive
|
||||
|
||||
or something like that.
|
||||
68
examples/expensive/handler.go
Normal file
68
examples/expensive/handler.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func handleWebpage(w http.ResponseWriter, rq *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`
|
||||
<meta charset=utf-8>
|
||||
<title>expensive relay</title>
|
||||
<h1>expensive relay</h1>
|
||||
<a href="https://github.com/fiatjaf/expensive-relay">https://github.com/fiatjaf/expensive-relay</a>
|
||||
<p>this is a nostr relay that only accepts events published from keys that pay a registration fee. this is an antispam measure. you can still be banned if you're spamming or doing something bad.</p>
|
||||
<p>to register your nostr public key, type it below and click the button.</p>
|
||||
<form>
|
||||
<label>
|
||||
nostr public key:
|
||||
<input name=pubkey />
|
||||
</label>
|
||||
<button>Get Invoice</button>
|
||||
</form>
|
||||
<p id=message></p>
|
||||
<a id=link><canvas id=qr /></a>
|
||||
<code id=invoice></code>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrious/4.0.2/qrious.min.js"></script>
|
||||
<script>
|
||||
document.querySelector('form').addEventListener('submit', async ev => {
|
||||
ev.preventDefault()
|
||||
let res = await (await fetch('/invoice?pubkey=' + ev.target.pubkey.value)).text()
|
||||
let { bolt11, error } = JSON.parse(res)
|
||||
if (bolt11) {
|
||||
invoice.innerHTML = bolt11
|
||||
link.href = 'lightning:' + bolt11
|
||||
new QRious({
|
||||
element: qr,
|
||||
value: bolt11.toUpperCase(),
|
||||
size: 300
|
||||
});
|
||||
} else {
|
||||
message.innerHTML = error
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
margin: 10px auto;
|
||||
width: 800px;
|
||||
max-width: 90%;
|
||||
}
|
||||
</style>
|
||||
`))
|
||||
}
|
||||
|
||||
func handleInvoice(w http.ResponseWriter, rq *http.Request, r *Relay) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
invoice, err := generateInvoice(r, rq.URL.Query().Get("pubkey"))
|
||||
if err != nil {
|
||||
json.NewEncoder(w).Encode(struct {
|
||||
Error string `json:"error"`
|
||||
}{err.Error()})
|
||||
} else {
|
||||
json.NewEncoder(w).Encode(struct {
|
||||
Invoice string `json:"bolt11"`
|
||||
}{invoice})
|
||||
}
|
||||
}
|
||||
88
examples/expensive/lightning.go
Normal file
88
examples/expensive/lightning.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
lnsocket "github.com/jb55/lnsocket/go"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func generateLabel(pubkey string) string { return fmt.Sprintf("relayer-expensive:ticket:%s", pubkey) }
|
||||
|
||||
func generateInvoice(r *Relay, pubkey string) (string, error) {
|
||||
label := generateLabel(pubkey)
|
||||
cln := lnsocket.LNSocket{}
|
||||
cln.GenKey()
|
||||
|
||||
err := cln.ConnectAndInit(r.CLNHost, r.CLNNodeId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer cln.Disconnect()
|
||||
|
||||
// check if there is an invoice already
|
||||
jparams, _ := json.Marshal(map[string]any{
|
||||
"label": label,
|
||||
})
|
||||
result, _ := cln.Rpc(r.CLNRune, "listinvoices", string(jparams))
|
||||
if gjson.Get(result, "result.invoices.#").Int() == 1 {
|
||||
timestamp := time.Now().Unix()
|
||||
if (gjson.Get(result, "result.invoices.0.expires_at").Int() > timestamp) {
|
||||
return gjson.Get(result, "result.invoices.0.bolt11").String(), nil
|
||||
}
|
||||
jparams, _ := json.Marshal(map[string]any{
|
||||
"label": label,
|
||||
"status": "expired",
|
||||
})
|
||||
cln.Rpc(r.CLNRune, "delinvoice", string(jparams))
|
||||
}
|
||||
|
||||
// otherwise generate an invoice
|
||||
jparams, _ = json.Marshal(map[string]any{
|
||||
"amount_msat": r.TicketPriceSats * 1000,
|
||||
"label": label,
|
||||
"description": fmt.Sprintf("%s's ticket for writing to relayer-expensive", pubkey),
|
||||
})
|
||||
result, err = cln.Rpc(r.CLNRune, "invoice", string(jparams))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resErr := gjson.Get(result, "error")
|
||||
if resErr.Type != gjson.Null {
|
||||
if resErr.Type == gjson.JSON {
|
||||
return "", errors.New(resErr.Get("message").String())
|
||||
} else if resErr.Type == gjson.String {
|
||||
return "", errors.New(resErr.String())
|
||||
}
|
||||
return "", fmt.Errorf("Unknown commando error: '%v'", resErr)
|
||||
}
|
||||
|
||||
invoice := gjson.Get(result, "result.bolt11")
|
||||
if invoice.Type != gjson.String {
|
||||
return "", fmt.Errorf("No bolt11 result found in invoice response, got %v", result)
|
||||
}
|
||||
|
||||
return invoice.String(), nil
|
||||
}
|
||||
|
||||
func checkInvoicePaidOk(pubkey string) bool {
|
||||
cln := lnsocket.LNSocket{}
|
||||
cln.GenKey()
|
||||
|
||||
err := cln.ConnectAndInit(r.CLNHost, r.CLNNodeId)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer cln.Disconnect()
|
||||
|
||||
jparams, _ := json.Marshal(map[string]any{
|
||||
"label": generateLabel(pubkey),
|
||||
})
|
||||
result, _ := cln.Rpc(r.CLNRune, "listinvoices", string(jparams))
|
||||
|
||||
return gjson.Get(result, "result.invoices.0.status").String() == "paid"
|
||||
}
|
||||
85
examples/expensive/main.go
Normal file
85
examples/expensive/main.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/fiatjaf/relayer/v2"
|
||||
"github.com/fiatjaf/relayer/v2/storage/postgresql"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
type Relay struct {
|
||||
PostgresDatabase string `envconfig:"POSTGRESQL_DATABASE"`
|
||||
CLNNodeId string `envconfig:"CLN_NODE_ID"`
|
||||
CLNHost string `envconfig:"CLN_HOST"`
|
||||
CLNRune string `envconfig:"CLN_RUNE"`
|
||||
TicketPriceSats int64 `envconfig:"TICKET_PRICE_SATS"`
|
||||
|
||||
storage *postgresql.PostgresBackend
|
||||
}
|
||||
|
||||
var r = &Relay{}
|
||||
|
||||
func (r *Relay) Name() string {
|
||||
return "ExpensiveRelay"
|
||||
}
|
||||
|
||||
func (r *Relay) Storage(ctx context.Context) relayer.Storage {
|
||||
return r.storage
|
||||
}
|
||||
|
||||
func (r *Relay) Init() error {
|
||||
// every hour, delete all very old events
|
||||
go func() {
|
||||
db := r.Storage(context.TODO()).(*postgresql.PostgresBackend)
|
||||
|
||||
for {
|
||||
time.Sleep(60 * time.Minute)
|
||||
db.DB.Exec(`DELETE FROM event WHERE created_at < $1`, time.Now().AddDate(0, -3, 0).Unix()) // 6 months
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Relay) AcceptEvent(ctx context.Context, evt *nostr.Event) bool {
|
||||
// only accept they have a good preimage for a paid invoice for their public key
|
||||
if !checkInvoicePaidOk(evt.PubKey) {
|
||||
return false
|
||||
}
|
||||
|
||||
// block events that are too large
|
||||
jsonb, _ := json.Marshal(evt)
|
||||
if len(jsonb) > 100000 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := Relay{}
|
||||
if err := envconfig.Process("", &r); err != nil {
|
||||
log.Fatalf("failed to read from env: %v", err)
|
||||
return
|
||||
}
|
||||
r.storage = &postgresql.PostgresBackend{DatabaseURL: r.PostgresDatabase}
|
||||
server, err := relayer.NewServer(&r)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create server: %v", err)
|
||||
}
|
||||
// special handlers
|
||||
server.Router().HandleFunc("/", handleWebpage)
|
||||
server.Router().HandleFunc("/invoice", func(w http.ResponseWriter, rq *http.Request) {
|
||||
handleInvoice(w, rq, &r)
|
||||
})
|
||||
if err := server.Start("0.0.0.0", 7447); err != nil {
|
||||
log.Fatalf("server terminated: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user