backporting relay client and pool from latest go-nostr

This commit is contained in:
2025-08-06 22:18:26 +01:00
parent a7dd958585
commit 9c8ff2976d
41 changed files with 1954 additions and 1623 deletions

View File

@@ -10,6 +10,7 @@ import (
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/encoders/filter"
"orly.dev/pkg/encoders/filters"
"orly.dev/pkg/encoders/hex"
"orly.dev/pkg/encoders/kind"
"orly.dev/pkg/encoders/kinds"
"orly.dev/pkg/encoders/tag"
@@ -19,12 +20,13 @@ import (
"orly.dev/pkg/protocol/ws"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/log"
"orly.dev/pkg/utils/values"
)
type Client struct {
pool *ws.Pool
relays []string
client *ws.Client
relay string
clientSecretKey signer.I
walletPublicKey []byte
conversationKey []byte // nip44
@@ -66,9 +68,13 @@ func NewClient(c context.T, connectionURI string) (cl *Client, err error) {
); chk.E(err) {
return
}
var relay *ws.Client
if relay, err = ws.RelayConnect(c, parts.relay); chk.E(err) {
return
}
cl = &Client{
pool: ws.NewPool(c),
relays: parts.relays,
client: relay,
relay: parts.relay,
clientSecretKey: clientKey,
walletPublicKey: parts.walletPublicKey,
conversationKey: ck,
@@ -81,14 +87,9 @@ type rpcOptions struct {
}
func (cl *Client) RPC(
c context.T, method Capability, params, result any, opts *rpcOptions,
) (err error) {
timeout := time.Duration(10)
if opts != nil && opts.timeout != nil {
timeout = *opts.timeout
}
ctx, cancel := context.Timeout(c, timeout)
defer cancel()
c context.T, method Capability, params, result any, noUnmarshal bool,
opts *rpcOptions,
) (raw []byte, err error) {
var req []byte
if req, err = json.Marshal(
Request{
@@ -107,155 +108,58 @@ func (cl *Client) RPC(
CreatedAt: timestamp.Now(),
Kind: kind.WalletRequest,
Tags: tags.New(
tag.New([]byte("p"), cl.walletPublicKey),
tag.New("p", hex.Enc(cl.walletPublicKey)),
tag.New(EncryptionTag, Nip44V2),
),
}
if err = ev.Sign(cl.clientSecretKey); chk.E(err) {
return
}
hasWorked := make(chan struct{})
evs := cl.pool.SubMany(
c, cl.relays, &filters.T{
F: []*filter.F{
{
Limit: values.ToUintPointer(1),
Kinds: kinds.New(kind.WalletRequest),
Authors: tag.New(cl.walletPublicKey),
Tags: tags.New(tag.New([]byte("#e"), ev.ID)),
},
},
},
)
for _, u := range cl.relays {
go func(u string) {
var relay *ws.Client
if relay, err = cl.pool.EnsureRelay(u); chk.E(err) {
return
}
if err = relay.Publish(c, ev); chk.E(err) {
return
}
select {
case hasWorked <- struct{}{}:
case <-ctx.Done():
err = fmt.Errorf("context canceled waiting for request send")
return
default:
}
}(u)
var ok bool
if ok, err = ev.Verify(); chk.E(err) {
}
select {
case <-hasWorked:
// continue
case <-ctx.Done():
err = fmt.Errorf("timed out waiting for relays")
log.I.F("verify: %v", ok)
var rc *ws.Client
if rc, err = ws.RelayConnect(c, cl.relay); chk.E(err) {
return
}
defer rc.Close()
var sub *ws.Subscription
if sub, err = rc.Subscribe(
c, filters.New(
&filter.F{
Limit: values.ToUintPointer(1),
Kinds: kinds.New(kind.WalletRequest),
Authors: tag.New(cl.clientSecretKey.Pub()),
Tags: tags.New(tag.New([]byte("#e"), ev.ID)),
},
),
); chk.E(err) {
return
}
defer sub.Unsub()
if err = rc.Publish(context.Bg(), ev); chk.E(err) {
return
}
log.I.F("published event %s", ev.Marshal(nil))
select {
case <-ctx.Done():
case <-c.Done():
err = fmt.Errorf("context canceled waiting for response")
case e := <-evs:
var plain []byte
if plain, err = encryption.Decrypt(
e.Event.Content, cl.conversationKey,
case e := <-sub.Events:
if raw, err = encryption.Decrypt(
e.Content, cl.conversationKey,
); chk.E(err) {
return
}
if noUnmarshal {
return
}
resp := &Response{
Result: &result,
}
if err = json.Unmarshal(plain, resp); chk.E(err) {
if err = json.Unmarshal(raw, resp); chk.E(err) {
return
}
return
}
return
}
// RPCRaw performs an RPC call and returns the raw JSON response
func (cl *Client) RPCRaw(
c context.T, method Capability, params any, opts *rpcOptions,
) (rawResponse []byte, err error) {
timeout := time.Duration(10)
if opts != nil && opts.timeout != nil {
timeout = *opts.timeout
}
ctx, cancel := context.Timeout(c, timeout)
defer cancel()
var req []byte
if req, err = json.Marshal(
Request{
Method: string(method),
Params: params,
},
); chk.E(err) {
return
}
var content []byte
if content, err = encryption.Encrypt(req, cl.conversationKey); chk.E(err) {
return
}
ev := &event.E{
Content: content,
CreatedAt: timestamp.Now(),
Kind: kind.WalletRequest,
Tags: tags.New(
tag.New([]byte("p"), cl.walletPublicKey),
tag.New(EncryptionTag, Nip44V2),
),
}
if err = ev.Sign(cl.clientSecretKey); chk.E(err) {
return
}
hasWorked := make(chan struct{})
evs := cl.pool.SubMany(
c, cl.relays, &filters.T{
F: []*filter.F{
{
Limit: values.ToUintPointer(1),
Kinds: kinds.New(kind.WalletRequest),
Authors: tag.New(cl.walletPublicKey),
Tags: tags.New(tag.New([]byte("#e"), ev.ID)),
},
},
},
)
for _, u := range cl.relays {
go func(u string) {
var relay *ws.Client
if relay, err = cl.pool.EnsureRelay(u); chk.E(err) {
return
}
if err = relay.Publish(c, ev); chk.E(err) {
return
}
select {
case hasWorked <- struct{}{}:
case <-ctx.Done():
err = fmt.Errorf("context canceled waiting for request send")
return
default:
}
}(u)
}
select {
case <-hasWorked:
// continue
case <-ctx.Done():
err = fmt.Errorf("timed out waiting for relays")
return
}
select {
case <-ctx.Done():
err = fmt.Errorf("context canceled waiting for response")
case e := <-evs:
if rawResponse, err = encryption.Decrypt(
e.Event.Content, cl.conversationKey,
); chk.E(err) {
return
}
return
}
return
}

View File

@@ -2,42 +2,55 @@ package nwc
import (
"bytes"
"encoding/json"
"fmt"
"time"
"orly.dev/pkg/encoders/filter"
"orly.dev/pkg/encoders/filters"
"orly.dev/pkg/encoders/kind"
"orly.dev/pkg/encoders/kinds"
"orly.dev/pkg/encoders/tag"
"orly.dev/pkg/protocol/ws"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/values"
)
func (cl *Client) GetWalletServiceInfo(c context.T) (
wsi *WalletServiceInfo, err error,
func (cl *Client) GetWalletServiceInfo(c context.T, noUnmarshal bool) (
wsi *WalletServiceInfo, raw []byte, err error,
) {
lim := uint(1)
evc := cl.pool.SubMany(
c, cl.relays, &filters.T{
F: []*filter.F{
{
Limit: &lim,
Kinds: kinds.New(kind.WalletInfo),
Authors: tag.New(cl.walletPublicKey),
},
timeout := 10 * time.Second
ctx, cancel := context.Timeout(c, timeout)
defer cancel()
var rc *ws.Client
if rc, err = ws.RelayConnect(c, cl.relay); chk.E(err) {
return
}
if err = rc.Connect(c); chk.E(err) {
return
}
var sub *ws.Subscription
if sub, err = rc.Subscribe(
ctx, filters.New(
&filter.F{
Limit: values.ToUintPointer(1),
Kinds: kinds.New(kind.WalletRequest),
Authors: tag.New(cl.walletPublicKey),
},
},
)
),
); chk.E(err) {
return
}
defer sub.Unsub()
select {
case <-c.Done():
err = fmt.Errorf("GetWalletServiceInfo canceled")
return
case ev := <-evc:
case ev := <-sub.Events:
var encryptionTypes []EncryptionType
var notificationTypes []NotificationType
encryptionTag := ev.Event.Tags.GetFirst(tag.New("encryption"))
notificationsTag := ev.Event.Tags.GetFirst(tag.New("notifications"))
encryptionTag := ev.Tags.GetFirst(tag.New("encryption"))
notificationsTag := ev.Tags.GetFirst(tag.New("notifications"))
if encryptionTag != nil {
et := encryptionTag.ToSliceOfBytes()
encType := bytes.Split(et[0], []byte(" "))
@@ -52,7 +65,7 @@ func (cl *Client) GetWalletServiceInfo(c context.T) (
notificationTypes = append(notificationTypes, e)
}
}
cp := bytes.Split(ev.Event.Content, []byte(" "))
cp := bytes.Split(ev.Content, []byte(" "))
var capabilities []Capability
for _, capability := range cp {
capabilities = append(capabilities, capability)
@@ -66,190 +79,72 @@ func (cl *Client) GetWalletServiceInfo(c context.T) (
return
}
func (cl *Client) GetWalletServiceInfoRaw(c context.T) (
raw []byte, err error,
) {
lim := uint(1)
evc := cl.pool.SubMany(
c, cl.relays, &filters.T{
F: []*filter.F{
{
Limit: &lim,
Kinds: kinds.New(kind.WalletInfo),
Authors: tag.New(cl.walletPublicKey),
},
},
},
)
select {
case <-c.Done():
err = fmt.Errorf("GetWalletServiceInfoRaw canceled")
return
case ev := <-evc:
// Marshal the event to JSON
if raw, err = json.Marshal(ev.Event); chk.E(err) {
return
}
}
return
}
func (cl *Client) CancelHoldInvoice(
c context.T, chi *CancelHoldInvoiceParams,
) (err error) {
if err = cl.RPC(c, CancelHoldInvoice, chi, nil, nil); chk.E(err) {
return
}
return
}
func (cl *Client) CancelHoldInvoiceRaw(
c context.T, chi *CancelHoldInvoiceParams,
c context.T, chi *CancelHoldInvoiceParams, noUnmarshal bool,
) (raw []byte, err error) {
if raw, err = cl.RPCRaw(c, CancelHoldInvoice, chi, nil); chk.E(err) {
return
}
return
return cl.RPC(c, CancelHoldInvoice, chi, nil, noUnmarshal, nil)
}
func (cl *Client) CreateConnection(
c context.T, cc *CreateConnectionParams,
) (err error) {
if err = cl.RPC(c, CreateConnection, cc, nil, nil); chk.E(err) {
return
}
return
}
func (cl *Client) CreateConnectionRaw(
c context.T, cc *CreateConnectionParams,
c context.T, cc *CreateConnectionParams, noUnmarshal bool,
) (raw []byte, err error) {
if raw, err = cl.RPCRaw(c, CreateConnection, cc, nil); chk.E(err) {
return
}
return
return cl.RPC(c, CreateConnection, cc, nil, noUnmarshal, nil)
}
func (cl *Client) GetBalance(c context.T) (gb *GetBalanceResult, err error) {
func (cl *Client) GetBalance(c context.T, noUnmarshal bool) (
gb *GetBalanceResult, raw []byte, err error,
) {
gb = &GetBalanceResult{}
if err = cl.RPC(c, GetBalance, nil, gb, nil); chk.E(err) {
return
}
raw, err = cl.RPC(c, GetBalance, nil, gb, noUnmarshal, nil)
return
}
func (cl *Client) GetBalanceRaw(c context.T) (raw []byte, err error) {
if raw, err = cl.RPCRaw(c, GetBalance, nil, nil); chk.E(err) {
return
}
return
}
func (cl *Client) GetBudget(c context.T) (gb *GetBudgetResult, err error) {
func (cl *Client) GetBudget(c context.T, noUnmarshal bool) (
gb *GetBudgetResult, raw []byte, err error,
) {
gb = &GetBudgetResult{}
if err = cl.RPC(c, GetBudget, nil, gb, nil); chk.E(err) {
return
}
raw, err = cl.RPC(c, GetBudget, nil, gb, noUnmarshal, nil)
return
}
func (cl *Client) GetBudgetRaw(c context.T) (raw []byte, err error) {
if raw, err = cl.RPCRaw(c, GetBudget, nil, nil); chk.E(err) {
return
}
return
}
func (cl *Client) GetInfo(c context.T) (gi *GetInfoResult, err error) {
func (cl *Client) GetInfo(c context.T, noUnmarshal bool) (
gi *GetInfoResult, raw []byte, err error,
) {
gi = &GetInfoResult{}
if err = cl.RPC(c, GetInfo, nil, gi, nil); chk.E(err) {
return
}
return
}
func (cl *Client) GetInfoRaw(c context.T) (raw []byte, err error) {
if raw, err = cl.RPCRaw(c, GetInfo, nil, nil); chk.E(err) {
return
}
raw, err = cl.RPC(c, GetInfo, nil, gi, noUnmarshal, nil)
return
}
func (cl *Client) ListTransactions(
c context.T, params *ListTransactionsParams,
) (lt *ListTransactionsResult, err error) {
c context.T, params *ListTransactionsParams, noUnmarshal bool,
) (lt *ListTransactionsResult, raw []byte, err error) {
lt = &ListTransactionsResult{}
if err = cl.RPC(c, ListTransactions, params, &lt, nil); chk.E(err) {
return
}
return
}
func (cl *Client) ListTransactionsRaw(
c context.T, params *ListTransactionsParams,
) (raw []byte, err error) {
if raw, err = cl.RPCRaw(c, ListTransactions, params, nil); chk.E(err) {
return
}
raw, err = cl.RPC(c, ListTransactions, params, &lt, noUnmarshal, nil)
return
}
func (cl *Client) LookupInvoice(
c context.T, params *LookupInvoiceParams,
) (li *LookupInvoiceResult, err error) {
c context.T, params *LookupInvoiceParams, noUnmarshal bool,
) (li *LookupInvoiceResult, raw []byte, err error) {
li = &LookupInvoiceResult{}
if err = cl.RPC(c, LookupInvoice, params, &li, nil); chk.E(err) {
return
}
return
}
func (cl *Client) LookupInvoiceRaw(
c context.T, params *LookupInvoiceParams,
) (raw []byte, err error) {
if raw, err = cl.RPCRaw(c, LookupInvoice, params, nil); chk.E(err) {
return
}
raw, err = cl.RPC(c, LookupInvoice, params, &li, noUnmarshal, nil)
return
}
func (cl *Client) MakeHoldInvoice(
c context.T,
mhi *MakeHoldInvoiceParams,
) (mi *MakeInvoiceResult, err error) {
mhi *MakeHoldInvoiceParams, noUnmarshal bool,
) (mi *MakeInvoiceResult, raw []byte, err error) {
mi = &MakeInvoiceResult{}
if err = cl.RPC(c, MakeHoldInvoice, mhi, mi, nil); chk.E(err) {
return
}
return
}
func (cl *Client) MakeHoldInvoiceRaw(
c context.T,
mhi *MakeHoldInvoiceParams,
) (raw []byte, err error) {
if raw, err = cl.RPCRaw(c, MakeHoldInvoice, mhi, nil); chk.E(err) {
return
}
raw, err = cl.RPC(c, MakeHoldInvoice, mhi, mi, noUnmarshal, nil)
return
}
func (cl *Client) MakeInvoice(
c context.T, params *MakeInvoiceParams,
) (mi *MakeInvoiceResult, err error) {
c context.T, params *MakeInvoiceParams, noUnmarshal bool,
) (mi *MakeInvoiceResult, raw []byte, err error) {
mi = &MakeInvoiceResult{}
if err = cl.RPC(c, MakeInvoice, params, &mi, nil); chk.E(err) {
return
}
return
}
func (cl *Client) MakeInvoiceRaw(
c context.T, params *MakeInvoiceParams,
) (raw []byte, err error) {
if raw, err = cl.RPCRaw(c, MakeInvoice, params, nil); chk.E(err) {
return
}
raw, err = cl.RPC(c, MakeInvoice, params, &mi, noUnmarshal, nil)
return
}
@@ -258,76 +153,31 @@ func (cl *Client) MakeInvoiceRaw(
// MultiPayKeysend
func (cl *Client) PayKeysend(
c context.T, params *PayKeysendParams,
) (pk *PayKeysendResult, err error) {
c context.T, params *PayKeysendParams, noUnmarshal bool,
) (pk *PayKeysendResult, raw []byte, err error) {
pk = &PayKeysendResult{}
if err = cl.RPC(c, PayKeysend, params, &pk, nil); chk.E(err) {
return
}
return
}
func (cl *Client) PayKeysendRaw(
c context.T, params *PayKeysendParams,
) (raw []byte, err error) {
if raw, err = cl.RPCRaw(c, PayKeysend, params, nil); chk.E(err) {
return
}
raw, err = cl.RPC(c, PayKeysend, params, &pk, noUnmarshal, nil)
return
}
func (cl *Client) PayInvoice(
c context.T, params *PayInvoiceParams,
) (pi *PayInvoiceResult, err error) {
c context.T, params *PayInvoiceParams, noUnmarshal bool,
) (pi *PayInvoiceResult, raw []byte, err error) {
pi = &PayInvoiceResult{}
if err = cl.RPC(c, PayInvoice, params, &pi, nil); chk.E(err) {
return
}
return
}
func (cl *Client) PayInvoiceRaw(
c context.T, params *PayInvoiceParams,
) (raw []byte, err error) {
if raw, err = cl.RPCRaw(c, PayInvoice, params, nil); chk.E(err) {
return
}
raw, err = cl.RPC(c, PayInvoice, params, &pi, noUnmarshal, nil)
return
}
func (cl *Client) SettleHoldInvoice(
c context.T, shi *SettleHoldInvoiceParams,
) (err error) {
if err = cl.RPC(c, SettleHoldInvoice, shi, nil, nil); chk.E(err) {
return
}
return
}
func (cl *Client) SettleHoldInvoiceRaw(
c context.T, shi *SettleHoldInvoiceParams,
c context.T, shi *SettleHoldInvoiceParams, noUnmarshal bool,
) (raw []byte, err error) {
if raw, err = cl.RPCRaw(c, SettleHoldInvoice, shi, nil); chk.E(err) {
return
}
return
return cl.RPC(c, SettleHoldInvoice, shi, nil, noUnmarshal, nil)
}
func (cl *Client) SignMessage(
c context.T, sm *SignMessageParams,
) (res *SignMessageResult, err error) {
c context.T, sm *SignMessageParams, noUnmarshal bool,
) (res *SignMessageResult, raw []byte, err error) {
res = &SignMessageResult{}
if err = cl.RPC(c, SignMessage, sm, &res, nil); chk.E(err) {
return
}
return
}
func (cl *Client) SignMessageRaw(
c context.T, sm *SignMessageParams,
) (raw []byte, err error) {
if raw, err = cl.RPCRaw(c, SignMessage, sm, nil); chk.E(err) {
return
}
raw, err = cl.RPC(c, SignMessage, sm, &res, noUnmarshal, nil)
return
}

View File

@@ -11,7 +11,7 @@ import (
type ConnectionParams struct {
clientSecretKey []byte
walletPublicKey []byte
relays []string
relay string
}
// GetWalletPublicKey returns the wallet public key from the ConnectionParams.
@@ -35,13 +35,15 @@ func ParseConnectionURI(nwcUri string) (parts *ConnectionParams, err error) {
}
query := p.Query()
var ok bool
if parts.relays, ok = query["relay"]; !ok {
var relay []string
if relay, ok = query["relay"]; !ok {
err = errors.New("missing relay parameter")
return
}
if len(parts.relays) == 0 {
if len(relay) == 0 {
return nil, errors.New("no relays")
}
parts.relay = relay[0]
var secret string
if secret = query.Get("secret"); secret == "" {
err = errors.New("missing secret parameter")