Files
orly/pkg/protocol/nwc/client.go
mleku b351d0fb78 fix bugs in tag comparison code
nwc walletcli now works!

bumped to v0.5.0 because NWC client now in and available
2025-08-07 09:32:53 +01:00

160 lines
3.4 KiB
Go

package nwc
import (
"encoding/json"
"fmt"
"time"
"orly.dev/pkg/crypto/encryption"
"orly.dev/pkg/crypto/p256k"
"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"
"orly.dev/pkg/encoders/tags"
"orly.dev/pkg/encoders/timestamp"
"orly.dev/pkg/interfaces/signer"
"orly.dev/pkg/protocol/ws"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/values"
)
type Client struct {
client *ws.Client
relay string
clientSecretKey signer.I
walletPublicKey []byte
conversationKey []byte // nip44
}
type Request struct {
Method string `json:"method"`
Params any `json:"params"`
}
type ResponseError struct {
Code string `json:"code"`
Message string `json:"message"`
}
func (err *ResponseError) Error() string {
return fmt.Sprintf("%s %s", err.Code, err.Message)
}
type Response struct {
ResultType string `json:"result_type"`
Error *ResponseError `json:"error"`
Result any `json:"result"`
}
func NewClient(c context.T, connectionURI string) (cl *Client, err error) {
var parts *ConnectionParams
if parts, err = ParseConnectionURI(connectionURI); chk.E(err) {
return
}
clientKey := &p256k.Signer{}
if err = clientKey.InitSec(parts.clientSecretKey); chk.E(err) {
return
}
var ck []byte
if ck, err = encryption.GenerateConversationKeyWithSigner(
clientKey,
parts.walletPublicKey,
); chk.E(err) {
return
}
var relay *ws.Client
if relay, err = ws.RelayConnect(c, parts.relay); chk.E(err) {
return
}
cl = &Client{
client: relay,
relay: parts.relay,
clientSecretKey: clientKey,
walletPublicKey: parts.walletPublicKey,
conversationKey: ck,
}
return
}
type rpcOptions struct {
timeout *time.Duration
}
func (cl *Client) RPC(
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{
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("p", hex.Enc(cl.walletPublicKey)),
tag.New(EncryptionTag, Nip44V2),
),
}
if err = ev.Sign(cl.clientSecretKey); chk.E(err) {
return
}
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.WalletResponse),
Authors: tag.New(cl.walletPublicKey),
Tags: tags.New(tag.New("#e", hex.Enc(ev.ID))),
},
),
); chk.E(err) {
return
}
defer sub.Unsub()
if err = rc.Publish(context.Bg(), ev); chk.E(err) {
return
}
select {
case <-c.Done():
err = fmt.Errorf("context canceled waiting for response")
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(raw, resp); chk.E(err) {
return
}
}
return
}