NIP05, comments, hide front page

This commit is contained in:
Dr. Tobias Baur
2023-03-10 04:18:26 +01:00
parent 5dd8b28405
commit feb2624907
9 changed files with 746 additions and 218 deletions
+5
View File
@@ -19,6 +19,8 @@ SITE_OWNER_URL=https://t.me/qecez
SITE_OWNER_NAME=@qecez
SITE_NAME=Bitmia
NOSTR_PRIVATE_KEY=nsec213....
FORWARD_URL=https://thepageyouwanttoshowasmainpage.com
NIP05=true
```
3. Start the app with `./satdress`
@@ -43,6 +45,9 @@ Maybe ask for help on https://t.me/lnurl if you're in trouble.
## Status of the Fork:
- NIP57 for Nostr ("Zaps") work when using an LNBits or LND backend, other backends (sparko, lnpay, eclair, commando) still need verification of payments in waitforinvoice.go by API calls in order to sign the zap on Nostr. (Help appreciated, because I can't test them)
- NIP05 support: If user added a npub, they can use lnaddress for Nostr NIP05 verificaton
- Comments from Damus testflight are forwarded to lnbits (needs some rework)
- Added possibility to add a forward main page, go to /lnaddress to add new users
- Added an alternative API '/api/easy' that deletes users and creates new name and pin for them
- Code needs some refactoring
- Needs proper testing (especially in multi-user environment)
+29 -1
View File
@@ -27,6 +27,7 @@ type Params struct {
Pin string `json:"pin"`
MinSendable string `json:"minSendable"`
MaxSendable string `json:"maxSendable"`
Npub string `json:"npub"`
}
func SaveName(
@@ -65,7 +66,7 @@ func SaveName(
params.Domain = domain
// check if the given data works
if inv, err = makeInvoice(params, 1000, &pin, ""); err != nil {
if inv, err = makeInvoice(params, 1000, &pin, "", ""); err != nil {
return "", "", fmt.Errorf("couldn't make an invoice with the given data: %w", err)
}
@@ -79,6 +80,7 @@ func SaveName(
}
func GetName(name, domain string) (*Params, error) {
val, closer, err := db.Get([]byte(getID(name, domain)))
if err != nil {
return nil, err
@@ -95,6 +97,32 @@ func GetName(name, domain string) (*Params, error) {
return &params, nil
}
func GetAllUsers(domain string) ([]Params, error) {
var k []byte
var paramslist []Params
iter := db.NewIter(nil)
for iter.SeekGE(k); iter.Valid(); iter.Next() {
val, closer, err := db.Get([]byte(iter.Key()))
if err != nil {
return nil, err
}
defer closer.Close()
var params Params
if err := json.Unmarshal(val, &params); err != nil {
return nil, err
}
params.Domain = domain
paramslist = append(paramslist, params)
}
iter.Close()
return paramslist, nil
}
func DeleteName(name, domain string) error {
key := []byte(getID(name, domain))
+15 -2
View File
@@ -50,12 +50,13 @@
<label for="kind"> Node Backend Type </label>
<select name="kind" id="kind" id="kind" v-model="kind">
<option disabled value="">Please select one:</option>
<option value="lnbits">LNbits</option>
<option value="lnd">LND</option>
<option value="commando">Commando (CLN)</option>
<option value="sparko">Sparko (CLN)</option>
<option value="eclair">Eclair</option>
<option value="lnpay">LNPay</option>
<option value="lnbits">LNbits</option>
<option value="sparko">Sparko (CLN)</option>
</select>
</div>
<div v-if="kind == 'lnd'">
@@ -162,9 +163,21 @@
<label for="pin"> Secret PIN </label>
<input class="input full-width" name="pin" id="pin" />
</div>
<div class="field">
<label for="npub">
Nostr npub (optional for NIP05)
</label>
<input
class="input full-width"
name="npub"
id="npub"
placeholder="npub123..."
/>
</div>
<button class="submit">Submit</button>
</form>
</div>
<div class="resources">
<a
class="resource-button"
+114
View File
@@ -0,0 +1,114 @@
package main
import (
"encoding/json"
"fmt"
"strconv"
"time"
"github.com/nbd-wtf/go-nostr"
"github.com/fiatjaf/go-lnurl"
// "github.com/fiatjaf/makeinvoice"
)
func metaData(params *Params) lnurl.Metadata {
//addImageToMetaData(w.telegram, &metadata, username, user.Telegram)
return lnurl.Metadata{
Description: fmt.Sprintf("Pay to %s@%s", params.Name, params.Domain),
LightningAddress: fmt.Sprintf("%s@%s", params.Name, params.Domain),
}
}
func makeInvoice(
params *Params,
msat int,
pin *string,
zapEventSerializedStr string,
comment string,
) (bolt11 string, err error) {
// prepare params
var backend BackendParams
switch params.Kind {
case "sparko":
backend = SparkoParams{
Host: params.Host,
Key: params.Key,
}
case "lnd":
backend = LNDParams{
Host: params.Host,
Macaroon: params.Key,
}
case "lnbits":
backend = LNBitsParams{
Host: params.Host,
Key: params.Key,
Memo: comment,
}
case "lnpay":
backend = LNPayParams{
PublicAccessKey: params.Pak,
WalletInvoiceKey: params.Waki,
}
case "eclair":
backend = EclairParams{
Host: params.Host,
Password: "",
}
case "commando":
backend = CommandoParams{
Host: params.Host,
NodeId: params.NodeId,
Rune: params.Rune,
}
}
mip := MIParams{
Msatoshi: int64(msat),
Backend: backend,
Label: params.Domain + "/" + strconv.FormatInt(time.Now().Unix(), 16),
}
if pin != nil {
// use this as the description for new accounts
mip.Description = fmt.Sprintf("%s's PIN for '%s@%s' lightning address: %s", params.Domain, params.Name, params.Domain, *pin)
} else {
//use zapEventSerializedStr if nip57, else build hash descriptionhash from params
if zapEventSerializedStr != "" {
//Ok here it gets a bit tricky (due to lack of standards, if we have a comment, we overwrite it)
if comment != "" {
mip.Memo = comment
var zapEvent nostr.Event
err = json.Unmarshal([]byte(zapEventSerializedStr), &zapEvent)
if zapEvent.Content == "" {
zapEvent.Content = comment
zapEventSerialized, _ := json.Marshal(zapEvent)
zapEventSerializedStr = fmt.Sprintf("%s", zapEventSerialized)
}
}
mip.Description = zapEventSerializedStr
} else {
// make the lnurlpay description_hash
mip.Description = metaData(params).Encode()
mip.Memo = comment
}
mip.UseDescriptionHash = true
}
// actually generate the invoice
bolt11, err = MakeInvoice(mip)
log.Debug().Int("msatoshi", msat).
Interface("backend", backend).
Str("bolt11", bolt11).Err(err).Str("Description", mip.Description).
Msg("invoice generation")
return bolt11, err
}
+45 -55
View File
@@ -37,15 +37,20 @@ type LNURLPayParamsCustom struct {
type LNURLPayValuesCustom struct {
lnurl.LNURLResponse
SuccessAction *lnurl.SuccessAction `json:"successAction"`
Routes interface{} `json:"routes"` // ignored
PR string `json:"pr"`
Disposable *bool `json:"disposable,omitempty"`
ParsedInvoice decodepay.Bolt11 `json:"-"`
PayerDataJSON string `json:"-"`
nip57Receipt nostr.Event `json:"nip57Receipt"`
nip57ReceiptRelays []string `json:"nip57ReceiptRelays"`
SuccessAction *lnurl.SuccessAction `json:"successAction"`
Routes interface{} `json:"routes"` // ignored
PR string `json:"pr"`
Disposable *bool `json:"disposable,omitempty"`
Comment string `json:"comment"`
CreatedAt time.Time `json:"created_at"`
Paid bool `json:"paid"`
PaidAt time.Time `json:"paid_at"`
From string `json:"from"`
ParsedInvoice decodepay.Bolt11 `json:"-"`
PayerDataJSON string `json:"-"`
nip57Receipt nostr.Event `json:"nip57Receipt"`
nip57ReceiptRelays []string `json:"nip57ReceiptRelays"`
awaitInvoicePaid bool `json:"awaitInvoicePaid"`
}
func handleLNURL(w http.ResponseWriter, r *http.Request) {
@@ -130,6 +135,7 @@ func handleLNURL(w http.ResponseWriter, r *http.Request) {
return
}
var comment = ""
// nostr NIP-57
// the "nostr" query param has a zap request which is a nostr event
// that specifies which nostr note has been zapped.
@@ -155,15 +161,22 @@ func handleLNURL(w http.ResponseWriter, r *http.Request) {
}
}
comment = zapEvent.Content
log.Debug().Str("NIP57 Comment received", comment).Msg("Comment")
}
comment := r.FormValue("comment")
if len(comment) > CommentAllowed {
//If a comment is send with the Invoice, always use it (?)
regularcomment := r.FormValue("comment")
if len(regularcomment) > CommentAllowed {
log.Error().Err(err).Str("[handleLnUrl] Comment is too long", err.Error())
return
}
// payer data
if len(regularcomment) > 0 {
comment = regularcomment
log.Debug().Str("Regular Comment received", comment).Msg("Comment")
}
// payer data, not used currently
payerdata := r.FormValue("payerdata")
var payerData lnurl.PayerDataValues
if len(payerdata) > 0 {
@@ -176,7 +189,6 @@ func handleLNURL(w http.ResponseWriter, r *http.Request) {
//we outsource the second part in a function, we should do this for the first one too.
response, err = serveLNURLpSecond(w, params, username, msat, comment, payerData, zapEvent)
var payvaluescustom = response.(LNURLPayValuesCustom)
//var payvalues = response.(*lnurl.LNURLPayValues)
if err != nil {
if response != nil {
// there is a valid error response
@@ -192,14 +204,15 @@ func handleLNURL(w http.ResponseWriter, r *http.Request) {
SuccessAction: payvaluescustom.SuccessAction,
})
//err = json.NewEncoder(w).Encode(payvalues)
if err != nil {
json.NewEncoder(w).Encode(response)
return
}
// if err != nil {
// json.NewEncoder(w).Encode(response)
// return
// }
if allowNostr {
go WaitForInvoicePaid(payvaluescustom, params)
//if we provided a nsec and the response contained zap information, we wait for the invoice to be paid
//in order to submit the zap on nostr
if allowNostr && payvaluescustom.awaitInvoicePaid {
go WaitForInvoicePaid(payvaluescustom, params, comment)
}
}
}
@@ -224,7 +237,6 @@ func serveLNURLpSecond(w http.ResponseWriter, params *Params, username string, a
zapEventSerialized, err := json.Marshal(zapEvent)
zapEventSerializedStr = fmt.Sprintf("%s", zapEventSerialized)
if err != nil {
//log.Println(err)
return LNURLPayValuesCustom{
LNURLResponse: lnurl.LNURLResponse{
Status: "Error",
@@ -232,29 +244,18 @@ func serveLNURLpSecond(w http.ResponseWriter, params *Params, username string, a
}, err
}
// we extract the relays from the zap request
nip57ReceiptRelaysTags := zapEvent.Tags.GetFirst([]string{"relays"})
if len(fmt.Sprintf("%s", nip57ReceiptRelaysTags)) > 0 {
nip57ReceiptRelays = strings.Split(fmt.Sprintf("%s", nip57ReceiptRelaysTags), " ")
// this tirty method returns slice [ "[relays", "wss...", "wss...", "wss...]" ] we need to clean it up
if len(nip57ReceiptRelays) > 1 {
// remove the first entry
nip57ReceiptRelays = nip57ReceiptRelays[1:]
// clean up the last entry
len_last_entry := len(nip57ReceiptRelays[len(nip57ReceiptRelays)-1])
nip57ReceiptRelays[len(nip57ReceiptRelays)-1] = nip57ReceiptRelays[len(nip57ReceiptRelays)-1][:len_last_entry-1]
}
}
// calculate description hash from the serialized nostr event in makeinvoice.go
zapEventSerializedStr = zapEventSerializedStr
} else {
nip57ReceiptRelays = ExtractNostrRelays(zapEvent)
} else {
//If we have a regular call, we ignore zapEvent in makeinvoice later.
zapEventSerializedStr = ""
log.Debug().Str("Regular Invoice", "Not an NIP57 event").Msg("Note")
}
var response LNURLPayValuesCustom
invoice, err := makeInvoice(params, amount_msat, nil, zapEventSerializedStr)
invoice, err := makeInvoice(params, amount_msat, nil, zapEventSerializedStr, comment)
if err != nil {
err = fmt.Errorf("Couldn't create invoice: %v", err.Error())
err = fmt.Errorf("couldn't create invoice: %v", err.Error())
response = LNURLPayValuesCustom{
LNURLResponse: lnurl.LNURLResponse{
Status: "Error",
@@ -262,24 +263,11 @@ func serveLNURLpSecond(w http.ResponseWriter, params *Params, username string, a
}
return response, err
}
var awaitPaid = false //Check invoice paid if we actually have a NIP57 event
// nip57 - we need to store the newly created invoice in the zap receipt
if zapEvent.Sig != "" {
pk := nostrPrivkeyHex
pub, _ := nostr.GetPublicKey(pk)
nip57Receipt = nostr.Event{
PubKey: pub,
CreatedAt: time.Now(),
Kind: 9735,
Tags: nostr.Tags{
*zapEvent.Tags.GetFirst([]string{"p"}),
[]string{"bolt11", invoice},
[]string{"description", zapEventSerializedStr},
},
}
if zapEvent.Tags.GetFirst([]string{"e"}) != nil {
nip57Receipt.Tags = nip57Receipt.Tags.AppendUnique(*zapEvent.Tags.GetFirst([]string{"e"}))
}
nip57Receipt.Sign(pk)
nip57Receipt = CreateNostrReceipt(zapEvent, invoice)
awaitPaid = true
}
return LNURLPayValuesCustom{
@@ -287,8 +275,10 @@ func serveLNURLpSecond(w http.ResponseWriter, params *Params, username string, a
PR: invoice,
Routes: make([]struct{}, 0),
SuccessAction: &lnurl.SuccessAction{Message: "Payment Received!", Tag: "message"},
Comment: comment,
nip57Receipt: nip57Receipt,
nip57ReceiptRelays: nip57ReceiptRelays,
awaitInvoicePaid: awaitPaid,
}, nil
}
+27 -9
View File
@@ -24,14 +24,16 @@ type Settings struct {
Domain string `envconfig:"DOMAIN" required:"true"`
// GlobalUsers means that user@ part is globally unique across all domains
// WARNING: if you toggle this existing users won't work anymore for safety reasons!
GlobalUsers bool `envconfig:"GLOBAL_USERS" required:"false" default:false`
Secret string `envconfig:"SECRET" required:"true"`
SiteOwnerName string `envconfig:"SITE_OWNER_NAME" required:"true"`
SiteOwnerURL string `envconfig:"SITE_OWNER_URL" required:"true"`
SiteName string `envconfig:"SITE_NAME" required:"true"`
NostrPrivateKey string `envconfig:"NOSTR_PRIVATE_KEY" required:"false" default:""`
ForceMigrate bool `envconfig:"FORCE_MIGRATE" required:"false" default:false`
TorProxyURL string `envconfig:"TOR_PROXY_URL"`
GlobalUsers bool `envconfig:"GLOBAL_USERS" required:"false" default:false`
Secret string `envconfig:"SECRET" required:"true"`
SiteOwnerName string `envconfig:"SITE_OWNER_NAME" required:"true"`
SiteOwnerURL string `envconfig:"SITE_OWNER_URL" required:"true"`
SiteName string `envconfig:"SITE_NAME" required:"true"`
NostrPrivateKey string `envconfig:"NOSTR_PRIVATE_KEY" required:"false" default:""`
ForwardMainPageUrl string `envconfig:"FORWARD_URL"`
Nip05 bool `envconfig:"NIP05" required:"false" default:false`
ForceMigrate bool `envconfig:"FORCE_MIGRATE" required:"false" default:false`
TorProxyURL string `envconfig:"TOR_PROXY_URL"`
}
var (
@@ -80,12 +82,26 @@ func main() {
router.Path("/.well-known/lnurlp/{user}").Methods("GET").
HandlerFunc(handleLNURL)
router.Path("/").HandlerFunc(
router.Path("/.well-known/nostr.json").Methods("GET").
HandlerFunc(handleNip05)
router.Path("/lnaddress").HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
renderHTML(w, indexHTML, map[string]interface{}{})
},
)
router.Path("/").HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
if s.ForwardMainPageUrl != "" {
http.Redirect(w, r, s.ForwardMainPageUrl, http.StatusSeeOther)
} else {
http.Redirect(w, r, "/lnaddress", http.StatusSeeOther)
}
},
)
router.PathPrefix("/static/").Handler(http.FileServer(http.FS(static)))
router.Path("/grab").HandlerFunc(
@@ -115,6 +131,7 @@ func main() {
Waki: r.FormValue("waki"),
NodeId: r.FormValue("nodeid"),
Rune: r.FormValue("rune"),
Npub: r.FormValue("npub"),
}, r.FormValue("pin"), false, "")
if err != nil {
w.WriteHeader(500)
@@ -161,6 +178,7 @@ func main() {
Waki: r.FormValue("waki"),
NodeId: r.FormValue("nodeid"),
Rune: r.FormValue("rune"),
Npub: r.FormValue("npub"),
}
pin, _, err := SaveName(newname, domain, &params, currentPin, true, currentName)
+422 -71
View File
@@ -1,93 +1,444 @@
package main
import (
"bytes"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/fiatjaf/go-lnurl"
"github.com/fiatjaf/makeinvoice"
"github.com/fiatjaf/eclair-go"
lightning "github.com/fiatjaf/lightningd-gjson-rpc"
lnsocket "github.com/jb55/lnsocket/go"
"github.com/lnpay/lnpay-go"
"github.com/nbd-wtf/go-nostr"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
// "github.com/fiatjaf/makeinvoice"
)
func metaData(params *Params) lnurl.Metadata {
//modified Version of fiatjaf/makeinvoice.go
//addImageToMetaData(w.telegram, &metadata, username, user.Telegram)
return lnurl.Metadata{
Description: fmt.Sprintf("Pay to %s@%s", params.Name, params.Domain),
LightningAddress: fmt.Sprintf("%s@%s", params.Name, params.Domain),
var (
TorProxyURL = "socks5://127.0.0.1:9050"
Client = &http.Client{
Timeout: 10 * time.Second,
}
)
type MIParams struct {
Backend BackendParams
Msatoshi int64
Description string
Memo string
// setting this to true will cause .Description to be hashed and used as
// the description_hash (h) field on the bolt11 invoice
UseDescriptionHash bool
Label string // only used for c-lightning
}
func makeInvoice(
params *Params,
msat int,
pin *string,
zapEventSerializedStr string,
) (bolt11 string, err error) {
// prepare params
var backend makeinvoice.BackendParams
switch params.Kind {
case "sparko":
backend = makeinvoice.SparkoParams{
Host: params.Host,
Key: params.Key,
}
case "lnd":
backend = makeinvoice.LNDParams{
Host: params.Host,
Macaroon: params.Key,
}
case "lnbits":
backend = makeinvoice.LNBitsParams{
Host: params.Host,
Key: params.Key,
}
case "lnpay":
backend = makeinvoice.LNPayParams{
PublicAccessKey: params.Pak,
WalletInvoiceKey: params.Waki,
}
case "eclair":
backend = makeinvoice.EclairParams{
Host: params.Host,
Password: "",
}
case "commando":
backend = makeinvoice.CommandoParams{
Host: params.Host,
NodeId: params.NodeId,
Rune: params.Rune,
}
}
type CommandoParams struct {
Rune string
Host string
NodeId string
}
mip := makeinvoice.Params{
Msatoshi: int64(msat),
Backend: backend,
func (l CommandoParams) getCert() string { return "" }
func (l CommandoParams) isTor() bool { return strings.Index(l.Host, ".onion") != -1 }
Label: params.Domain + "/" + strconv.FormatInt(time.Now().Unix(), 16),
}
type SparkoParams struct {
Cert string
Host string
Key string
}
if pin != nil {
// use this as the description for new accounts
mip.Description = fmt.Sprintf("%s's PIN for '%s@%s' lightning address: %s", params.Domain, params.Name, params.Domain, *pin)
func (l SparkoParams) getCert() string { return l.Cert }
func (l SparkoParams) isTor() bool { return strings.Index(l.Host, ".onion") != -1 }
type LNDParams struct {
Cert string
Host string
Macaroon string
}
func (l LNDParams) getCert() string { return l.Cert }
func (l LNDParams) isTor() bool { return strings.Index(l.Host, ".onion") != -1 }
type LNBitsParams struct {
Cert string
Host string
Key string
Memo string
}
func (l LNBitsParams) getCert() string { return l.Cert }
func (l LNBitsParams) isTor() bool { return strings.Index(l.Host, ".onion") != -1 }
type LNPayParams struct {
PublicAccessKey string
WalletInvoiceKey string
}
func (l LNPayParams) getCert() string { return "" }
func (l LNPayParams) isTor() bool { return false }
type EclairParams struct {
Host string
Password string
Cert string
}
func (l EclairParams) getCert() string { return l.Cert }
func (l EclairParams) isTor() bool { return strings.Index(l.Host, ".onion") != -1 }
type StrikeParams struct {
Key string
Username string
Currency string
}
func (l StrikeParams) getCert() string { return "" }
func (l StrikeParams) isTor() bool { return false }
type BackendParams interface {
getCert() string
isTor() bool
}
func MakeInvoice(params MIParams) (bolt11 string, err error) {
defer func(prevTransport http.RoundTripper) {
Client.Transport = prevTransport
}(Client.Transport)
specialTransport := &http.Transport{}
// use a cert or skip TLS verification?
if params.Backend.getCert() != "" {
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM([]byte(params.Backend.getCert()))
specialTransport.TLSClientConfig = &tls.Config{RootCAs: caCertPool}
} else {
//use zapEventSerializedStr if nip57, else build hash descriptionhash from params
if zapEventSerializedStr != "" {
mip.Description = zapEventSerializedStr
} else {
// make the lnurlpay description_hash
mip.Description = metaData(params).Encode()
}
mip.UseDescriptionHash = true
specialTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
// actually generate the invoice
bolt11, err = makeinvoice.MakeInvoice(mip)
// use a tor proxy?
if params.Backend.isTor() {
torURL, _ := url.Parse(TorProxyURL)
specialTransport.Proxy = http.ProxyURL(torURL)
}
log.Debug().Int("msatoshi", msat).
Interface("backend", backend).
Str("bolt11", bolt11).Err(err).Str("Description", mip.Description).
Msg("invoice generation")
Client.Transport = specialTransport
return bolt11, err
// description hash?
var hexh, b64h string
if params.UseDescriptionHash {
descriptionHash := sha256.Sum256([]byte(params.Description))
hexh = hex.EncodeToString(descriptionHash[:])
b64h = base64.StdEncoding.EncodeToString(descriptionHash[:])
}
switch backend := params.Backend.(type) {
case SparkoParams:
spark := &lightning.Client{
SparkURL: backend.Host,
SparkToken: backend.Key,
CallTimeout: time.Second * 3,
}
var method, desc string
if params.UseDescriptionHash {
method = "invoicewithdescriptionhash"
desc = hexh
} else {
method = "invoice"
desc = params.Description
}
label := params.Label
if label == "" {
label = makeRandomLabel()
}
inv, err := spark.Call(method, params.Msatoshi, label, desc)
if err != nil {
return "", fmt.Errorf(method+" call failed: %w", err)
}
return inv.Get("bolt11").String(), nil
case LNDParams:
body, _ := sjson.Set("{}", "value_msat", params.Msatoshi)
if params.UseDescriptionHash {
body, _ = sjson.Set(body, "description_hash", b64h)
} else {
body, _ = sjson.Set(body, "memo", params.Description)
}
req, err := http.NewRequest("POST",
backend.Host+"/v1/invoices",
bytes.NewBufferString(body),
)
if err != nil {
return "", err
}
// macaroon must be hex, so if it is on base64 we adjust that
if b, err := base64.StdEncoding.DecodeString(backend.Macaroon); err == nil {
backend.Macaroon = hex.EncodeToString(b)
}
req.Header.Set("Grpc-Metadata-macaroon", backend.Macaroon)
resp, err := Client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
body, _ := ioutil.ReadAll(resp.Body)
text := string(body)
if len(text) > 300 {
text = text[:300]
}
return "", fmt.Errorf("call to lnd failed (%d): %s", resp.StatusCode, text)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return gjson.ParseBytes(b).Get("payment_request").String(), nil
case LNBitsParams:
body, _ := sjson.Set("{}", "amount", params.Msatoshi/1000)
body, _ = sjson.Set(body, "out", false)
if params.UseDescriptionHash {
var zapEvent nostr.Event
err = json.Unmarshal([]byte(params.Description), &zapEvent)
if err == nil && zapEvent.Content != "" {
//This works
log.Debug().Str("Zap Event Content", zapEvent.Content).Msg("Message")
body, _ = sjson.Set(body, "memo", zapEvent.Content)
} else {
//But this does not?
// if params.Memo != "" {
// log.Debug().Str("Empty/Non Zap Event Content", params.Memo).Msg("Message")
// body, _ = sjson.Set(body, "memo", params.Memo)
// } else {
log.Debug().Str("Empty/Non Zap Event Hash", hex.EncodeToString([]byte(params.Description))).Msg("Message")
body, _ = sjson.Set(body, "unhashed_description", hex.EncodeToString([]byte(params.Description)))
//}
}
} else {
if params.Description == "" {
body, _ = sjson.Set(body, "memo", "created by makeinvoice")
} else {
log.Debug().Str("Description", params.Description).Msg("Message")
body, _ = sjson.Set(body, "memo", params.Description)
}
}
req, err := http.NewRequest("POST",
backend.Host+"/api/v1/payments",
bytes.NewBufferString(body),
)
if err != nil {
return "", err
}
req.Header.Set("X-Api-Key", backend.Key)
req.Header.Set("Content-Type", "application/json")
resp, err := Client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
body, _ := ioutil.ReadAll(resp.Body)
text := string(body)
if len(text) > 300 {
text = text[:300]
}
return "", fmt.Errorf("call to lnbits failed (%d): %s", resp.StatusCode, text)
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return gjson.ParseBytes(b).Get("payment_request").String(), nil
case LNPayParams:
client := lnpay.NewClient(backend.PublicAccessKey)
wallet := client.Wallet(backend.WalletInvoiceKey)
lntx, err := wallet.Invoice(lnpay.InvoiceParams{
NumSatoshis: params.Msatoshi / 1000,
Memo: params.Description,
DescriptionHash: hexh,
})
if err != nil {
return "", fmt.Errorf("error creating invoice on lnpay: %w", err)
}
return lntx.PaymentRequest, nil
case EclairParams:
client := eclair.Client{Host: backend.Host, Password: backend.Password}
eclairParams := eclair.Params{"amountMsat": params.Msatoshi}
if params.UseDescriptionHash {
eclairParams["descriptionHash"] = hexh
} else {
eclairParams["description"] = params.Description
}
inv, err := client.Call("createinvoice", eclairParams)
if err != nil {
return "", fmt.Errorf("error creating invoice on eclair: %w", err)
}
return inv.Get("serialized").String(), nil
case StrikeParams:
payload := struct {
Description string `json:"description"`
Amount struct {
Currency string `json:"currency"`
Amount string `json:"amount"`
} `json:"amount"`
}{}
payload.Description = "created by makeinvoice"
if params.Description != "" {
payload.Description = params.Description
}
// TODO: BTC currency does not seem to be supported at the moment Currently the currency needs to be the user's base currency (USD for the US, USDT for El Sal and Argentina). However, we're going to enable BTC invoices in the coming weeks.
payload.Amount.Currency = backend.Currency
payload.Amount.Amount = fmt.Sprintf("%.8f",
float32(params.Msatoshi)/100000000000)
jpayload := &bytes.Buffer{}
json.NewEncoder(jpayload).Encode(payload)
client := &http.Client{}
req, err := http.NewRequest("POST",
"https://api.strike.me/v1/invoices/handle/"+backend.Username, jpayload)
if err != nil {
return "", err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
req.Header.Add("Authorization", "Bearer "+backend.Key)
res, err := client.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
invoiceId := gjson.ParseBytes(body).Get("invoiceId").String()
// got strike invoice - get actual LN invoice now. sigh.
req, err = http.NewRequest("POST",
"https://api.strike.me/v1/invoices/"+invoiceId+"/quote", jpayload)
if err != nil {
return "", err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
req.Header.Add("Authorization", "Bearer "+backend.Key)
res, err = client.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
body, err = ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
lnInvoice := gjson.ParseBytes(body).Get("lnInvoice").String()
return lnInvoice, nil
case CommandoParams:
ln := lnsocket.LNSocket{}
ln.GenKey()
err := ln.ConnectAndInit(backend.Host, backend.NodeId)
if err != nil {
return "", err
}
defer ln.Disconnect()
label := params.Label
if label == "" {
label = makeRandomLabel()
}
invoiceParams := map[string]interface{}{
"amount_msat": params.Msatoshi,
"label": label,
"description": params.Description,
}
if params.UseDescriptionHash {
invoiceParams["deschashonly"] = true
}
jparams, _ := json.Marshal(invoiceParams)
body, err := ln.Rpc(backend.Rune, "invoice", string(jparams))
if err != nil {
return "", err
}
resErr := gjson.Get(body, "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(body, "result.bolt11")
if invoice.Type != gjson.String {
return "", fmt.Errorf("No bolt11 result found in invoice response, got %v", body)
}
return invoice.String(), nil
}
return "", errors.New("missing backend params")
}
func makeRandomLabel() string {
return "makeinvoice/" + strconv.FormatInt(time.Now().Unix(), 16)
}
+77 -2
View File
@@ -4,6 +4,9 @@ import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
@@ -34,7 +37,7 @@ func Nip57DescriptionHash(zapEventSerialized string) string {
}
func DecodeBench32(key string) string {
if _, v, err := nip19.Decode(s.NostrPrivateKey); err == nil {
if _, v, err := nip19.Decode(key); err == nil {
privatekeyhex := v.(string)
nostrPrivkeyHex = privatekeyhex
return nostrPrivkeyHex
@@ -43,10 +46,43 @@ func DecodeBench32(key string) string {
}
func handleNip05(w http.ResponseWriter, r *http.Request) {
var err error
var response string
var allusers []Params
allusers, err = GetAllUsers(s.Domain)
firstpartstring := "{\n\t\"names\":{\n"
finalpartstring := "\t\t}\n}"
var middlestring = ""
for _, user := range allusers {
if user.Npub != "" { //do some more validation checks
middlestring = middlestring + "\t\t\"" + user.Name + "\"" + ":" + "\"" + DecodeBench32(user.Npub) + "\"" + ",\n"
}
}
if s.Nip05 {
middlestringtrim := middlestring[:len(middlestring)-2]
middlestringtrim += "\n"
response = firstpartstring + middlestringtrim + finalpartstring
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
fmt.Fprintf(w, response)
} else {
return
}
if err != nil {
return
}
}
func publishNostrEvent(ev nostr.Event, relays []string) {
pk := s.NostrPrivateKey
ev.Sign(pk)
log.Debug().Str("[NOSTR] 🟣 publishing nostr event %s", ev.ID)
log.Debug().Str("publishing nostr event %s", ev.ID)
// more relays
relays = append(relays, "wss://relay.nostr.ch", "wss://eden.nostr.land", "wss://nostr.btcmp.com", "wss://nostr.relayer.se", "wss://relay.current.fyi", "wss://nos.lol", "wss://nostr.mom", "wss://relay.nostr.info", "wss://nostr.zebedee.cloud", "wss://nostr-pub.wellorder.net", "wss://relay.snort.social/", "wss://relay.damus.io/", "wss://nostr.oxtr.dev/", "wss://nostr.fmt.wiz.biz/", "wss://brb.io")
// remove trailing /
@@ -74,6 +110,45 @@ func publishNostrEvent(ev nostr.Event, relays []string) {
}
}
func ExtractNostrRelays(zapEvent nostr.Event) []string {
nip57ReceiptRelaysTags := zapEvent.Tags.GetFirst([]string{"relays"})
if len(fmt.Sprintf("%s", nip57ReceiptRelaysTags)) > 0 {
nip57ReceiptRelays = strings.Split(fmt.Sprintf("%s", nip57ReceiptRelaysTags), " ")
// this tirty method returns slice [ "[relays", "wss...", "wss...", "wss...]" ] we need to clean it up
if len(nip57ReceiptRelays) > 1 {
// remove the first entry
nip57ReceiptRelays = nip57ReceiptRelays[1:]
// clean up the last entry
len_last_entry := len(nip57ReceiptRelays[len(nip57ReceiptRelays)-1])
nip57ReceiptRelays[len(nip57ReceiptRelays)-1] = nip57ReceiptRelays[len(nip57ReceiptRelays)-1][:len_last_entry-1]
}
}
return nip57ReceiptRelays
}
func CreateNostrReceipt(zapEvent nostr.Event, invoice string) nostr.Event {
pk := nostrPrivkeyHex
pub, _ := nostr.GetPublicKey(pk)
zapEventSerialized, _ := json.Marshal(zapEvent)
zapEventSerializedStr = fmt.Sprintf("%s", zapEventSerialized)
nip57Receipt = nostr.Event{
PubKey: pub,
CreatedAt: time.Now(),
Kind: 9735,
Tags: nostr.Tags{
*zapEvent.Tags.GetFirst([]string{"p"}),
[]string{"bolt11", invoice},
[]string{"description", zapEventSerializedStr},
},
}
if zapEvent.Tags.GetFirst([]string{"e"}) != nil {
nip57Receipt.Tags = nip57Receipt.Tags.AppendUnique(*zapEvent.Tags.GetFirst([]string{"e"}))
}
nip57Receipt.Sign(pk)
return nip57Receipt
}
func uniqueSlice(slice []string) []string {
keys := make(map[string]bool)
list := []string{}
+12 -78
View File
@@ -11,20 +11,12 @@ import (
"net/http"
"net/url"
"strconv"
"strings"
"time"
decodepay "github.com/nbd-wtf/ln-decodepay"
"github.com/tidwall/gjson"
)
var (
TorProxyURL = "socks5://127.0.0.1:9050"
Client = &http.Client{
Timeout: 25 * time.Second,
}
)
type LNParams struct {
Backend BackendParams
Msatoshi int64
@@ -37,74 +29,7 @@ type LNParams struct {
Label string // only used for c-lightning
}
type CommandoParams struct {
Rune string
Host string
NodeId string
}
func (l CommandoParams) getCert() string { return "" }
func (l CommandoParams) isTor() bool { return strings.Contains(l.Host, ".onion") }
type SparkoParams struct {
Cert string
Host string
Key string
}
func (l SparkoParams) getCert() string { return l.Cert }
func (l SparkoParams) isTor() bool { return strings.Contains(l.Host, ".onion") }
type LNDParams struct {
Cert string
Host string
Macaroon string
}
func (l LNDParams) getCert() string { return l.Cert }
func (l LNDParams) isTor() bool { return strings.Contains(l.Host, ".onion") }
type LNBitsParams struct {
Cert string
Host string
Key string
}
func (l LNBitsParams) getCert() string { return l.Cert }
func (l LNBitsParams) isTor() bool { return strings.Contains(l.Host, ".onion") }
type LNPayParams struct {
PublicAccessKey string
WalletInvoiceKey string
}
func (l LNPayParams) getCert() string { return "" }
func (l LNPayParams) isTor() bool { return false }
type EclairParams struct {
Host string
Password string
Cert string
}
func (l EclairParams) getCert() string { return l.Cert }
func (l EclairParams) isTor() bool { return strings.Contains(l.Host, ".onion") }
type StrikeParams struct {
Key string
Username string
Currency string
}
func (l StrikeParams) getCert() string { return "" }
func (l StrikeParams) isTor() bool { return false }
type BackendParams interface {
getCert() string
isTor() bool
}
func WaitForInvoicePaid(payvalues LNURLPayValuesCustom, params *Params) {
func WaitForInvoicePaid(payvalues LNURLPayValuesCustom, params *Params, comment string) {
//Check for a minute if invoice is paid
//Do we have an easier way to do this? How does it work for other backends than lnbits.
go func() {
@@ -245,8 +170,17 @@ func WaitForInvoicePaid(payvalues LNURLPayValuesCustom, params *Params) {
//If invoice is paid and DescriptionHash matches Nip57 DescriptionHash, publish Zap Nostr Event. This is rather a sanity check.
if isPaid {
if payvalues.nip57Receipt.Kind == 0 {
log.Debug().Str("Paid", "Paid non-Nostr Invoice").Msg("Paid")
close(quit)
return
}
log.Debug().Str("DescriptionHash", bolt11.DescriptionHash).Msg("zapped")
log.Debug().Str("Description", bolt11.Description).Msg("zapped")
var descriptionTag = *payvalues.nip57Receipt.Tags.GetFirst([]string{"description"})
if bolt11.DescriptionHash == Nip57DescriptionHash(descriptionTag.Value()) {
if (bolt11.DescriptionHash == Nip57DescriptionHash(descriptionTag.Value())) || (bolt11.Description == comment && descriptionTag.Value() != "") {
log.Debug().Str("ZAP", "Published on Nostr").Msg("zapped")
publishNostrEvent(payvalues.nip57Receipt, payvalues.nip57ReceiptRelays)
close(quit)
@@ -256,7 +190,7 @@ func WaitForInvoicePaid(payvalues LNURLPayValuesCustom, params *Params) {
//Timeout waiting for payment after maxiterations
if maxiterations == 0 {
log.Debug().Str("NIP57", bolt11.PaymentHash).Msg("Timed out")
log.Debug().Str("NIP57 Bolt11", bolt11.PaymentHash).Msg("Timed out")
close(quit)
return
}