refactor nip-44 and server side of bunker
This commit is contained in:
69
bunker/main.go
Normal file
69
bunker/main.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package bunker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"relay.mleku.dev/chk"
|
||||
"relay.mleku.dev/context"
|
||||
"relay.mleku.dev/event"
|
||||
"relay.mleku.dev/keys"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
ID string `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params [][]byte `json:"params"`
|
||||
}
|
||||
|
||||
func (r *Request) String() (s string) {
|
||||
var j []byte
|
||||
var err error
|
||||
if j, err = json.Marshal(r); chk.E(err) {
|
||||
return
|
||||
}
|
||||
return string(j)
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
ID string `json:"id"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Result string `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
func (r *Response) String() (s string) {
|
||||
var j []byte
|
||||
var err error
|
||||
if j, err = json.Marshal(r); chk.E(err) {
|
||||
return
|
||||
}
|
||||
return string(j)
|
||||
}
|
||||
|
||||
type Signer interface {
|
||||
GetSession(clientPubkey string) (Session, bool)
|
||||
HandleRequest(context.T, *event.T) (req Request, resp Response,
|
||||
eventResponse *event.T, err error)
|
||||
}
|
||||
|
||||
type RelayReadWrite struct {
|
||||
Read, Write bool
|
||||
}
|
||||
|
||||
func IsValidBunkerURL(input string) bool {
|
||||
p, err := url.Parse(input)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if p.Scheme != "bunker" {
|
||||
return false
|
||||
}
|
||||
if !keys.IsValidPublicKey(p.Host) {
|
||||
return false
|
||||
}
|
||||
if !strings.Contains(p.RawQuery, "relay=") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
63
bunker/session.go
Normal file
63
bunker/session.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package bunker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"relay.mleku.dev/chk"
|
||||
"relay.mleku.dev/encryption"
|
||||
"relay.mleku.dev/event"
|
||||
"relay.mleku.dev/kind"
|
||||
"relay.mleku.dev/tag"
|
||||
"relay.mleku.dev/tags"
|
||||
"relay.mleku.dev/timestamp"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
Pubkey string
|
||||
SharedKey []byte
|
||||
ConversationKey []byte
|
||||
}
|
||||
|
||||
func (s *Session) ParseRequest(ev *event.T) (req *Request, err error) {
|
||||
var b []byte
|
||||
if b, err = encryption.Decrypt(ev.Content, s.ConversationKey); chk.E(err) {
|
||||
if b, err = encryption.DecryptNip4(ev.Content, s.SharedKey); chk.E(err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err = json.Unmarshal(b, &req); chk.E(err) {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Session) MakeResponse(id, requester, result string,
|
||||
inErr error) (resp *Response,
|
||||
ev *event.T, err error) {
|
||||
if inErr != nil {
|
||||
resp = &Response{
|
||||
ID: string(id),
|
||||
Result: inErr.Error(),
|
||||
}
|
||||
} else if len(result) > 0 {
|
||||
resp = &Response{
|
||||
ID: string(id),
|
||||
Result: string(result),
|
||||
}
|
||||
}
|
||||
var j []byte
|
||||
if j, err = json.Marshal(resp); chk.E(err) {
|
||||
return
|
||||
}
|
||||
var ciphertext []byte
|
||||
if ciphertext, err = encryption.Encrypt(j, s.ConversationKey); chk.E(err) {
|
||||
return
|
||||
}
|
||||
ev = &event.T{
|
||||
Content: ciphertext,
|
||||
CreatedAt: timestamp.Now(),
|
||||
Kind: kind.NostrConnect,
|
||||
Tags: tags.New(tag.New("p", requester)),
|
||||
}
|
||||
return
|
||||
}
|
||||
199
bunker/static.go
Normal file
199
bunker/static.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package bunker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"relay.mleku.dev/chk"
|
||||
"relay.mleku.dev/context"
|
||||
"relay.mleku.dev/ec/schnorr"
|
||||
"relay.mleku.dev/encryption"
|
||||
"relay.mleku.dev/errorf"
|
||||
"relay.mleku.dev/event"
|
||||
"relay.mleku.dev/hex"
|
||||
"relay.mleku.dev/keys"
|
||||
"relay.mleku.dev/kind"
|
||||
"relay.mleku.dev/signer"
|
||||
)
|
||||
|
||||
type StaticKeySigner struct {
|
||||
sync.Mutex
|
||||
secretKey signer.I
|
||||
sessions map[string]*Session
|
||||
RelaysToAdvertise map[string]RelayReadWrite
|
||||
AuthorizeRequest func(harmless bool, from, secret []byte) bool
|
||||
}
|
||||
|
||||
func NewStaticKeySigner(secretKey signer.I) *StaticKeySigner {
|
||||
return &StaticKeySigner{secretKey: secretKey,
|
||||
RelaysToAdvertise: make(map[string]RelayReadWrite)}
|
||||
}
|
||||
|
||||
func (p *StaticKeySigner) GetSession(clientPubkey string) (s *Session, exists bool) {
|
||||
s, exists = p.sessions[clientPubkey]
|
||||
return
|
||||
}
|
||||
|
||||
func (p *StaticKeySigner) getOrCreateSession(clientPubkey []byte) (s *Session, err error) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
s = new(Session)
|
||||
var exists bool
|
||||
if s, exists = p.sessions[string(clientPubkey)]; exists {
|
||||
return
|
||||
}
|
||||
if s.SharedKey, err = encryption.ComputeSharedSecret(clientPubkey,
|
||||
p.secretKey.Sec()); chk.E(err) {
|
||||
return
|
||||
}
|
||||
s.ConversationKey = p.secretKey.Pub()
|
||||
s.Pubkey = hex.Enc(p.secretKey.Pub())
|
||||
// add to pool
|
||||
p.sessions[string(clientPubkey)] = s
|
||||
return
|
||||
}
|
||||
|
||||
func (p *StaticKeySigner) HandleRequest(_ context.T, ev *event.T) (req *Request, res *Response,
|
||||
eventResponse *event.T, err error) {
|
||||
if !ev.Kind.Equal(kind.NostrConnect) {
|
||||
err = errorf.E("event kind is %s, but we expected %s",
|
||||
ev.Kind.Name(), kind.NostrConnect.Name())
|
||||
return
|
||||
}
|
||||
var session *Session
|
||||
if session, err = p.getOrCreateSession(ev.Pubkey); chk.E(err) {
|
||||
return
|
||||
}
|
||||
if req, err = session.ParseRequest(ev); chk.E(err) {
|
||||
return
|
||||
}
|
||||
var secret, result []byte
|
||||
var harmless bool
|
||||
var rErr error
|
||||
switch req.Method {
|
||||
case "connect":
|
||||
if len(req.Params) >= 2 {
|
||||
secret = req.Params[1]
|
||||
}
|
||||
result = []byte("ack")
|
||||
harmless = true
|
||||
case "get_public_key":
|
||||
result = []byte(session.Pubkey)
|
||||
harmless = true
|
||||
case "sign_event":
|
||||
if len(req.Params) != 1 {
|
||||
rErr = errorf.E("wrong number of arguments to 'sign_event'")
|
||||
break
|
||||
}
|
||||
evt := &event.T{}
|
||||
if rErr = json.Unmarshal(req.Params[0], evt); chk.E(rErr) {
|
||||
break
|
||||
}
|
||||
if rErr = evt.Sign(p.secretKey); chk.E(rErr) {
|
||||
break
|
||||
}
|
||||
result = evt.Serialize()
|
||||
case "get_relays":
|
||||
if result, rErr = json.Marshal(p.RelaysToAdvertise); chk.E(rErr) {
|
||||
break
|
||||
}
|
||||
harmless = true
|
||||
case "nip44_encrypt":
|
||||
var pk, sharedSecret []byte
|
||||
if pk, rErr = CheckParamsAndKey(req); chk.E(err) {
|
||||
break
|
||||
}
|
||||
if sharedSecret, rErr = p.GetConversationKey(pk); chk.E(err) {
|
||||
break
|
||||
}
|
||||
if result, rErr = encryption.Encrypt(req.Params[1], sharedSecret); chk.E(err) {
|
||||
break
|
||||
}
|
||||
case "nip44_decrypt":
|
||||
var pk, sharedSecret []byte
|
||||
if pk, rErr = CheckParamsAndKey(req); chk.E(err) {
|
||||
break
|
||||
}
|
||||
if sharedSecret, rErr = p.GetConversationKey(pk); chk.E(err) {
|
||||
break
|
||||
}
|
||||
if result, err = encryption.Decrypt(req.Params[1], sharedSecret); chk.E(err) {
|
||||
break
|
||||
}
|
||||
case "nip04_encrypt":
|
||||
var pk, sharedSecret []byte
|
||||
if pk, rErr = CheckParamsAndKey(req); chk.E(err) {
|
||||
break
|
||||
}
|
||||
if sharedSecret, rErr = p.ComputeSharedSecret(pk); chk.E(err) {
|
||||
break
|
||||
}
|
||||
if result, rErr = encryption.EncryptNip4(req.Params[1],
|
||||
sharedSecret); chk.E(err) {
|
||||
break
|
||||
}
|
||||
case "nip04_decrypt":
|
||||
var pk, sharedSecret []byte
|
||||
if pk, rErr = CheckParamsAndKey(req); chk.E(err) {
|
||||
break
|
||||
}
|
||||
if sharedSecret, rErr = p.ComputeSharedSecret(pk); chk.E(err) {
|
||||
break
|
||||
}
|
||||
if result, rErr = encryption.DecryptNip4(req.Params[1],
|
||||
sharedSecret); chk.E(err) {
|
||||
break
|
||||
}
|
||||
case "ping":
|
||||
result = []byte("pong")
|
||||
harmless = true
|
||||
default:
|
||||
rErr = errorf.E("unknown method '%s'", req.Method)
|
||||
}
|
||||
if rErr == nil && p.AuthorizeRequest != nil {
|
||||
if !p.AuthorizeRequest(harmless, ev.Pubkey, secret) {
|
||||
rErr = fmt.Errorf("unauthorized")
|
||||
}
|
||||
}
|
||||
if res, eventResponse, err = session.MakeResponse(req.ID, hex.Enc(ev.Pubkey),
|
||||
string(result), rErr); chk.E(err) {
|
||||
return
|
||||
}
|
||||
if err = eventResponse.Sign(p.secretKey); chk.E(err) {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *StaticKeySigner) GetConversationKey(pk []byte) (sharedSecret []byte, rErr error) {
|
||||
if sharedSecret, rErr = encryption.GenerateConversationKey(pk,
|
||||
p.secretKey.Sec()); chk.E(rErr) {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *StaticKeySigner) ComputeSharedSecret(pk []byte) (sharedSecret []byte, rErr error) {
|
||||
if sharedSecret, rErr = encryption.ComputeSharedSecret(pk,
|
||||
p.secretKey.Sec()); chk.E(rErr) {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func CheckParamsAndKey(req *Request) (pk []byte, rErr error) {
|
||||
if len(req.Params) != 2 {
|
||||
rErr = errorf.E("wrong number of arguments to 'nip04_decrypt'")
|
||||
return
|
||||
}
|
||||
if !keys.IsValidPublicKey(req.Params[0]) {
|
||||
rErr = errorf.E("first argument to 'nip04_decrypt' is not a pubkey string")
|
||||
return
|
||||
}
|
||||
pk = make([]byte, schnorr.PubKeyBytesLen)
|
||||
if _, rErr = hex.DecBytes(pk, req.Params[0]); chk.E(rErr) {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -5,32 +5,22 @@ import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
|
||||
"lukechampine.com/frand"
|
||||
|
||||
"relay.mleku.dev/hex"
|
||||
"relay.mleku.dev/p256k"
|
||||
|
||||
"relay.mleku.dev/chk"
|
||||
"relay.mleku.dev/errorf"
|
||||
"relay.mleku.dev/p256k"
|
||||
)
|
||||
|
||||
// ComputeSharedSecret returns a shared secret key used to encrypt messages. The private and
|
||||
// public keys should be hex encoded. Uses the Diffie-Hellman key exchange (ECDH) (RFC 4753).
|
||||
func ComputeSharedSecret(pkh, skh string) (sharedSecret []byte, err error) {
|
||||
var skb, pkb []byte
|
||||
if skb, err = hex.Dec(skh); chk.E(err) {
|
||||
return
|
||||
}
|
||||
if pkb, err = hex.Dec(pkh); chk.E(err) {
|
||||
return
|
||||
}
|
||||
func ComputeSharedSecret(pk, sk []byte) (sharedSecret []byte, err error) {
|
||||
signer := new(p256k.Signer)
|
||||
if err = signer.InitSec(skb); chk.E(err) {
|
||||
if err = signer.InitSec(sk); chk.E(err) {
|
||||
return
|
||||
}
|
||||
if sharedSecret, err = signer.ECDH(pkb); chk.E(err) {
|
||||
if sharedSecret, err = signer.ECDH(pk); chk.E(err) {
|
||||
return
|
||||
}
|
||||
return
|
||||
@@ -42,7 +32,7 @@ func ComputeSharedSecret(pkh, skh string) (sharedSecret []byte, err error) {
|
||||
// Returns: base64(encrypted_bytes) + "?iv=" + base64(initialization_vector).
|
||||
//
|
||||
// Deprecated: upgrade to using Decrypt with the NIP-44 algorithm.
|
||||
func EncryptNip4(msg string, key []byte) (ct []byte, err error) {
|
||||
func EncryptNip4(msg []byte, key []byte) (ct []byte, err error) {
|
||||
// block size is 16 bytes
|
||||
iv := make([]byte, 16)
|
||||
if _, err = frand.Read(iv); chk.E(err) {
|
||||
@@ -56,7 +46,7 @@ func EncryptNip4(msg string, key []byte) (ct []byte, err error) {
|
||||
return
|
||||
}
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
plaintext := []byte(msg)
|
||||
plaintext := msg
|
||||
// add padding
|
||||
base := len(plaintext)
|
||||
// this will be a number between 1 and 16 (inclusive), never 0
|
||||
@@ -75,19 +65,19 @@ func EncryptNip4(msg string, key []byte) (ct []byte, err error) {
|
||||
// EncryptNip4(message, key).
|
||||
//
|
||||
// Deprecated: upgrade to using Decrypt with the NIP-44 algorithm.
|
||||
func DecryptNip4(content string, key []byte) (msg []byte, err error) {
|
||||
parts := strings.Split(content, "?iv=")
|
||||
func DecryptNip4(content, key []byte) (msg []byte, err error) {
|
||||
parts := bytes.Split(content, []byte("?iv="))
|
||||
if len(parts) < 2 {
|
||||
return nil, errorf.E(
|
||||
"error parsing encrypted message: no initialization vector")
|
||||
}
|
||||
var ciphertext []byte
|
||||
if ciphertext, err = base64.StdEncoding.DecodeString(parts[0]); chk.E(err) {
|
||||
ciphertext := make([]byte, base64.StdEncoding.EncodedLen(len(parts[0])))
|
||||
iv := make([]byte, base64.StdEncoding.EncodedLen(len(parts[1])))
|
||||
if _, err = base64.StdEncoding.Decode(ciphertext, parts[0]); chk.E(err) {
|
||||
err = errorf.E("error decoding ciphertext from base64: %w", err)
|
||||
return
|
||||
}
|
||||
var iv []byte
|
||||
if iv, err = base64.StdEncoding.DecodeString(parts[1]); chk.E(err) {
|
||||
if _, err = base64.StdEncoding.Decode(iv, parts[1]); chk.E(err) {
|
||||
err = errorf.E("error decoding iv from base64: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@ func WithCustomNonce(salt []byte) func(opts *Opts) {
|
||||
|
||||
// Encrypt data using a provided symmetric conversation key using NIP-44 encryption (chacha20
|
||||
// cipher stream and sha256 HMAC).
|
||||
func Encrypt(plaintext string, conversationKey []byte,
|
||||
applyOptions ...func(opts *Opts)) (cipherString string,
|
||||
func Encrypt(plaintext, conversationKey []byte,
|
||||
applyOptions ...func(opts *Opts)) (cipherString []byte,
|
||||
err error) {
|
||||
|
||||
var o Opts
|
||||
@@ -67,7 +67,7 @@ func Encrypt(plaintext string, conversationKey []byte,
|
||||
if enc, cc20nonce, auth, err = getKeys(conversationKey, o.nonce); chk.E(err) {
|
||||
return
|
||||
}
|
||||
plain := []byte(plaintext)
|
||||
plain := plaintext
|
||||
size := len(plain)
|
||||
if size < MinPlaintextSize || size > MaxPlaintextSize {
|
||||
err = errorf.E("plaintext should be between 1b and 64kB")
|
||||
@@ -90,25 +90,26 @@ func Encrypt(plaintext string, conversationKey []byte,
|
||||
ct = append(ct, o.nonce...)
|
||||
ct = append(ct, cipher...)
|
||||
ct = append(ct, mac...)
|
||||
cipherString = base64.StdEncoding.EncodeToString(ct)
|
||||
cipherString = make([]byte, base64.StdEncoding.EncodedLen(len(ct)))
|
||||
base64.StdEncoding.Encode(cipherString, ct)
|
||||
return
|
||||
}
|
||||
|
||||
// Decrypt data that has been encoded using a provided symmetric conversation key using NIP-44
|
||||
// encryption (chacha20 cipher stream and sha256 HMAC).
|
||||
func Decrypt(b64ciphertextWrapped string, conversationKey []byte) (plaintext string,
|
||||
func Decrypt(b64ciphertextWrapped []byte, conversationKey []byte) (plaintext []byte,
|
||||
err error) {
|
||||
cLen := len(b64ciphertextWrapped)
|
||||
if cLen < 132 || cLen > 87472 {
|
||||
err = errorf.E("invalid payload length: %d", cLen)
|
||||
return
|
||||
}
|
||||
if b64ciphertextWrapped[:1] == "#" {
|
||||
if b64ciphertextWrapped[0] == '#' {
|
||||
err = errorf.E("unknown version")
|
||||
return
|
||||
}
|
||||
var decoded []byte
|
||||
if decoded, err = base64.StdEncoding.DecodeString(b64ciphertextWrapped); chk.E(err) {
|
||||
if decoded, err = base64.StdEncoding.DecodeString(string(b64ciphertextWrapped)); chk.E(err) {
|
||||
return
|
||||
}
|
||||
if decoded[0] != version {
|
||||
@@ -143,25 +144,18 @@ func Decrypt(b64ciphertextWrapped string, conversationKey []byte) (plaintext str
|
||||
err = errorf.E("invalid padding")
|
||||
return
|
||||
}
|
||||
unpadded := padded[2:][:unpaddedLen]
|
||||
if len(unpadded) == 0 || len(unpadded) != int(unpaddedLen) {
|
||||
plaintext = padded[2:][:unpaddedLen]
|
||||
if len(plaintext) == 0 || len(plaintext) != int(unpaddedLen) {
|
||||
err = errorf.E("invalid padding")
|
||||
return
|
||||
}
|
||||
plaintext = string(unpadded)
|
||||
return
|
||||
}
|
||||
|
||||
// GenerateConversationKey performs an ECDH key generation hashed with the nip-44-v2 using hkdf.
|
||||
func GenerateConversationKey(pkh, skh string) (ck []byte, err error) {
|
||||
if skh >= "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141" ||
|
||||
skh == "0000000000000000000000000000000000000000000000000000000000000000" {
|
||||
err = errorf.E("invalid private key: x coordinate %s is not on the secp256k1 curve",
|
||||
skh)
|
||||
return
|
||||
}
|
||||
func GenerateConversationKey(pk, sk []byte) (ck []byte, err error) {
|
||||
var shared []byte
|
||||
if shared, err = ComputeSharedSecret(pkh, skh); chk.E(err) {
|
||||
if shared, err = ComputeSharedSecret(pk, sk); chk.E(err) {
|
||||
return
|
||||
}
|
||||
ck = hkdf.Extract(sha256.New, shared, []byte("nip44-v2"))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
16
keys/keys.go
16
keys/keys.go
@@ -34,7 +34,7 @@ func GenerateSecretKeyHex() (sks string) {
|
||||
return hex.Enc(skb)
|
||||
}
|
||||
|
||||
// GetPublicKeyHex generates a public key from a hex encoded secret key.
|
||||
// GetPublicKeyHex generates a hex encoded public key from a hex encoded secret key.
|
||||
func GetPublicKeyHex(sk string) (pk string, err error) {
|
||||
var b []byte
|
||||
if b, err = hex.Dec(sk); chk.E(err) {
|
||||
@@ -48,6 +48,20 @@ func GetPublicKeyHex(sk string) (pk string, err error) {
|
||||
return hex.Enc(signer.Pub()), nil
|
||||
}
|
||||
|
||||
// GetPublicKey generates a public key from a hex encoded secret key.
|
||||
func GetPublicKey(sk string) (pk []byte, err error) {
|
||||
var b []byte
|
||||
if b, err = hex.Dec(sk); chk.E(err) {
|
||||
return
|
||||
}
|
||||
signer := &p256k.Signer{}
|
||||
if err = signer.InitSec(b); chk.E(err) {
|
||||
return
|
||||
}
|
||||
pk = signer.Pub()
|
||||
return
|
||||
}
|
||||
|
||||
// SecretBytesToPubKeyHex generates a public key from secret key bytes.
|
||||
func SecretBytesToPubKeyHex(skb []byte) (pk string, err error) {
|
||||
signer := &p256k.Signer{}
|
||||
|
||||
@@ -55,3 +55,8 @@ func (b *Backend) QueryEvents(c context.T, f *filter.T) (ch event.Ts, err error)
|
||||
func (b *Backend) SaveEvent(c context.T, ev *event.T) (err error) {
|
||||
return b.Backend.SaveEvent(c, ev)
|
||||
}
|
||||
|
||||
func (b *Backend) SetLogLevel(level string) {
|
||||
b.L2.SetLogLevel(level)
|
||||
b.L1.SetLogLevel(level)
|
||||
}
|
||||
|
||||
@@ -267,3 +267,8 @@ func (b *Backend) Sync() (err error) {
|
||||
err = errors.Join(err1, err2)
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Backend) SetLogLevel(level string) {
|
||||
b.L2.SetLogLevel(level)
|
||||
b.L1.SetLogLevel(level)
|
||||
}
|
||||
|
||||
@@ -61,8 +61,6 @@ func (s *Signer) InitSec(skb []byte) (err error) {
|
||||
s.skb = skb
|
||||
s.SecretKey = &cs.Key
|
||||
s.PublicKey = cx.Key
|
||||
// s.ECPublicKey = cp.Key
|
||||
// needed for ecdh
|
||||
s.BTCECSec, _ = btcec.PrivKeyFromBytes(s.skb)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"relay.mleku.dev/tags"
|
||||
)
|
||||
|
||||
const RELAY = "wss://mleku.realy.lol"
|
||||
const RELAY = "wss://realy.mleku.dev"
|
||||
|
||||
// // test if we can fetch a couple of random events
|
||||
// func TestSubscribeBasic(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user