refactor: Simplify NWC protocol structures and update method handling
- cmd/lerproxy/app/bufpool.go - Removed bufferPool-related code and `Pool` struct - cmd/nwcclient/main.go - Renamed `Method` to `Capability` for clarity in method handling - pkg/utils/values/values.go - Added utility functions to return pointers for various types - pkg/utils/pointers/pointers.go - Revised documentation to reference `utils/values` package for pointer utilities - pkg/protocol/nwc/types.go - Replaced redundant types and structures with simplified versions - Introduced dedicated structs for `MakeInvoice`, `PayInvoice`, and related results - Refactored `Transaction` and its fields for consistent type usage - pkg/protocol/nwc/uri.go - Added `ParseConnectionURI` function for URI parsing and validation - pkg/protocol/nwc/client.go - Refactored `Client` struct to improve key management and relay handling - Introduced `Request` struct for generic method invocation payloads
This commit is contained in:
@@ -1,15 +0,0 @@
|
||||
package app
|
||||
|
||||
import "sync"
|
||||
|
||||
var bufferPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
buf := make([]byte, 32*1024)
|
||||
return &buf
|
||||
},
|
||||
}
|
||||
|
||||
type Pool struct{}
|
||||
|
||||
func (bp Pool) Get() []byte { return *(bufferPool.Get().(*[]byte)) }
|
||||
func (bp Pool) Put(b []byte) { bufferPool.Put(&b) }
|
||||
@@ -45,7 +45,7 @@ func main() {
|
||||
// Parse connection URL and method
|
||||
connectionURL := os.Args[1]
|
||||
methodStr := os.Args[2]
|
||||
method := nwc.Method(methodStr)
|
||||
method := nwc.Capability(methodStr)
|
||||
|
||||
// Parse the wallet connect URL
|
||||
opts, err := nwc.ParseWalletConnectURL(connectionURL)
|
||||
|
||||
@@ -158,8 +158,8 @@ func Decrypt(b64ciphertextWrapped, conversationKey []byte) (
|
||||
return
|
||||
}
|
||||
|
||||
// GenerateConversationKey performs an ECDH key generation hashed with the nip-44-v2 using hkdf.
|
||||
func GenerateConversationKey(pkh, skh string) (ck []byte, err error) {
|
||||
// GenerateConversationKeyFromHex performs an ECDH key generation hashed with the nip-44-v2 using hkdf.
|
||||
func GenerateConversationKeyFromHex(pkh, skh string) (ck []byte, err error) {
|
||||
if skh >= "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141" ||
|
||||
skh == "0000000000000000000000000000000000000000000000000000000000000000" {
|
||||
err = errorf.E(
|
||||
@@ -184,6 +184,17 @@ func GenerateConversationKey(pkh, skh string) (ck []byte, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func GenerateConversationKeyWithSigner(sign signer.I, pk []byte) (
|
||||
ck []byte, err error,
|
||||
) {
|
||||
var shared []byte
|
||||
if shared, err = sign.ECDH(pk); chk.E(err) {
|
||||
return
|
||||
}
|
||||
ck = hkdf.Extract(sha256.New, shared, []byte("nip44-v2"))
|
||||
return
|
||||
}
|
||||
|
||||
func encrypt(key, nonce, message []byte) (dst []byte, err error) {
|
||||
var cipher *chacha20.Cipher
|
||||
if cipher, err = chacha20.NewUnauthenticatedCipher(key, nonce); chk.E(err) {
|
||||
|
||||
@@ -47,7 +47,9 @@ func assertCryptPriv(
|
||||
return
|
||||
}
|
||||
expectedBytes = []byte(expected)
|
||||
if ok = assert.Equalf(t, string(expectedBytes), string(actualBytes), "wrong encryption"); !ok {
|
||||
if ok = assert.Equalf(
|
||||
t, string(expectedBytes), string(actualBytes), "wrong encryption",
|
||||
); !ok {
|
||||
return
|
||||
}
|
||||
decrypted, err = Decrypt(expectedBytes, k1)
|
||||
@@ -62,8 +64,8 @@ func assertDecryptFail(
|
||||
) {
|
||||
var (
|
||||
k1, ciphertextBytes []byte
|
||||
ok bool
|
||||
err error
|
||||
ok bool
|
||||
err error
|
||||
)
|
||||
k1, err = hex.Dec(conversationKey)
|
||||
if ok = assert.NoErrorf(
|
||||
@@ -79,7 +81,7 @@ func assertDecryptFail(
|
||||
func assertConversationKeyFail(
|
||||
t *testing.T, priv string, pub string, msg string,
|
||||
) {
|
||||
_, err := GenerateConversationKey(pub, priv)
|
||||
_, err := GenerateConversationKeyFromHex(pub, priv)
|
||||
assert.ErrorContains(t, err, msg)
|
||||
}
|
||||
|
||||
@@ -98,7 +100,7 @@ func assertConversationKeyGeneration(
|
||||
); !ok {
|
||||
return false
|
||||
}
|
||||
actualConversationKey, err = GenerateConversationKey(pub, priv)
|
||||
actualConversationKey, err = GenerateConversationKeyFromHex(pub, priv)
|
||||
if ok = assert.NoErrorf(
|
||||
t, err, "conversation key generation failed: %v", err,
|
||||
); !ok {
|
||||
@@ -1312,7 +1314,7 @@ func TestMaxLength(t *testing.T) {
|
||||
pub2, _ := keys.GetPublicKeyHex(string(sk2))
|
||||
salt := make([]byte, 32)
|
||||
rand.Read(salt)
|
||||
conversationKey, _ := GenerateConversationKey(pub2, string(sk1))
|
||||
conversationKey, _ := GenerateConversationKeyFromHex(pub2, string(sk1))
|
||||
plaintext := strings.Repeat("a", MaxPlaintextSize)
|
||||
plaintextBytes := []byte(plaintext)
|
||||
encrypted, err := Encrypt(
|
||||
@@ -1366,7 +1368,9 @@ func assertCryptPub(
|
||||
return
|
||||
}
|
||||
expectedBytes = []byte(expected)
|
||||
if ok = assert.Equalf(t, string(expectedBytes), string(actualBytes), "wrong encryption"); !ok {
|
||||
if ok = assert.Equalf(
|
||||
t, string(expectedBytes), string(actualBytes), "wrong encryption",
|
||||
); !ok {
|
||||
return
|
||||
}
|
||||
decrypted, err = Decrypt(expectedBytes, k1)
|
||||
|
||||
@@ -275,9 +275,9 @@ var (
|
||||
WalletRequest = &T{23194}
|
||||
// NWCWalletResponse is an event type that...
|
||||
NWCWalletResponse = &T{23195}
|
||||
WalletResponse = NWCWalletResponse
|
||||
WalletResponse = &T{23195}
|
||||
NWCNotification = &T{23196}
|
||||
WalletNotification = NWCNotification
|
||||
WalletNotification = &T{23196}
|
||||
// NostrConnect is an event type that...
|
||||
NostrConnect = &T{24133}
|
||||
HTTPAuth = &T{27235}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
package nwc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"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"
|
||||
@@ -19,596 +18,265 @@ import (
|
||||
"orly.dev/pkg/protocol/ws"
|
||||
"orly.dev/pkg/utils/chk"
|
||||
"orly.dev/pkg/utils/context"
|
||||
"orly.dev/pkg/utils/log"
|
||||
"strings"
|
||||
"sync"
|
||||
"orly.dev/pkg/utils/values"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Options represents options for a NWC client
|
||||
type Options struct {
|
||||
RelayURL string
|
||||
Secret signer.I
|
||||
WalletPubkey []byte
|
||||
Lud16 string
|
||||
}
|
||||
|
||||
// Client represents a NWC client
|
||||
type Client struct {
|
||||
options Options
|
||||
relay *ws.Client
|
||||
mu sync.Mutex
|
||||
pool *ws.Pool
|
||||
relays []string
|
||||
clientSecretKey signer.I
|
||||
walletPublicKey []byte
|
||||
conversationKey []byte // nip44
|
||||
}
|
||||
|
||||
// ParseWalletConnectURL parses a wallet connect URL
|
||||
func ParseWalletConnectURL(walletConnectURL string) (opts *Options, err error) {
|
||||
if !strings.HasPrefix(walletConnectURL, "nostr+walletconnect://") {
|
||||
return nil, fmt.Errorf("unexpected scheme. Should be nostr+walletconnect://")
|
||||
}
|
||||
// Parse URL
|
||||
colonIndex := strings.Index(walletConnectURL, ":")
|
||||
if colonIndex == -1 {
|
||||
err = fmt.Errorf("invalid URL format")
|
||||
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
|
||||
}
|
||||
walletConnectURL = walletConnectURL[colonIndex+1:]
|
||||
if strings.HasPrefix(walletConnectURL, "//") {
|
||||
walletConnectURL = walletConnectURL[2:]
|
||||
}
|
||||
walletConnectURL = "https://" + walletConnectURL
|
||||
var u *url.URL
|
||||
if u, err = url.Parse(walletConnectURL); chk.E(err) {
|
||||
err = fmt.Errorf("failed to parse URL: %w", err)
|
||||
clientKey := &p256k.Signer{}
|
||||
if err = clientKey.InitSec(parts.clientSecretKey); chk.E(err) {
|
||||
return
|
||||
}
|
||||
// Get wallet pubkey
|
||||
walletPubkey := u.Host
|
||||
if len(walletPubkey) != 64 {
|
||||
err = fmt.Errorf("incorrect wallet pubkey found in auth string")
|
||||
var ck []byte
|
||||
if ck, err = encryption.GenerateConversationKeyWithSigner(
|
||||
clientKey,
|
||||
parts.walletPublicKey,
|
||||
); chk.E(err) {
|
||||
return
|
||||
}
|
||||
var pk []byte
|
||||
if pk, err = hex.Dec(walletPubkey); chk.E(err) {
|
||||
err = fmt.Errorf("failed to decode pubkey: %w", err)
|
||||
return
|
||||
}
|
||||
// Get relay URL
|
||||
relayURL := u.Query().Get("relay")
|
||||
if relayURL == "" {
|
||||
return nil, fmt.Errorf("no relay URL found in auth string")
|
||||
}
|
||||
// Get secret
|
||||
secret := u.Query().Get("secret")
|
||||
if secret == "" {
|
||||
return nil, fmt.Errorf("no secret found in auth string")
|
||||
}
|
||||
var sk []byte
|
||||
if sk, err = hex.Dec(secret); chk.E(err) {
|
||||
return
|
||||
}
|
||||
sign := &p256k.Signer{}
|
||||
if err = sign.InitSec(sk); chk.E(err) {
|
||||
return
|
||||
}
|
||||
opts = &Options{
|
||||
RelayURL: relayURL,
|
||||
Secret: sign,
|
||||
WalletPubkey: pk,
|
||||
cl = &Client{
|
||||
pool: ws.NewPool(c),
|
||||
relays: parts.relays,
|
||||
clientSecretKey: clientKey,
|
||||
walletPublicKey: parts.walletPublicKey,
|
||||
conversationKey: ck,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// NewNWCClient creates a new NWC client
|
||||
func NewNWCClient(options *Options) (cl *Client, err error) {
|
||||
if options.RelayURL == "" {
|
||||
err = fmt.Errorf("missing relay URL")
|
||||
return
|
||||
type rpcOptions struct {
|
||||
timeout *time.Duration
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if options.Secret == nil {
|
||||
err = fmt.Errorf("missing secret")
|
||||
return
|
||||
}
|
||||
if options.WalletPubkey == nil {
|
||||
err = fmt.Errorf("missing wallet pubkey")
|
||||
return
|
||||
}
|
||||
return &Client{
|
||||
options: Options{
|
||||
RelayURL: options.RelayURL,
|
||||
Secret: options.Secret,
|
||||
WalletPubkey: options.WalletPubkey,
|
||||
Lud16: options.Lud16,
|
||||
ctx, cancel := context.Timeout(c, timeout)
|
||||
defer cancel()
|
||||
var req []byte
|
||||
if req, err = json.Marshal(
|
||||
Request{
|
||||
Method: string(method),
|
||||
Params: params,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NostrWalletConnectURL returns the nostr wallet connect URL
|
||||
func (c *Client) NostrWalletConnectURL() string {
|
||||
return c.GetNostrWalletConnectURL(true)
|
||||
}
|
||||
|
||||
// GetNostrWalletConnectURL returns the nostr wallet connect URL
|
||||
func (c *Client) GetNostrWalletConnectURL(includeSecret bool) string {
|
||||
params := url.Values{}
|
||||
params.Add("relay", c.options.RelayURL)
|
||||
if includeSecret {
|
||||
params.Add("secret", hex.Enc(c.options.Secret.Sec()))
|
||||
); chk.E(err) {
|
||||
return
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"nostr+walletconnect://%s?%s", c.options.WalletPubkey, params.Encode(),
|
||||
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("encryption", "nip44_v2"),
|
||||
),
|
||||
}
|
||||
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)),
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Connected returns whether the client is connected to the relay
|
||||
func (c *Client) Connected() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.relay != nil && c.relay.IsConnected()
|
||||
}
|
||||
|
||||
// GetPublicKey returns the client's public key
|
||||
func (c *Client) GetPublicKey() (pubkey []byte, err error) {
|
||||
pubkey = c.options.Secret.Pub()
|
||||
return
|
||||
}
|
||||
|
||||
// Close closes the relay connection
|
||||
func (c *Client) Close() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.relay != nil {
|
||||
c.relay.Close()
|
||||
c.relay = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt encrypts content for a pubkey
|
||||
func (c *Client) encrypt(pubkey, content []byte) (
|
||||
cipherText []byte, err error,
|
||||
) {
|
||||
var sharedSecret []byte
|
||||
if sharedSecret, err = c.options.Secret.ECDH(pubkey); chk.E(err) {
|
||||
return
|
||||
}
|
||||
cipherText, err = encryption.EncryptNip4(content, sharedSecret)
|
||||
return
|
||||
}
|
||||
|
||||
// Decrypt decrypts content from a pubkey
|
||||
func (c *Client) decrypt(pubkey, content []byte) (plaintext []byte, err error) {
|
||||
var sharedSecret []byte
|
||||
if sharedSecret, err = c.options.Secret.ECDH(pubkey); chk.E(err) {
|
||||
return
|
||||
}
|
||||
plaintext, err = encryption.DecryptNip4(content, sharedSecret)
|
||||
return
|
||||
}
|
||||
|
||||
// GetInfo gets wallet info
|
||||
func (c *Client) GetInfo() (response *GetInfoResponse, err error) {
|
||||
var result []byte
|
||||
if result, err = c.executeRequest(GetInfo, nil); chk.E(err) {
|
||||
return
|
||||
}
|
||||
response = &GetInfoResponse{}
|
||||
if err = json.Unmarshal(result, response); err != nil {
|
||||
err = fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetBudget gets wallet budget
|
||||
func (c *Client) GetBudget() (response *GetBudgetResponse, err error) {
|
||||
var result []byte
|
||||
result, err = c.executeRequest(GetBudget, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response = &GetBudgetResponse{}
|
||||
if err = json.Unmarshal(result, response); err != nil {
|
||||
err = fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetBalance gets wallet balance
|
||||
func (c *Client) GetBalance() (response *GetBalanceResponse, err error) {
|
||||
var result []byte
|
||||
if result, err = c.executeRequest(GetBalance, nil); chk.E(err) {
|
||||
return
|
||||
}
|
||||
response = &GetBalanceResponse{}
|
||||
if err = json.Unmarshal(result, response); err != nil {
|
||||
err = fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// PayInvoice pays an invoice
|
||||
func (c *Client) PayInvoice(request *PayInvoiceRequest) (
|
||||
response *PayResponse, err error,
|
||||
) {
|
||||
var result []byte
|
||||
result, err = c.executeRequest(PayInvoice, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response = &PayResponse{}
|
||||
if err = json.Unmarshal(result, response); err != nil {
|
||||
err = fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// PayKeysend sends a keysend payment
|
||||
func (c *Client) PayKeysend(request *PayKeysendRequest) (
|
||||
response *PayResponse, err error,
|
||||
) {
|
||||
var result []byte
|
||||
if result, err = c.executeRequest(PayKeysend, request); chk.E(err) {
|
||||
return
|
||||
}
|
||||
response = &PayResponse{}
|
||||
if err = json.Unmarshal(result, response); err != nil {
|
||||
err = fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MakeInvoice creates an invoice
|
||||
func (c *Client) MakeInvoice(request *MakeInvoiceRequest) (
|
||||
response *Transaction, err error,
|
||||
) {
|
||||
var result []byte
|
||||
if result, err = c.executeRequest(MakeInvoice, request); chk.E(err) {
|
||||
return
|
||||
}
|
||||
response = &Transaction{}
|
||||
if err = json.Unmarshal(result, response); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LookupInvoice looks up an invoice
|
||||
func (c *Client) LookupInvoice(request *LookupInvoiceRequest) (
|
||||
response *Transaction, err error,
|
||||
) {
|
||||
var result []byte
|
||||
if result, err = c.executeRequest(LookupInvoice, request); chk.E(err) {
|
||||
return
|
||||
}
|
||||
response = &Transaction{}
|
||||
if err = json.Unmarshal(result, response); err != nil {
|
||||
err = fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ListTransactions lists transactions
|
||||
func (c *Client) ListTransactions(request *ListTransactionsRequest) (
|
||||
response *ListTransactionsResponse, err error,
|
||||
) {
|
||||
var result []byte
|
||||
if result, err = c.executeRequest(ListTransactions, request); chk.E(err) {
|
||||
return
|
||||
}
|
||||
response = &ListTransactionsResponse{}
|
||||
if err = json.Unmarshal(result, response); chk.E(err) {
|
||||
err = fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SignMessage signs a message
|
||||
func (c *Client) SignMessage(request *SignMessageRequest) (
|
||||
response *SignMessageResponse, err error,
|
||||
) {
|
||||
var result []byte
|
||||
if result, err = c.executeRequest(SignMessage, request); chk.E(err) {
|
||||
return
|
||||
}
|
||||
response = &SignMessageResponse{}
|
||||
if err = json.Unmarshal(result, response); err != nil {
|
||||
err = fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// NotificationHandler is a function that handles notifications
|
||||
type NotificationHandler func(*Notification)
|
||||
|
||||
// SubscribeNotifications subscribes to notifications
|
||||
func (c *Client) SubscribeNotifications(
|
||||
handler NotificationHandler,
|
||||
notificationTypes []NotificationType,
|
||||
) (stop func(), err error) {
|
||||
if handler == nil {
|
||||
err = fmt.Errorf("missing notification handler")
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.Cancel(context.Bg())
|
||||
doneCh := make(chan struct{})
|
||||
stop = func() {
|
||||
cancel()
|
||||
<-doneCh
|
||||
}
|
||||
go func() {
|
||||
defer close(doneCh)
|
||||
for {
|
||||
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:
|
||||
// Check connection
|
||||
if err := c.checkConnected(); err != nil {
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
// Get client pubkey
|
||||
var clientPubkey []byte
|
||||
if clientPubkey, err = c.GetPublicKey(); chk.E(err) {
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
// Subscribe to events
|
||||
f := &filter.F{
|
||||
Kinds: kinds.New(kind.WalletResponse),
|
||||
Authors: tag.New(c.options.WalletPubkey),
|
||||
Tags: tags.New(tag.New([]byte("#p"), clientPubkey)),
|
||||
}
|
||||
var sub *ws.Subscription
|
||||
if sub, err = c.relay.Subscribe(
|
||||
context.Bg(), &filters.T{
|
||||
F: []*filter.F{f},
|
||||
},
|
||||
); chk.E(err) {
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
// Handle events
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
sub.Close()
|
||||
return
|
||||
case ev := <-sub.Events:
|
||||
// Decrypt content
|
||||
var decryptedContent []byte
|
||||
if decryptedContent, err = c.decrypt(
|
||||
c.options.WalletPubkey, ev.Content,
|
||||
); chk.E(err) {
|
||||
log.E.F(
|
||||
"Failed to decrypt event content: %v\n", err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
// Parse notification
|
||||
notification := &Notification{}
|
||||
if err = json.Unmarshal(
|
||||
decryptedContent, notification,
|
||||
); chk.E(err) {
|
||||
log.E.F(
|
||||
"Failed to parse notification: %v\n", err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
// Check if notification type is requested
|
||||
if len(notificationTypes) > 0 {
|
||||
found := false
|
||||
for _, t := range notificationTypes {
|
||||
if notification.NotificationType == t {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Handle notification
|
||||
handler(notification)
|
||||
case <-sub.EndOfStoredEvents:
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}(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:
|
||||
var plain []byte
|
||||
if plain, err = encryption.Decrypt(
|
||||
e.Event.Content, cl.conversationKey,
|
||||
); chk.E(err) {
|
||||
return
|
||||
}
|
||||
}()
|
||||
resp := &Response{
|
||||
Result: &result,
|
||||
}
|
||||
if err = json.Unmarshal(plain, resp); chk.E(err) {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// executeRequest executes a NIP-47 request
|
||||
func (c *Client) executeRequest(
|
||||
method Method,
|
||||
params any,
|
||||
) (msg json.RawMessage, err error) {
|
||||
// Default timeout values
|
||||
replyTimeout := 3 * time.Second
|
||||
publishTimeout := 3 * time.Second
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.Timeout(context.Bg(), replyTimeout)
|
||||
defer cancel()
|
||||
// Create result channel
|
||||
resultCh := make(chan json.RawMessage, 1)
|
||||
errCh := make(chan error, 1)
|
||||
// Check connection
|
||||
if err = c.checkConnected(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Create request
|
||||
request := struct {
|
||||
Method Method `json:"method"`
|
||||
Params any `json:"params,omitempty"`
|
||||
}{
|
||||
Method: method,
|
||||
Params: params,
|
||||
}
|
||||
// Marshal request
|
||||
requestJSON, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
// Encrypt request
|
||||
var encryptedContent []byte
|
||||
if encryptedContent, err = c.encrypt(
|
||||
c.options.WalletPubkey, requestJSON,
|
||||
); chk.E(err) {
|
||||
return nil, fmt.Errorf("failed to encrypt request: %w", err)
|
||||
}
|
||||
// Create request event
|
||||
requestEvent := &event.E{
|
||||
Kind: kind.WalletRequest,
|
||||
CreatedAt: timestamp.New(time.Now().Unix()),
|
||||
Tags: tags.New(tag.New("p", hex.Enc(c.options.WalletPubkey))),
|
||||
Content: encryptedContent,
|
||||
}
|
||||
// Sign request event
|
||||
err = requestEvent.Sign(c.options.Secret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign request event: %w", err)
|
||||
}
|
||||
// Subscribe to response events
|
||||
f := &filter.F{
|
||||
Kinds: kinds.New(kind.WalletResponse),
|
||||
Authors: tag.New(c.options.WalletPubkey),
|
||||
Tags: tags.New(tag.New([]byte("#p"), requestEvent.ID)),
|
||||
}
|
||||
log.I.F("%s", f.Marshal(nil))
|
||||
var sub *ws.Subscription
|
||||
if sub, err = c.relay.Subscribe(
|
||||
ctx, &filters.T{
|
||||
F: []*filter.F{f},
|
||||
},
|
||||
); chk.E(err) {
|
||||
err = fmt.Errorf(
|
||||
"failed to subscribe to response events: %w", err,
|
||||
)
|
||||
return
|
||||
}
|
||||
defer sub.Close()
|
||||
// Set up reply timeout
|
||||
replyTimer := time.AfterFunc(
|
||||
replyTimeout, func() {
|
||||
errCh <- NewReplyTimeoutError(
|
||||
fmt.Sprintf("Timeout waiting for reply to %s", method),
|
||||
"TIMEOUT",
|
||||
)
|
||||
func (cl *Client) GetWalletServiceInfo(c context.T) (
|
||||
wsi *WalletServiceInfo, 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),
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
defer replyTimer.Stop()
|
||||
// Handle response events
|
||||
go func() {
|
||||
var resErr error
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case ev := <-sub.Events:
|
||||
// Decrypt content
|
||||
var decryptedContent []byte
|
||||
decryptedContent, resErr = c.decrypt(
|
||||
c.options.WalletPubkey, ev.Content,
|
||||
)
|
||||
if chk.E(resErr) {
|
||||
errCh <- fmt.Errorf(
|
||||
"failed to decrypt response: %w",
|
||||
resErr,
|
||||
)
|
||||
return
|
||||
}
|
||||
// Parse response
|
||||
var response struct {
|
||||
ResultType string `json:"result_type"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
Error *struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
if resErr = json.Unmarshal(
|
||||
decryptedContent, &response,
|
||||
); chk.E(resErr) {
|
||||
errCh <- fmt.Errorf("failed to parse response: %w", resErr)
|
||||
return
|
||||
}
|
||||
// Check for error
|
||||
if response.Error != nil {
|
||||
errCh <- NewWalletError(
|
||||
response.Error.Message,
|
||||
response.Error.Code,
|
||||
)
|
||||
return
|
||||
}
|
||||
// Send result
|
||||
resultCh <- response.Result
|
||||
return
|
||||
case <-sub.EndOfStoredEvents:
|
||||
// Ignore
|
||||
select {
|
||||
case <-c.Done():
|
||||
err = fmt.Errorf("GetWalletServiceInfo canceled")
|
||||
return
|
||||
case ev := <-evc:
|
||||
var encryptionTypes []EncryptionType
|
||||
var notificationTypes []NotificationType
|
||||
encryptionTag := ev.Event.Tags.GetFirst(tag.New("encryption"))
|
||||
notificationsTag := ev.Event.Tags.GetFirst(tag.New("notifications"))
|
||||
if encryptionTag != nil {
|
||||
et := encryptionTag.ToSliceOfBytes()
|
||||
encType := bytes.Split(et[0], []byte(" "))
|
||||
for _, e := range encType {
|
||||
encryptionTypes = append(encryptionTypes, e)
|
||||
}
|
||||
}
|
||||
}()
|
||||
// Publish request event
|
||||
publishCtx, publishCancel := context.Timeout(
|
||||
context.Bg(), publishTimeout,
|
||||
)
|
||||
defer publishCancel()
|
||||
if err = c.relay.Publish(publishCtx, requestEvent); chk.E(err) {
|
||||
err = fmt.Errorf("failed to publish request event: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for result or error
|
||||
select {
|
||||
case msg = <-resultCh:
|
||||
return
|
||||
case err = <-errCh:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
err = NewReplyTimeoutError(
|
||||
fmt.Sprintf("Timeout waiting for reply to %s", method),
|
||||
"TIMEOUT",
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// checkConnected checks if the client is connected to the relay
|
||||
func (c *Client) checkConnected() (err error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.options.RelayURL == "" {
|
||||
return fmt.Errorf("missing relay URL")
|
||||
}
|
||||
|
||||
if c.relay == nil {
|
||||
if c.relay, err = ws.RelayConnect(
|
||||
context.Bg(), c.options.RelayURL,
|
||||
); chk.E(err) {
|
||||
return NewNetworkError(
|
||||
"Failed to connect to "+c.options.RelayURL,
|
||||
"OTHER",
|
||||
)
|
||||
if notificationsTag != nil {
|
||||
nt := notificationsTag.ToSliceOfBytes()
|
||||
notifs := bytes.Split(nt[0], []byte(" "))
|
||||
for _, e := range notifs {
|
||||
notificationTypes = append(notificationTypes, e)
|
||||
}
|
||||
}
|
||||
} else if !c.relay.IsConnected() {
|
||||
c.relay.Close()
|
||||
if c.relay, err = ws.RelayConnect(
|
||||
context.Bg(), c.options.RelayURL,
|
||||
); chk.E(err) {
|
||||
return NewNetworkError(
|
||||
"Failed to connect to "+c.options.RelayURL,
|
||||
"OTHER",
|
||||
)
|
||||
cp := bytes.Split(ev.Event.Content, []byte(" "))
|
||||
var capabilities []Capability
|
||||
for _, capability := range cp {
|
||||
capabilities = append(capabilities, capability)
|
||||
}
|
||||
wsi = &WalletServiceInfo{
|
||||
EncryptionTypes: encryptionTypes,
|
||||
NotificationTypes: notificationTypes,
|
||||
Capabilities: capabilities,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
func (cl *Client) GetInfo(c context.T) (gi *GetInfoResult, err error) {
|
||||
gi = &GetInfoResult{}
|
||||
if err = cl.RPC(c, GetInfo, nil, gi, nil); chk.E(err) {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (cl *Client) MakeInvoice(
|
||||
c context.T, params *MakeInvoiceParams,
|
||||
) (mi *MakeInvoiceResult, err error) {
|
||||
mi = &MakeInvoiceResult{}
|
||||
if err = cl.RPC(c, MakeInvoice, params, &mi, nil); chk.E(err) {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (cl *Client) PayInvoice(
|
||||
c context.T, params *PayInvoiceParams,
|
||||
) (pi *PayInvoiceResult, err error) {
|
||||
pi = &PayInvoiceResult{}
|
||||
if err = cl.RPC(c, PayInvoice, params, &pi, nil); chk.E(err) {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (cl *Client) LookupInvoice(
|
||||
c context.T, params *LookupInvoiceParams,
|
||||
) (li *LookupInvoiceResult, err error) {
|
||||
li = &LookupInvoiceResult{}
|
||||
if err = cl.RPC(c, LookupInvoice, params, &li, nil); chk.E(err) {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (cl *Client) ListTransactions(
|
||||
c context.T, params *ListTransactionsParams,
|
||||
) (lt *ListTransactionsResult, err error) {
|
||||
lt = &ListTransactionsResult{}
|
||||
if err = cl.RPC(c, ListTransactions, params, <, nil); chk.E(err) {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (cl *Client) GetBalance(c context.T) (gb *GetBalanceResult, err error) {
|
||||
gb = &GetBalanceResult{}
|
||||
if err = cl.RPC(c, GetBalance, nil, gb, nil); chk.E(err) {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,473 +1,116 @@
|
||||
package nwc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
// Capability represents a NIP-47 method
|
||||
type Capability []byte
|
||||
|
||||
var (
|
||||
GetInfo = Capability("get_info")
|
||||
GetBalance = Capability("get_balance")
|
||||
GetBudget = Capability("get_budget")
|
||||
MakeInvoice = Capability("make_invoice")
|
||||
PayInvoice = Capability("pay_invoice")
|
||||
PayKeysend = Capability("pay_keysend")
|
||||
LookupInvoice = Capability("lookup_invoice")
|
||||
ListTransactions = Capability("list_transactions")
|
||||
SignMessage = Capability("sign_message")
|
||||
CreateConnection = Capability("create_connection")
|
||||
MakeHoldInvoice = Capability("make_hold_invoice")
|
||||
SettleHoldInvoice = Capability("settle_hold_invoice")
|
||||
CancelHoldInvoice = Capability("cancel_hold_invoice")
|
||||
MultiPayInvoice = Capability("multi_pay_invoice")
|
||||
MultiPayKeysend = Capability("multi_pay_keysend")
|
||||
)
|
||||
|
||||
// EncryptionType represents the encryption type used for NIP-47 messages
|
||||
type EncryptionType string
|
||||
type EncryptionType []byte
|
||||
|
||||
const (
|
||||
Nip04 EncryptionType = "nip04"
|
||||
Nip44V2 EncryptionType = "nip44_v2"
|
||||
var (
|
||||
Nip04 = EncryptionType("nip04")
|
||||
Nip44V2 = EncryptionType("nip44_v2")
|
||||
)
|
||||
|
||||
// AuthorizationUrlOptions represents options for creating an NWC authorization URL
|
||||
type AuthorizationUrlOptions struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
RequestMethods []Method `json:"requestMethods,omitempty"`
|
||||
NotificationTypes []NotificationType `json:"notificationTypes,omitempty"`
|
||||
ReturnTo string `json:"returnTo,omitempty"`
|
||||
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
|
||||
MaxAmount *int64 `json:"maxAmount,omitempty"`
|
||||
BudgetRenewal BudgetRenewalPeriod `json:"budgetRenewal,omitempty"`
|
||||
Isolated bool `json:"isolated,omitempty"`
|
||||
Metadata interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
type NotificationType []byte
|
||||
|
||||
// Err is the base error type for NIP-47 errors
|
||||
type Err struct {
|
||||
Message string
|
||||
Code string
|
||||
}
|
||||
|
||||
func (e *Err) Error() string {
|
||||
return fmt.Sprintf("%s (code: %s)", e.Message, e.Code)
|
||||
}
|
||||
|
||||
// NewError creates a new Error
|
||||
func NewError(message, code string) *Err {
|
||||
return &Err{
|
||||
Message: message,
|
||||
Code: code,
|
||||
}
|
||||
}
|
||||
|
||||
// NetworkError represents a network error in NIP-47 operations
|
||||
type NetworkError struct{ *Err }
|
||||
|
||||
// NewNetworkError creates a new NetworkError
|
||||
func NewNetworkError(message, code string) *NetworkError {
|
||||
return &NetworkError{
|
||||
Err: NewError(message, code),
|
||||
}
|
||||
}
|
||||
|
||||
// WalletError represents a wallet error in NIP-47 operations
|
||||
type WalletError struct {
|
||||
*Err
|
||||
}
|
||||
|
||||
// NewWalletError creates a new WalletError
|
||||
func NewWalletError(message, code string) *WalletError {
|
||||
return &WalletError{
|
||||
Err: NewError(message, code),
|
||||
}
|
||||
}
|
||||
|
||||
// TimeoutError represents a timeout error in NIP-47 operations
|
||||
type TimeoutError struct{ *Err }
|
||||
|
||||
// NewTimeoutError creates a new TimeoutError
|
||||
func NewTimeoutError(message, code string) *TimeoutError {
|
||||
return &TimeoutError{
|
||||
Err: NewError(message, code),
|
||||
}
|
||||
}
|
||||
|
||||
// PublishTimeoutError represents a publish timeout error in NIP-47 operations
|
||||
type PublishTimeoutError struct{ *TimeoutError }
|
||||
|
||||
// NewPublishTimeoutError creates a new PublishTimeoutError
|
||||
func NewPublishTimeoutError(message, code string) *PublishTimeoutError {
|
||||
return &PublishTimeoutError{
|
||||
TimeoutError: NewTimeoutError(message, code),
|
||||
}
|
||||
}
|
||||
|
||||
// ReplyTimeoutError represents a reply timeout error in NIP-47 operations
|
||||
type ReplyTimeoutError struct{ *TimeoutError }
|
||||
|
||||
// NewReplyTimeoutError creates a new ReplyTimeoutError
|
||||
func NewReplyTimeoutError(message, code string) *ReplyTimeoutError {
|
||||
return &ReplyTimeoutError{
|
||||
TimeoutError: NewTimeoutError(message, code),
|
||||
}
|
||||
}
|
||||
|
||||
// PublishError represents a publish error in NIP-47 operations
|
||||
type PublishError struct{ *Err }
|
||||
|
||||
// NewPublishError creates a new PublishError
|
||||
func NewPublishError(message, code string) *PublishError {
|
||||
return &PublishError{
|
||||
Err: NewError(message, code),
|
||||
}
|
||||
}
|
||||
|
||||
// ResponseDecodingError represents a response decoding error in NIP-47 operations
|
||||
type ResponseDecodingError struct{ *Err }
|
||||
|
||||
// NewResponseDecodingError creates a new ResponseDecodingError
|
||||
func NewResponseDecodingError(message, code string) *ResponseDecodingError {
|
||||
return &ResponseDecodingError{
|
||||
Err: NewError(message, code),
|
||||
}
|
||||
}
|
||||
|
||||
// ResponseValidationError represents a response validation error in NIP-47 operations
|
||||
type ResponseValidationError struct{ *Err }
|
||||
|
||||
// NewResponseValidationError creates a new ResponseValidationError
|
||||
func NewResponseValidationError(message, code string) *ResponseValidationError {
|
||||
return &ResponseValidationError{
|
||||
Err: NewError(message, code),
|
||||
}
|
||||
}
|
||||
|
||||
// UnexpectedResponseError represents an unexpected response error in NIP-47 operations
|
||||
type UnexpectedResponseError struct{ *Err }
|
||||
|
||||
// NewUnexpectedResponseError creates a new UnexpectedResponseError
|
||||
func NewUnexpectedResponseError(message, code string) *UnexpectedResponseError {
|
||||
return &UnexpectedResponseError{
|
||||
Err: NewError(message, code),
|
||||
}
|
||||
}
|
||||
|
||||
// UnsupportedEncryptionError represents an unsupported encryption error in NIP-47 operations
|
||||
type UnsupportedEncryptionError struct {
|
||||
*Err
|
||||
}
|
||||
|
||||
// NewUnsupportedEncryptionError creates a new UnsupportedEncryptionError
|
||||
func NewUnsupportedEncryptionError(message, code string) *UnsupportedEncryptionError {
|
||||
return &UnsupportedEncryptionError{
|
||||
Err: NewError(message, code),
|
||||
}
|
||||
}
|
||||
|
||||
// WithDTag represents a type with a dTag field
|
||||
type WithDTag struct {
|
||||
DTag string `json:"dTag"`
|
||||
}
|
||||
|
||||
// WithOptionalId represents a type with an optional id field
|
||||
type WithOptionalId struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
// Method represents a NIP-47 method
|
||||
type Method string
|
||||
|
||||
// SingleMethod represents a single NIP-47 method
|
||||
const (
|
||||
GetInfo Method = "get_info"
|
||||
GetBalance Method = "get_balance"
|
||||
GetBudget Method = "get_budget"
|
||||
MakeInvoice Method = "make_invoice"
|
||||
PayInvoice Method = "pay_invoice"
|
||||
PayKeysend Method = "pay_keysend"
|
||||
LookupInvoice Method = "lookup_invoice"
|
||||
ListTransactions Method = "list_transactions"
|
||||
SignMessage Method = "sign_message"
|
||||
CreateConnection Method = "create_connection"
|
||||
MakeHoldInvoice Method = "make_hold_invoice"
|
||||
SettleHoldInvoice Method = "settle_hold_invoice"
|
||||
CancelHoldInvoice Method = "cancel_hold_invoice"
|
||||
var (
|
||||
PaymentReceived = NotificationType("payment_received")
|
||||
PaymentSent = NotificationType("payment_sent")
|
||||
)
|
||||
|
||||
// MultiMethod represents a multi NIP-47 method
|
||||
const (
|
||||
MultiPayInvoice Method = "multi_pay_invoice"
|
||||
MultiPayKeysend Method = "multi_pay_keysend"
|
||||
)
|
||||
|
||||
// Capability represents a NIP-47 capability
|
||||
type Capability string
|
||||
|
||||
const (
|
||||
Notifications Capability = "notifications"
|
||||
)
|
||||
|
||||
// BudgetRenewalPeriod represents a budget renewal period
|
||||
type BudgetRenewalPeriod string
|
||||
|
||||
const (
|
||||
Daily BudgetRenewalPeriod = "daily"
|
||||
Weekly BudgetRenewalPeriod = "weekly"
|
||||
Monthly BudgetRenewalPeriod = "monthly"
|
||||
Yearly BudgetRenewalPeriod = "yearly"
|
||||
Never BudgetRenewalPeriod = "never"
|
||||
)
|
||||
|
||||
// GetInfoResponse represents a response to a get_info request
|
||||
type GetInfoResponse struct {
|
||||
Alias string `json:"alias"`
|
||||
Color string `json:"color"`
|
||||
Pubkey string `json:"pubkey"`
|
||||
Network string `json:"network"`
|
||||
BlockHeight int64 `json:"block_height"`
|
||||
BlockHash string `json:"block_hash"`
|
||||
Methods []Method `json:"methods"`
|
||||
Notifications []NotificationType `json:"notifications,omitempty"`
|
||||
Metadata interface{} `json:"metadata,omitempty"`
|
||||
Lud16 string `json:"lud16,omitempty"`
|
||||
type WalletServiceInfo struct {
|
||||
EncryptionTypes []EncryptionType
|
||||
Capabilities []Capability
|
||||
NotificationTypes []NotificationType
|
||||
}
|
||||
|
||||
// GetBudgetResponse represents a response to a get_budget request
|
||||
type GetBudgetResponse struct {
|
||||
UsedBudget int64 `json:"used_budget,omitempty"`
|
||||
TotalBudget int64 `json:"total_budget,omitempty"`
|
||||
RenewsAt *int64 `json:"renews_at,omitempty"`
|
||||
RenewalPeriod BudgetRenewalPeriod `json:"renewal_period,omitempty"`
|
||||
type GetInfoResult struct {
|
||||
Alias string `json:"alias"`
|
||||
Color string `json:"color"`
|
||||
Pubkey string `json:"pubkey"`
|
||||
Network string `json:"network"`
|
||||
BlockHeight uint `json:"block_height"`
|
||||
BlockHash string `json:"block_hash"`
|
||||
Methods []string `json:"methods"`
|
||||
Notifications []string `json:"notifications"`
|
||||
}
|
||||
|
||||
// GetBalanceResponse represents a response to a get_balance request
|
||||
type GetBalanceResponse struct {
|
||||
Balance int64 `json:"balance"` // msats
|
||||
type MakeInvoiceParams struct {
|
||||
Amount uint64 `json:"amount"`
|
||||
Expiry *uint32 `json:"expiry"`
|
||||
Description string `json:"description"`
|
||||
DescriptionHash string `json:"description_hash"`
|
||||
Metadata any `json:"metadata"`
|
||||
}
|
||||
|
||||
// PayResponse represents a response to a pay request
|
||||
type PayResponse struct {
|
||||
type PayInvoiceParams struct {
|
||||
Invoice string `json:"invoice"`
|
||||
Amount *uint64 `json:"amount"`
|
||||
Metadata any `json:"metadata"`
|
||||
}
|
||||
|
||||
type LookupInvoiceParams struct {
|
||||
PaymentHash string `json:"payment_hash"`
|
||||
Invoice string `json:"invoice"`
|
||||
}
|
||||
|
||||
type ListTransactionsParams struct {
|
||||
From uint64 `json:"from"`
|
||||
To uint64 `json:"to"`
|
||||
Limit uint16 `json:"limit"`
|
||||
Offset uint32 `json:"offset"`
|
||||
Unpaid bool `json:"unpaid"`
|
||||
UnpaidOutgoing bool `json:"unpaid_outgoing"`
|
||||
UnpaidIncoming bool `json:"unpaid_incoming"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type GetBalanceResult struct {
|
||||
Balance uint64 `json:"balance"`
|
||||
}
|
||||
|
||||
type PayInvoiceResult struct {
|
||||
Preimage string `json:"preimage"`
|
||||
FeesPaid int64 `json:"fees_paid"`
|
||||
FeesPaid uint64 `json:"fees_paid"`
|
||||
}
|
||||
|
||||
// MultiPayInvoiceRequest represents a request to pay multiple invoices
|
||||
type MultiPayInvoiceRequest struct {
|
||||
Invoices []PayInvoiceRequestWithID `json:"invoices"`
|
||||
}
|
||||
|
||||
// PayInvoiceRequestWithID combines PayInvoiceRequest with WithOptionalId
|
||||
type PayInvoiceRequestWithID struct {
|
||||
PayInvoiceRequest
|
||||
WithOptionalId
|
||||
}
|
||||
|
||||
// MultiPayKeysendRequest represents a request to pay multiple keysends
|
||||
type MultiPayKeysendRequest struct {
|
||||
Keysends []PayKeysendRequestWithID `json:"keysends"`
|
||||
}
|
||||
|
||||
// PayKeysendRequestWithID combines PayKeysendRequest with WithOptionalId
|
||||
type PayKeysendRequestWithID struct {
|
||||
PayKeysendRequest
|
||||
WithOptionalId
|
||||
}
|
||||
|
||||
// MultiPayInvoiceResponse represents a response to a multi_pay_invoice request
|
||||
type MultiPayInvoiceResponse struct {
|
||||
Invoices []MultiPayInvoiceResponseItem `json:"invoices"`
|
||||
Errors []interface{} `json:"errors"` // TODO: add error handling
|
||||
}
|
||||
|
||||
// MultiPayInvoiceResponseItem represents an item in a multi_pay_invoice response
|
||||
type MultiPayInvoiceResponseItem struct {
|
||||
Invoice PayInvoiceRequest `json:"invoice"`
|
||||
PayResponse
|
||||
WithDTag
|
||||
}
|
||||
|
||||
// MultiPayKeysendResponse represents a response to a multi_pay_keysend request
|
||||
type MultiPayKeysendResponse struct {
|
||||
Keysends []MultiPayKeysendResponseItem `json:"keysends"`
|
||||
Errors []interface{} `json:"errors"` // TODO: add error handling
|
||||
}
|
||||
|
||||
// MultiPayKeysendResponseItem represents an item in a multi_pay_keysend response
|
||||
type MultiPayKeysendResponseItem struct {
|
||||
Keysend PayKeysendRequest `json:"keysend"`
|
||||
PayResponse
|
||||
WithDTag
|
||||
}
|
||||
|
||||
// ListTransactionsRequest represents a request to list transactions
|
||||
type ListTransactionsRequest struct {
|
||||
From *int64 `json:"from,omitempty"`
|
||||
Until *int64 `json:"until,omitempty"`
|
||||
Limit *int64 `json:"limit,omitempty"`
|
||||
Offset *int64 `json:"offset,omitempty"`
|
||||
Unpaid *bool `json:"unpaid,omitempty"`
|
||||
UnpaidOutgoing *bool `json:"unpaid_outgoing,omitempty"` // NOTE: non-NIP-47 spec compliant
|
||||
UnpaidIncoming *bool `json:"unpaid_incoming,omitempty"` // NOTE: non-NIP-47 spec compliant
|
||||
Type *string `json:"type,omitempty"` // "incoming" or "outgoing"
|
||||
}
|
||||
|
||||
// ListTransactionsResponse represents a response to a list_transactions request
|
||||
type ListTransactionsResponse struct {
|
||||
type MakeInvoiceResult = Transaction
|
||||
type LookupInvoiceResult = Transaction
|
||||
type ListTransactionsResult struct {
|
||||
Transactions []Transaction `json:"transactions"`
|
||||
TotalCount int64 `json:"total_count"` // NOTE: non-NIP-47 spec compliant
|
||||
TotalCount uint32 `json:"total_count"`
|
||||
}
|
||||
|
||||
// TransactionType represents the type of a transaction
|
||||
type TransactionType string
|
||||
|
||||
const (
|
||||
Incoming TransactionType = "incoming"
|
||||
Outgoing TransactionType = "outgoing"
|
||||
)
|
||||
|
||||
// TransactionState represents the state of a transaction
|
||||
type TransactionState string
|
||||
|
||||
const (
|
||||
Settled TransactionState = "settled"
|
||||
Pending TransactionState = "pending"
|
||||
Failed TransactionState = "failed"
|
||||
)
|
||||
|
||||
// Transaction represents a transaction
|
||||
type Transaction struct {
|
||||
Type TransactionType `json:"type"`
|
||||
State TransactionState `json:"state"` // NOTE: non-NIP-47 spec compliant
|
||||
Invoice string `json:"invoice"`
|
||||
Description string `json:"description"`
|
||||
DescriptionHash string `json:"description_hash"`
|
||||
Preimage string `json:"preimage"`
|
||||
PaymentHash string `json:"payment_hash"`
|
||||
Amount int64 `json:"amount"`
|
||||
FeesPaid int64 `json:"fees_paid"`
|
||||
SettledAt int64 `json:"settled_at"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
SettleDeadline *int64 `json:"settle_deadline,omitempty"` // NOTE: non-NIP-47 spec compliant
|
||||
Metadata *TransactionMetadata `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// TransactionMetadata represents metadata for a transaction
|
||||
type TransactionMetadata struct {
|
||||
Comment string `json:"comment,omitempty"` // LUD-12
|
||||
PayerData *PayerData `json:"payer_data,omitempty"` // LUD-18
|
||||
RecipientData *RecipientData `json:"recipient_data,omitempty"` // LUD-18
|
||||
Nostr *NostrData `json:"nostr,omitempty"` // NIP-57
|
||||
ExtraData map[string]interface{} `json:"-"` // For additional fields
|
||||
}
|
||||
|
||||
// PayerData represents payer data for a transaction
|
||||
type PayerData struct {
|
||||
Email string `json:"email,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Pubkey string `json:"pubkey,omitempty"`
|
||||
}
|
||||
|
||||
// RecipientData represents recipient data for a transaction
|
||||
type RecipientData struct {
|
||||
Identifier string `json:"identifier,omitempty"`
|
||||
}
|
||||
|
||||
// NostrData represents Nostr data for a transaction
|
||||
type NostrData struct {
|
||||
Pubkey string `json:"pubkey"`
|
||||
Tags [][]string `json:"tags"`
|
||||
}
|
||||
|
||||
// NotificationType represents a notification type
|
||||
type NotificationType string
|
||||
|
||||
const (
|
||||
PaymentReceived NotificationType = "payment_received"
|
||||
PaymentSent NotificationType = "payment_sent"
|
||||
HoldInvoiceAccepted NotificationType = "hold_invoice_accepted"
|
||||
)
|
||||
|
||||
// Notification represents a notification
|
||||
type Notification struct {
|
||||
NotificationType NotificationType `json:"notification_type"`
|
||||
Notification Transaction `json:"notification"`
|
||||
}
|
||||
|
||||
// PayInvoiceRequest represents a request to pay an invoice
|
||||
type PayInvoiceRequest struct {
|
||||
Invoice string `json:"invoice"`
|
||||
Metadata *TransactionMetadata `json:"metadata,omitempty"`
|
||||
Amount *int64 `json:"amount,omitempty"` // msats
|
||||
}
|
||||
|
||||
// PayKeysendRequest represents a request to pay a keysend
|
||||
type PayKeysendRequest struct {
|
||||
Amount int64 `json:"amount"` // msats
|
||||
Pubkey string `json:"pubkey"`
|
||||
Preimage string `json:"preimage,omitempty"`
|
||||
TlvRecords []TlvRecord `json:"tlv_records,omitempty"`
|
||||
}
|
||||
|
||||
// TlvRecord represents a TLV record
|
||||
type TlvRecord struct {
|
||||
Type int64 `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// MakeInvoiceRequest represents a request to make an invoice
|
||||
type MakeInvoiceRequest struct {
|
||||
Amount int64 `json:"amount"` // msats
|
||||
Description string `json:"description,omitempty"`
|
||||
DescriptionHash string `json:"description_hash,omitempty"`
|
||||
Expiry *int64 `json:"expiry,omitempty"` // in seconds
|
||||
Metadata *TransactionMetadata `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// MakeHoldInvoiceRequest represents a request to make a hold invoice
|
||||
type MakeHoldInvoiceRequest struct {
|
||||
MakeInvoiceRequest
|
||||
PaymentHash string `json:"payment_hash"`
|
||||
}
|
||||
|
||||
// SettleHoldInvoiceRequest represents a request to settle a hold invoice
|
||||
type SettleHoldInvoiceRequest struct {
|
||||
Preimage string `json:"preimage"`
|
||||
}
|
||||
|
||||
// SettleHoldInvoiceResponse represents a response to a settle_hold_invoice request
|
||||
type SettleHoldInvoiceResponse struct{}
|
||||
|
||||
// CancelHoldInvoiceRequest represents a request to cancel a hold invoice
|
||||
type CancelHoldInvoiceRequest struct {
|
||||
PaymentHash string `json:"payment_hash"`
|
||||
}
|
||||
|
||||
// CancelHoldInvoiceResponse represents a response to a cancel_hold_invoice request
|
||||
type CancelHoldInvoiceResponse struct{}
|
||||
|
||||
// LookupInvoiceRequest represents a request to lookup an invoice
|
||||
type LookupInvoiceRequest struct {
|
||||
PaymentHash string `json:"payment_hash,omitempty"`
|
||||
Invoice string `json:"invoice,omitempty"`
|
||||
}
|
||||
|
||||
// SignMessageRequest represents a request to sign a message
|
||||
type SignMessageRequest struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// CreateConnectionRequest represents a request to create a connection
|
||||
type CreateConnectionRequest struct {
|
||||
Pubkey string `json:"pubkey"`
|
||||
Name string `json:"name"`
|
||||
RequestMethods []Method `json:"request_methods"`
|
||||
NotificationTypes []NotificationType `json:"notification_types,omitempty"`
|
||||
MaxAmount *int64 `json:"max_amount,omitempty"`
|
||||
BudgetRenewal *BudgetRenewalPeriod `json:"budget_renewal,omitempty"`
|
||||
ExpiresAt *int64 `json:"expires_at,omitempty"`
|
||||
Isolated *bool `json:"isolated,omitempty"`
|
||||
Metadata any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// CreateConnectionResponse represents a response to a create_connection request
|
||||
type CreateConnectionResponse struct {
|
||||
WalletPubkey string `json:"wallet_pubkey"`
|
||||
}
|
||||
|
||||
// SignMessageResponse represents a response to a sign_message request
|
||||
type SignMessageResponse struct {
|
||||
Message string `json:"message"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
// TimeoutValues represents timeout values for NIP-47 requests
|
||||
type TimeoutValues struct {
|
||||
ReplyTimeout *int64 `json:"replyTimeout,omitempty"`
|
||||
PublishTimeout *int64 `json:"publishTimeout,omitempty"`
|
||||
Type string `json:"type"`
|
||||
State string `json:"state"`
|
||||
Invoice string `json:"invoice"`
|
||||
Description string `json:"description"`
|
||||
DescriptionHash string `json:"description_hash"`
|
||||
Preimage string `json:"preimage"`
|
||||
PaymentHash string `json:"payment_hash"`
|
||||
Amount uint64 `json:"amount"`
|
||||
FeesPaid uint64 `json:"fees_paid"`
|
||||
CreatedAt uint64 `json:"created_at"`
|
||||
ExpiresAt uint64 `json:"expires_at"`
|
||||
SettledAt *uint64 `json:"settled_at"`
|
||||
Metadata any `json:"metadata"`
|
||||
}
|
||||
|
||||
49
pkg/protocol/nwc/uri.go
Normal file
49
pkg/protocol/nwc/uri.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package nwc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"orly.dev/pkg/crypto/p256k"
|
||||
"orly.dev/pkg/utils/chk"
|
||||
)
|
||||
|
||||
type ConnectionParams struct {
|
||||
clientSecretKey []byte
|
||||
walletPublicKey []byte
|
||||
relays []string
|
||||
}
|
||||
|
||||
func ParseConnectionURI(nwcUri string) (parts *ConnectionParams, err error) {
|
||||
var p *url.URL
|
||||
if p, err = url.Parse(nwcUri); chk.E(err) {
|
||||
return
|
||||
}
|
||||
parts = &ConnectionParams{}
|
||||
if p.Scheme != "nostr+walletconnect" {
|
||||
err = errors.New("incorrect scheme")
|
||||
return
|
||||
}
|
||||
if parts.walletPublicKey, err = p256k.HexToBin(p.Host); chk.E(err) {
|
||||
err = errors.New("invalid public key")
|
||||
return
|
||||
}
|
||||
query := p.Query()
|
||||
var ok bool
|
||||
if parts.relays, ok = query["relay"]; !ok {
|
||||
err = errors.New("missing relay parameter")
|
||||
return
|
||||
}
|
||||
if len(parts.relays) == 0 {
|
||||
return nil, errors.New("no relays")
|
||||
}
|
||||
var secret string
|
||||
if secret = query.Get("secret"); secret == "" {
|
||||
err = errors.New("missing secret parameter")
|
||||
return
|
||||
}
|
||||
if parts.clientSecretKey, err = p256k.HexToBin(secret); chk.E(err) {
|
||||
err = errors.New("invalid secret")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"orly.dev/pkg/utils/chk"
|
||||
"orly.dev/pkg/utils/context"
|
||||
"orly.dev/pkg/utils/errorf"
|
||||
"orly.dev/pkg/utils/log"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -178,7 +179,7 @@ func (sub *Subscription) Fire() (err error) {
|
||||
} else {
|
||||
b = countenvelope.NewRequest(id, sub.Filters).Marshal(b)
|
||||
}
|
||||
// log.T.F("{%s} sending %s", sub.Relay.URL, b)
|
||||
log.T.F("{%s} sending %s", sub.Relay.URL, b)
|
||||
sub.live.Store(true)
|
||||
if err = <-sub.Relay.Write(b); chk.T(err) {
|
||||
sub.cancel()
|
||||
|
||||
@@ -5,8 +5,11 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// PointerToValue is a generic interface to refer to any pointer to almost any kind of common
|
||||
// type of value.
|
||||
// PointerToValue is a generic interface (type constraint) to refer to any
|
||||
// pointer to almost any kind of common type of value.
|
||||
//
|
||||
// see the utils/values package for a set of methods to accept these values and
|
||||
// return the correct type pointer to them.
|
||||
type PointerToValue interface {
|
||||
~*uint | ~*int | ~*uint8 | ~*uint16 | ~*uint32 | ~*uint64 | ~*int8 | ~*int16 | ~*int32 |
|
||||
~*int64 | ~*float32 | ~*float64 | ~*string | ~*[]string | ~*time.Time | ~*time.Duration |
|
||||
|
||||
101
pkg/utils/values/values.go
Normal file
101
pkg/utils/values/values.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package values
|
||||
|
||||
import (
|
||||
"orly.dev/pkg/encoders/unix"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ToUintPointer returns a pointer to the uint value passed in.
|
||||
func ToUintPointer(v uint) *uint {
|
||||
return &v
|
||||
}
|
||||
|
||||
// ToIntPointer returns a pointer to the int value passed in.
|
||||
func ToIntPointer(v int) *int {
|
||||
return &v
|
||||
}
|
||||
|
||||
// ToUint8Pointer returns a pointer to the uint8 value passed in.
|
||||
func ToUint8Pointer(v uint8) *uint8 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// ToUint16Pointer returns a pointer to the uint16 value passed in.
|
||||
func ToUint16Pointer(v uint16) *uint16 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// ToUint32Pointer returns a pointer to the uint32 value passed in.
|
||||
func ToUint32Pointer(v uint32) *uint32 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// ToUint64Pointer returns a pointer to the uint64 value passed in.
|
||||
func ToUint64Pointer(v uint64) *uint64 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// ToInt8Pointer returns a pointer to the int8 value passed in.
|
||||
func ToInt8Pointer(v int8) *int8 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// ToInt16Pointer returns a pointer to the int16 value passed in.
|
||||
func ToInt16Pointer(v int16) *int16 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// ToInt32Pointer returns a pointer to the int32 value passed in.
|
||||
func ToInt32Pointer(v int32) *int32 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// ToInt64Pointer returns a pointer to the int64 value passed in.
|
||||
func ToInt64Pointer(v int64) *int64 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// ToFloat32Pointer returns a pointer to the float32 value passed in.
|
||||
func ToFloat32Pointer(v float32) *float32 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// ToFloat64Pointer returns a pointer to the float64 value passed in.
|
||||
func ToFloat64Pointer(v float64) *float64 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// ToStringPointer returns a pointer to the string value passed in.
|
||||
func ToStringPointer(v string) *string {
|
||||
return &v
|
||||
}
|
||||
|
||||
// ToStringSlicePointer returns a pointer to the []string value passed in.
|
||||
func ToStringSlicePointer(v []string) *[]string {
|
||||
return &v
|
||||
}
|
||||
|
||||
// ToTimePointer returns a pointer to the time.Time value passed in.
|
||||
func ToTimePointer(v time.Time) *time.Time {
|
||||
return &v
|
||||
}
|
||||
|
||||
// ToDurationPointer returns a pointer to the time.Duration value passed in.
|
||||
func ToDurationPointer(v time.Duration) *time.Duration {
|
||||
return &v
|
||||
}
|
||||
|
||||
// ToBytesPointer returns a pointer to the []byte value passed in.
|
||||
func ToBytesPointer(v []byte) *[]byte {
|
||||
return &v
|
||||
}
|
||||
|
||||
// ToByteSlicesPointer returns a pointer to the [][]byte value passed in.
|
||||
func ToByteSlicesPointer(v [][]byte) *[][]byte {
|
||||
return &v
|
||||
}
|
||||
|
||||
// ToUnixTimePointer returns a pointer to the unix.Time value passed in.
|
||||
func ToUnixTimePointer(v unix.Time) *unix.Time {
|
||||
return &v
|
||||
}
|
||||
Reference in New Issue
Block a user