mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-03-29 11:12:04 +01:00
nip46: client.
This commit is contained in:
parent
09ea5d5758
commit
60359110d7
180
nip46/client.go
Normal file
180
nip46/client.go
Normal file
@ -0,0 +1,180 @@
|
||||
package nip46
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/mailru/easyjson"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip04"
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
)
|
||||
|
||||
type BunkerClient struct {
|
||||
serial atomic.Uint64
|
||||
clientSecretKey string
|
||||
pool *nostr.SimplePool
|
||||
target string
|
||||
relays []string
|
||||
sharedSecret []byte
|
||||
listeners *xsync.MapOf[string, chan Response]
|
||||
|
||||
// memoized
|
||||
getPublicKeyResponse string
|
||||
}
|
||||
|
||||
// ConnectBunker establishes an RPC connection to a NIP-46 signer using the relays and secret provided in the bunkerURL.
|
||||
// pool can be passed to reuse an existing pool, otherwise a new pool will be created.
|
||||
func ConnectBunker(
|
||||
ctx context.Context,
|
||||
clientSecretKey string,
|
||||
bunkerURL string,
|
||||
pool *nostr.SimplePool,
|
||||
) (*BunkerClient, error) {
|
||||
parsed, err := url.Parse(bunkerURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid url: %w", err)
|
||||
}
|
||||
|
||||
if parsed.Scheme != "bunker" {
|
||||
return nil, fmt.Errorf("wrong scheme '%s', must be bunker://", parsed.Scheme)
|
||||
}
|
||||
|
||||
target := parsed.Host
|
||||
if !nostr.IsValidPublicKey(target) {
|
||||
return nil, fmt.Errorf("'%s' is not a valid public key hex", target)
|
||||
}
|
||||
|
||||
secret := parsed.Query().Get("secret")
|
||||
relays := parsed.Query()["relay"]
|
||||
|
||||
if pool == nil {
|
||||
pool = nostr.NewSimplePool(ctx)
|
||||
}
|
||||
|
||||
shared, err := nip04.ComputeSharedSecret(target, clientSecretKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compute shared secret: %w", err)
|
||||
}
|
||||
|
||||
clientPubKey, _ := nostr.GetPublicKey(clientSecretKey)
|
||||
bunker := &BunkerClient{
|
||||
clientSecretKey: clientSecretKey,
|
||||
pool: pool,
|
||||
target: target,
|
||||
relays: relays,
|
||||
sharedSecret: shared,
|
||||
listeners: xsync.NewMapOf[string, chan Response](),
|
||||
}
|
||||
|
||||
go func() {
|
||||
events := pool.SubMany(ctx, relays, nostr.Filters{
|
||||
{
|
||||
Tags: nostr.TagMap{"p": []string{clientPubKey}},
|
||||
Kinds: []int{nostr.KindNostrConnect},
|
||||
},
|
||||
})
|
||||
for ie := range events {
|
||||
if ie.Kind != nostr.KindNostrConnect {
|
||||
continue
|
||||
}
|
||||
|
||||
var resp Response
|
||||
plain, err := nip04.Decrypt(ie.Content, shared)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(plain), &resp)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if dispatcher, ok := bunker.listeners.Load(resp.ID); ok {
|
||||
dispatcher <- resp
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ourPubkey, _ := nostr.GetPublicKey(clientSecretKey)
|
||||
_, err = bunker.RPC(ctx, "connect", []string{ourPubkey, secret})
|
||||
return bunker, err
|
||||
}
|
||||
|
||||
func (bunker *BunkerClient) Ping(ctx context.Context) error {
|
||||
_, err := bunker.RPC(ctx, "ping", []string{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bunker *BunkerClient) GetPublicKey(ctx context.Context) (string, error) {
|
||||
if bunker.getPublicKeyResponse != "" {
|
||||
return bunker.getPublicKeyResponse, nil
|
||||
}
|
||||
resp, err := bunker.RPC(ctx, "get_public_key", []string{})
|
||||
bunker.getPublicKeyResponse = resp
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (bunker *BunkerClient) SignEvent(ctx context.Context, evt *nostr.Event) error {
|
||||
resp, err := bunker.RPC(ctx, "sign_event", []string{evt.String()})
|
||||
if err == nil {
|
||||
err = easyjson.Unmarshal([]byte(resp), evt)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (bunker *BunkerClient) RPC(ctx context.Context, method string, params []string) (string, error) {
|
||||
id := strconv.FormatUint(bunker.serial.Add(1), 10)
|
||||
req, err := json.Marshal(Request{
|
||||
ID: id,
|
||||
Method: method,
|
||||
Params: params,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
content, err := nip04.Encrypt(string(req), bunker.sharedSecret)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error encrypting request: %w", err)
|
||||
}
|
||||
|
||||
evt := nostr.Event{
|
||||
Content: content,
|
||||
CreatedAt: nostr.Now(),
|
||||
Kind: nostr.KindNostrConnect,
|
||||
Tags: nostr.Tags{{"p", bunker.target}},
|
||||
}
|
||||
if err := evt.Sign(bunker.clientSecretKey); err != nil {
|
||||
return "", fmt.Errorf("failed to sign request event: %w", err)
|
||||
}
|
||||
|
||||
respWaiter := make(chan Response)
|
||||
bunker.listeners.Store(id, respWaiter)
|
||||
|
||||
hasWorked := false
|
||||
for _, r := range bunker.relays {
|
||||
relay, err := bunker.pool.EnsureRelay(r)
|
||||
if err == nil {
|
||||
hasWorked = true
|
||||
}
|
||||
relay.Publish(ctx, evt)
|
||||
}
|
||||
if !hasWorked {
|
||||
return "", fmt.Errorf("couldn't connect to any relay")
|
||||
}
|
||||
|
||||
resp := <-respWaiter
|
||||
if resp.Error != "" {
|
||||
return "", fmt.Errorf("response error: %s", resp.Error)
|
||||
}
|
||||
|
||||
return resp.Result, nil
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user