move example relay implementations to examples/ folder.

This commit is contained in:
fiatjaf
2023-05-18 09:48:56 -03:00
parent b2bf358789
commit 47b8ee106f
37 changed files with 48 additions and 0 deletions

1
examples/expensive/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
relayer-expensive

View 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

View 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.

View 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})
}
}

View 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"
}

View 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)
}
}