Add wallet service implementation and mock CLI tool

- pkg/protocol/nwc/wallet.go
  - Implemented `WalletService` with method registration and request handling.
  - Added default stub handlers for supported wallet methods.
  - Included support for notifications with `SendNotification`.

- pkg/protocol/nwc/client-methods.go
  - Added `Subscribe` function for handling client subscriptions.

- cmd/walletcli/mock-wallet-service/main.go
  - Implemented a mock CLI tool for wallet service.
  - Added command-line flags for relay connection and key management.
  - Added handlers for various wallet service methods (e.g., `GetInfo`, `GetBalance`, etc.).

- pkg/protocol/nwc/types.go
  - Added `GetWalletServiceInfo` to the list of wallet service capabilities.
This commit is contained in:
2025-08-08 17:34:44 +01:00
parent bb8f070992
commit e94d68c3b2
8 changed files with 2010 additions and 189 deletions

View File

@@ -106,8 +106,59 @@ func handleGetWalletServiceInfo(c context.T, cl *nwc.Client) {
}
}
func handleGetInfo(c context.T, cl *nwc.Client) {
if _, raw, err := cl.GetInfo(c, true); !chk.E(err) {
func handleCancelHoldInvoice(c context.T, cl *nwc.Client, args []string) {
if len(args) < 1 {
fmt.Println("Error: Missing required arguments")
fmt.Println("Usage: walletcli <NWC connection URL> cancel_hold_invoice <payment_hash>")
return
}
params := &nwc.CancelHoldInvoiceParams{
PaymentHash: args[0],
}
var err error
var raw []byte
if raw, err = cl.CancelHoldInvoice(c, params, true); !chk.E(err) {
fmt.Println(string(raw))
}
}
func handleCreateConnection(c context.T, cl *nwc.Client, args []string) {
if len(args) < 3 {
fmt.Println("Error: Missing required arguments")
fmt.Println("Usage: walletcli <NWC connection URL> create_connection <pubkey> <name> <methods> [<notification_types>] [<max_amount>] [<budget_renewal>] [<expires_at>]")
return
}
params := &nwc.CreateConnectionParams{
Pubkey: args[0],
Name: args[1],
RequestMethods: strings.Split(args[2], ","),
}
if len(args) > 3 {
params.NotificationTypes = strings.Split(args[3], ",")
}
if len(args) > 4 {
maxAmount, err := strconv.ParseUint(args[4], 10, 64)
if err != nil {
fmt.Printf("Error parsing max_amount: %v\n", err)
return
}
params.MaxAmount = &maxAmount
}
if len(args) > 5 {
params.BudgetRenewal = &args[5]
}
if len(args) > 6 {
expiresAt, err := strconv.ParseInt(args[6], 10, 64)
if err != nil {
fmt.Printf("Error parsing expires_at: %v\n", err)
return
}
params.ExpiresAt = &expiresAt
}
var raw []byte
var err error
if raw, err = cl.CreateConnection(c, params, true); !chk.E(err) {
fmt.Println(string(raw))
}
}
@@ -124,86 +175,8 @@ func handleGetBudget(c context.T, cl *nwc.Client) {
}
}
func handleMakeInvoice(c context.T, cl *nwc.Client, args []string) {
if len(args) < 1 {
fmt.Println("Error: Missing required arguments")
fmt.Println("Usage: walletcli <NWC connection URL> make_invoice <amount> [<description>] [<description_hash>] [<expiry>]")
return
}
amount, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
fmt.Printf("Error parsing amount: %v\n", err)
return
}
params := &nwc.MakeInvoiceParams{
Amount: amount,
}
if len(args) > 1 {
params.Description = args[1]
}
if len(args) > 2 {
params.DescriptionHash = args[2]
}
if len(args) > 3 {
expiry, err := strconv.ParseInt(args[3], 10, 64)
if err != nil {
fmt.Printf("Error parsing expiry: %v\n", err)
return
}
params.Expiry = &expiry
}
var raw []byte
if _, raw, err = cl.MakeInvoice(c, params, true); !chk.E(err) {
fmt.Println(string(raw))
}
}
func handlePayInvoice(c context.T, cl *nwc.Client, args []string) {
if len(args) < 1 {
fmt.Println("Error: Missing required arguments")
fmt.Println("Usage: walletcli <NWC connection URL> pay_invoice <invoice> [<amount>] [<comment>]")
return
}
params := &nwc.PayInvoiceParams{
Invoice: args[0],
}
if len(args) > 1 {
amount, err := strconv.ParseUint(args[1], 10, 64)
if err != nil {
fmt.Printf("Error parsing amount: %v\n", err)
return
}
params.Amount = &amount
}
if len(args) > 2 {
comment := args[2]
params.Metadata = &nwc.PayInvoiceMetadata{
Comment: &comment,
}
}
if _, raw, err := cl.PayInvoice(c, params, true); !chk.E(err) {
fmt.Println(string(raw))
}
}
func handleLookupInvoice(c context.T, cl *nwc.Client, args []string) {
if len(args) < 1 {
fmt.Println("Error: Missing required arguments")
fmt.Println("Usage: walletcli <NWC connection URL> lookup_invoice <payment_hash or invoice>")
return
}
params := &nwc.LookupInvoiceParams{}
// Determine if the argument is a payment hash or an invoice
if strings.HasPrefix(args[0], "ln") {
invoice := args[0]
params.Invoice = &invoice
} else {
paymentHash := args[0]
params.PaymentHash = &paymentHash
}
var err error
var raw []byte
if _, raw, err = cl.LookupInvoice(c, params, true); !chk.E(err) {
func handleGetInfo(c context.T, cl *nwc.Client) {
if _, raw, err := cl.GetInfo(c, true); !chk.E(err) {
fmt.Println(string(raw))
}
}
@@ -251,6 +224,28 @@ func handleListTransactions(c context.T, cl *nwc.Client, args []string) {
}
}
func handleLookupInvoice(c context.T, cl *nwc.Client, args []string) {
if len(args) < 1 {
fmt.Println("Error: Missing required arguments")
fmt.Println("Usage: walletcli <NWC connection URL> lookup_invoice <payment_hash or invoice>")
return
}
params := &nwc.LookupInvoiceParams{}
// Determine if the argument is a payment hash or an invoice
if strings.HasPrefix(args[0], "ln") {
invoice := args[0]
params.Invoice = &invoice
} else {
paymentHash := args[0]
params.PaymentHash = &paymentHash
}
var err error
var raw []byte
if _, raw, err = cl.LookupInvoice(c, params, true); !chk.E(err) {
fmt.Println(string(raw))
}
}
func handleMakeHoldInvoice(c context.T, cl *nwc.Client, args []string) {
if len(args) < 2 {
fmt.Println("Error: Missing required arguments")
@@ -286,52 +281,36 @@ func handleMakeHoldInvoice(c context.T, cl *nwc.Client, args []string) {
}
}
func handleSettleHoldInvoice(c context.T, cl *nwc.Client, args []string) {
func handleMakeInvoice(c context.T, cl *nwc.Client, args []string) {
if len(args) < 1 {
fmt.Println("Error: Missing required arguments")
fmt.Println("Usage: walletcli <NWC connection URL> settle_hold_invoice <preimage>")
fmt.Println("Usage: walletcli <NWC connection URL> make_invoice <amount> [<description>] [<description_hash>] [<expiry>]")
return
}
params := &nwc.SettleHoldInvoiceParams{
Preimage: args[0],
}
var raw []byte
var err error
if raw, err = cl.SettleHoldInvoice(c, params, true); !chk.E(err) {
fmt.Println(string(raw))
}
}
func handleCancelHoldInvoice(c context.T, cl *nwc.Client, args []string) {
if len(args) < 1 {
fmt.Println("Error: Missing required arguments")
fmt.Println("Usage: walletcli <NWC connection URL> cancel_hold_invoice <payment_hash>")
amount, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
fmt.Printf("Error parsing amount: %v\n", err)
return
}
params := &nwc.CancelHoldInvoiceParams{
PaymentHash: args[0],
params := &nwc.MakeInvoiceParams{
Amount: amount,
}
var err error
var raw []byte
if raw, err = cl.CancelHoldInvoice(c, params, true); !chk.E(err) {
fmt.Println(string(raw))
if len(args) > 1 {
params.Description = args[1]
}
}
func handleSignMessage(c context.T, cl *nwc.Client, args []string) {
if len(args) < 1 {
fmt.Println("Error: Missing required arguments")
fmt.Println("Usage: walletcli <NWC connection URL> sign_message <message>")
return
if len(args) > 2 {
params.DescriptionHash = args[2]
}
params := &nwc.SignMessageParams{
Message: args[0],
if len(args) > 3 {
expiry, err := strconv.ParseInt(args[3], 10, 64)
if err != nil {
fmt.Printf("Error parsing expiry: %v\n", err)
return
}
params.Expiry = &expiry
}
var raw []byte
var err error
if _, raw, err = cl.SignMessage(c, params, true); !chk.E(err) {
if _, raw, err = cl.MakeInvoice(c, params, true); !chk.E(err) {
fmt.Println(string(raw))
}
}
@@ -381,42 +360,63 @@ func handlePayKeysend(c context.T, cl *nwc.Client, args []string) {
}
}
func handleCreateConnection(c context.T, cl *nwc.Client, args []string) {
if len(args) < 3 {
func handlePayInvoice(c context.T, cl *nwc.Client, args []string) {
if len(args) < 1 {
fmt.Println("Error: Missing required arguments")
fmt.Println("Usage: walletcli <NWC connection URL> create_connection <pubkey> <name> <methods> [<notification_types>] [<max_amount>] [<budget_renewal>] [<expires_at>]")
fmt.Println("Usage: walletcli <NWC connection URL> pay_invoice <invoice> [<amount>] [<comment>]")
return
}
params := &nwc.CreateConnectionParams{
Pubkey: args[0],
Name: args[1],
RequestMethods: strings.Split(args[2], ","),
params := &nwc.PayInvoiceParams{
Invoice: args[0],
}
if len(args) > 3 {
params.NotificationTypes = strings.Split(args[3], ",")
}
if len(args) > 4 {
maxAmount, err := strconv.ParseUint(args[4], 10, 64)
if len(args) > 1 {
amount, err := strconv.ParseUint(args[1], 10, 64)
if err != nil {
fmt.Printf("Error parsing max_amount: %v\n", err)
fmt.Printf("Error parsing amount: %v\n", err)
return
}
params.MaxAmount = &maxAmount
params.Amount = &amount
}
if len(args) > 5 {
params.BudgetRenewal = &args[5]
}
if len(args) > 6 {
expiresAt, err := strconv.ParseInt(args[6], 10, 64)
if err != nil {
fmt.Printf("Error parsing expires_at: %v\n", err)
return
if len(args) > 2 {
comment := args[2]
params.Metadata = &nwc.PayInvoiceMetadata{
Comment: &comment,
}
params.ExpiresAt = &expiresAt
}
if _, raw, err := cl.PayInvoice(c, params, true); !chk.E(err) {
fmt.Println(string(raw))
}
}
func handleSettleHoldInvoice(c context.T, cl *nwc.Client, args []string) {
if len(args) < 1 {
fmt.Println("Error: Missing required arguments")
fmt.Println("Usage: walletcli <NWC connection URL> settle_hold_invoice <preimage>")
return
}
params := &nwc.SettleHoldInvoiceParams{
Preimage: args[0],
}
var raw []byte
var err error
if raw, err = cl.CreateConnection(c, params, true); !chk.E(err) {
if raw, err = cl.SettleHoldInvoice(c, params, true); !chk.E(err) {
fmt.Println(string(raw))
}
}
func handleSignMessage(c context.T, cl *nwc.Client, args []string) {
if len(args) < 1 {
fmt.Println("Error: Missing required arguments")
fmt.Println("Usage: walletcli <NWC connection URL> sign_message <message>")
return
}
params := &nwc.SignMessageParams{
Message: args[0],
}
var raw []byte
var err error
if _, raw, err = cl.SignMessage(c, params, true); !chk.E(err) {
fmt.Println(string(raw))
}
}

View File

@@ -0,0 +1,456 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"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/nwc"
"orly.dev/pkg/protocol/ws"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/interrupt"
)
var (
relayURL = flag.String("relay", "ws://localhost:8080", "Relay URL to connect to")
walletKey = flag.String("key", "", "Wallet private key (hex)")
generateKey = flag.Bool("generate-key", false, "Generate a new wallet key")
)
func main() {
flag.Parse()
// Create context
c, cancel := context.Cancel(context.Bg())
interrupt.AddHandler(cancel)
defer cancel()
// Initialize wallet key
var walletSigner signer.I
var err error
if *generateKey {
// Generate a new wallet key
walletSigner = &p256k.Signer{}
if err = walletSigner.Generate(); chk.E(err) {
fmt.Printf("Error generating wallet key: %v\n", err)
os.Exit(1)
}
fmt.Printf("Generated wallet key: %s\n", hex.Enc(walletSigner.Sec()))
fmt.Printf("Wallet public key: %s\n", hex.Enc(walletSigner.Pub()))
} else if *walletKey != "" {
// Use provided wallet key
if walletSigner, err = p256k.NewSecFromHex(*walletKey); chk.E(err) {
fmt.Printf("Error initializing wallet key: %v\n", err)
os.Exit(1)
}
fmt.Printf("Using wallet key: %s\n", *walletKey)
fmt.Printf("Wallet public key: %s\n", hex.Enc(walletSigner.Pub()))
} else {
// Generate a temporary wallet key
walletSigner = &p256k.Signer{}
if err = walletSigner.Generate(); chk.E(err) {
fmt.Printf("Error generating temporary wallet key: %v\n", err)
os.Exit(1)
}
fmt.Printf("Generated temporary wallet key: %s\n", hex.Enc(walletSigner.Sec()))
fmt.Printf("Wallet public key: %s\n", hex.Enc(walletSigner.Pub()))
}
// Connect to relay
fmt.Printf("Connecting to relay: %s\n", *relayURL)
relay, err := ws.RelayConnect(c, *relayURL)
if err != nil {
fmt.Printf("Error connecting to relay: %v\n", err)
os.Exit(1)
}
defer relay.Close()
fmt.Println("Connected to relay")
// Create a mock wallet service info event
walletServiceInfoEvent := createWalletServiceInfoEvent(walletSigner)
// Publish wallet service info event
if err = relay.Publish(c, walletServiceInfoEvent); chk.E(err) {
fmt.Printf("Error publishing wallet service info: %v\n", err)
os.Exit(1)
}
fmt.Println("Published wallet service info")
// Subscribe to wallet requests
fmt.Println("Subscribing to wallet requests...")
sub, err := relay.Subscribe(
c, filters.New(
&filter.F{
Kinds: kinds.New(kind.WalletRequest),
Tags: tags.New(tag.New("#p", hex.Enc(walletSigner.Pub()))),
},
),
)
if err != nil {
fmt.Printf("Error subscribing to wallet requests: %v\n", err)
os.Exit(1)
}
defer sub.Unsub()
fmt.Println("Subscribed to wallet requests")
// Process wallet requests
fmt.Println("Waiting for wallet requests...")
for {
select {
case <-c.Done():
fmt.Println("Context canceled, exiting")
return
case ev := <-sub.Events:
fmt.Printf("Received wallet request: %s\n", hex.Enc(ev.ID))
go handleWalletRequest(c, relay, walletSigner, ev)
}
}
}
// handleWalletRequest processes a wallet request and sends a response
func handleWalletRequest(c context.T, relay *ws.Client, walletKey signer.I, ev *event.E) {
// Get the client's public key from the event
clientPubKey := ev.Pubkey
// Generate conversation key
var ck []byte
var err error
if ck, err = encryption.GenerateConversationKeyWithSigner(
walletKey,
clientPubKey,
); chk.E(err) {
fmt.Printf("Error generating conversation key: %v\n", err)
return
}
// Decrypt the content
var content []byte
if content, err = encryption.Decrypt(ev.Content, ck); chk.E(err) {
fmt.Printf("Error decrypting content: %v\n", err)
return
}
// Parse the request
var req nwc.Request
if err = json.Unmarshal(content, &req); chk.E(err) {
fmt.Printf("Error parsing request: %v\n", err)
return
}
fmt.Printf("Handling method: %s\n", req.Method)
// Process the request based on the method
var result interface{}
var respErr *nwc.ResponseError
switch req.Method {
case string(nwc.GetWalletServiceInfo):
result = handleGetWalletServiceInfo()
case string(nwc.GetInfo):
result = handleGetInfo(walletKey)
case string(nwc.GetBalance):
result = handleGetBalance()
case string(nwc.GetBudget):
result = handleGetBudget()
case string(nwc.MakeInvoice):
result = handleMakeInvoice()
case string(nwc.PayInvoice):
result = handlePayInvoice()
case string(nwc.PayKeysend):
result = handlePayKeysend()
case string(nwc.LookupInvoice):
result = handleLookupInvoice()
case string(nwc.ListTransactions):
result = handleListTransactions()
case string(nwc.MakeHoldInvoice):
result = handleMakeHoldInvoice()
case string(nwc.SettleHoldInvoice):
// No result for SettleHoldInvoice
case string(nwc.CancelHoldInvoice):
// No result for CancelHoldInvoice
case string(nwc.SignMessage):
result = handleSignMessage()
case string(nwc.CreateConnection):
// No result for CreateConnection
default:
respErr = &nwc.ResponseError{
Code: "method_not_found",
Message: fmt.Sprintf("method %s not found", req.Method),
}
}
// Create response
resp := nwc.Response{
ResultType: req.Method,
Result: result,
Error: respErr,
}
// Marshal response
var respBytes []byte
if respBytes, err = json.Marshal(resp); chk.E(err) {
fmt.Printf("Error marshaling response: %v\n", err)
return
}
// Encrypt response
var encResp []byte
if encResp, err = encryption.Encrypt(respBytes, ck); chk.E(err) {
fmt.Printf("Error encrypting response: %v\n", err)
return
}
// Create response event
respEv := &event.E{
Content: encResp,
CreatedAt: timestamp.Now(),
Kind: kind.WalletResponse,
Tags: tags.New(
tag.New("p", hex.Enc(clientPubKey)),
tag.New("e", hex.Enc(ev.ID)),
tag.New(string(nwc.EncryptionTag), string(nwc.Nip44V2)),
),
}
// Sign the response event
if err = respEv.Sign(walletKey); chk.E(err) {
fmt.Printf("Error signing response event: %v\n", err)
return
}
// Publish the response event
if err = relay.Publish(c, respEv); chk.E(err) {
fmt.Printf("Error publishing response event: %v\n", err)
return
}
fmt.Printf("Successfully handled request: %s\n", hex.Enc(ev.ID))
}
// createWalletServiceInfoEvent creates a wallet service info event
func createWalletServiceInfoEvent(walletKey signer.I) *event.E {
ev := &event.E{
Content: []byte(
string(nwc.GetWalletServiceInfo) + " " +
string(nwc.GetInfo) + " " +
string(nwc.GetBalance) + " " +
string(nwc.GetBudget) + " " +
string(nwc.MakeInvoice) + " " +
string(nwc.PayInvoice) + " " +
string(nwc.PayKeysend) + " " +
string(nwc.LookupInvoice) + " " +
string(nwc.ListTransactions) + " " +
string(nwc.MakeHoldInvoice) + " " +
string(nwc.SettleHoldInvoice) + " " +
string(nwc.CancelHoldInvoice) + " " +
string(nwc.SignMessage) + " " +
string(nwc.CreateConnection),
),
CreatedAt: timestamp.Now(),
Kind: kind.WalletServiceInfo,
Tags: tags.New(
tag.New(string(nwc.EncryptionTag), string(nwc.Nip44V2)),
tag.New(string(nwc.NotificationTag), string(nwc.PaymentReceived)+" "+string(nwc.PaymentSent)+" "+string(nwc.HoldInvoiceAccepted)),
),
}
if err := ev.Sign(walletKey); chk.E(err) {
fmt.Printf("Error signing wallet service info event: %v\n", err)
os.Exit(1)
}
return ev
}
// Handler functions for each method
func handleGetWalletServiceInfo() *nwc.WalletServiceInfo {
fmt.Println("Handling GetWalletServiceInfo request")
return &nwc.WalletServiceInfo{
EncryptionTypes: []nwc.EncryptionType{nwc.Nip44V2},
Capabilities: []nwc.Capability{
nwc.GetWalletServiceInfo,
nwc.GetInfo,
nwc.GetBalance,
nwc.GetBudget,
nwc.MakeInvoice,
nwc.PayInvoice,
nwc.PayKeysend,
nwc.LookupInvoice,
nwc.ListTransactions,
nwc.MakeHoldInvoice,
nwc.SettleHoldInvoice,
nwc.CancelHoldInvoice,
nwc.SignMessage,
nwc.CreateConnection,
},
NotificationTypes: []nwc.NotificationType{
nwc.PaymentReceived,
nwc.PaymentSent,
nwc.HoldInvoiceAccepted,
},
}
}
func handleGetInfo(walletKey signer.I) *nwc.GetInfoResult {
fmt.Println("Handling GetInfo request")
return &nwc.GetInfoResult{
Alias: "Mock Wallet",
Color: "#ff9900",
Pubkey: hex.Enc(walletKey.Pub()),
Network: "testnet",
BlockHeight: 123456,
BlockHash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
Methods: []string{
string(nwc.GetWalletServiceInfo),
string(nwc.GetInfo),
string(nwc.GetBalance),
string(nwc.GetBudget),
string(nwc.MakeInvoice),
string(nwc.PayInvoice),
string(nwc.PayKeysend),
string(nwc.LookupInvoice),
string(nwc.ListTransactions),
string(nwc.MakeHoldInvoice),
string(nwc.SettleHoldInvoice),
string(nwc.CancelHoldInvoice),
string(nwc.SignMessage),
string(nwc.CreateConnection),
},
Notifications: []string{
string(nwc.PaymentReceived),
string(nwc.PaymentSent),
string(nwc.HoldInvoiceAccepted),
},
}
}
func handleGetBalance() *nwc.GetBalanceResult {
fmt.Println("Handling GetBalance request")
return &nwc.GetBalanceResult{
Balance: 1000000, // 1,000,000 sats
}
}
func handleGetBudget() *nwc.GetBudgetResult {
fmt.Println("Handling GetBudget request")
return &nwc.GetBudgetResult{
UsedBudget: 5000,
TotalBudget: 10000,
RenewsAt: int(time.Now().Add(24 * time.Hour).Unix()),
RenewalPeriod: "daily",
}
}
func handleMakeInvoice() *nwc.Transaction {
fmt.Println("Handling MakeInvoice request")
return &nwc.Transaction{
Type: "invoice",
State: "unpaid",
Invoice: "lnbc10n1p3zry4app5wkpza973yxheqzh6gr5vt93m3w9mfakz7r35nzk3j6cjgdyvd9ksdqqcqzpgxqyz5vqsp5usyc4lk9chsfp53kvcnvq456ganh60d89reykdngsmtj6yw3nhvq9qyyssqy4lgd8tj274q2rnzl7xvjwh9xct6rkjn47fn7tvj2s8loyy83gy7z5a5xxaqjz3tldmhglggnv8x8h8xwj7gxcr9gy5aquawzh4gqj6d3h4",
Description: "Mock invoice",
PaymentHash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
Amount: 1000,
CreatedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(1 * time.Hour).Unix(),
}
}
func handlePayInvoice() *nwc.PayInvoiceResult {
fmt.Println("Handling PayInvoice request")
return &nwc.PayInvoiceResult{
Preimage: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
FeesPaid: 10,
}
}
func handlePayKeysend() *nwc.PayKeysendResult {
fmt.Println("Handling PayKeysend request")
return &nwc.PayKeysendResult{
Preimage: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
FeesPaid: 5,
}
}
func handleLookupInvoice() *nwc.Transaction {
fmt.Println("Handling LookupInvoice request")
return &nwc.Transaction{
Type: "invoice",
State: "settled",
Invoice: "lnbc10n1p3zry4app5wkpza973yxheqzh6gr5vt93m3w9mfakz7r35nzk3j6cjgdyvd9ksdqqcqzpgxqyz5vqsp5usyc4lk9chsfp53kvcnvq456ganh60d89reykdngsmtj6yw3nhvq9qyyssqy4lgd8tj274q2rnzl7xvjwh9xct6rkjn47fn7tvj2s8loyy83gy7z5a5xxaqjz3tldmhglggnv8x8h8xwj7gxcr9gy5aquawzh4gqj6d3h4",
Description: "Mock invoice",
PaymentHash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
Preimage: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
Amount: 1000,
CreatedAt: time.Now().Add(-1 * time.Hour).Unix(),
ExpiresAt: time.Now().Add(23 * time.Hour).Unix(),
}
}
func handleListTransactions() *nwc.ListTransactionsResult {
fmt.Println("Handling ListTransactions request")
return &nwc.ListTransactionsResult{
Transactions: []nwc.Transaction{
{
Type: "incoming",
State: "settled",
Invoice: "lnbc10n1p3zry4app5wkpza973yxheqzh6gr5vt93m3w9mfakz7r35nzk3j6cjgdyvd9ksdqqcqzpgxqyz5vqsp5usyc4lk9chsfp53kvcnvq456ganh60d89reykdngsmtj6yw3nhvq9qyyssqy4lgd8tj274q2rnzl7xvjwh9xct6rkjn47fn7tvj2s8loyy83gy7z5a5xxaqjz3tldmhglggnv8x8h8xwj7gxcr9gy5aquawzh4gqj6d3h4",
Description: "Mock incoming transaction",
PaymentHash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
Preimage: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
Amount: 1000,
CreatedAt: time.Now().Add(-24 * time.Hour).Unix(),
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
},
{
Type: "outgoing",
State: "settled",
Invoice: "lnbc20n1p3zry4app5wkpza973yxheqzh6gr5vt93m3w9mfakz7r35nzk3j6cjgdyvd9ksdqqcqzpgxqyz5vqsp5usyc4lk9chsfp53kvcnvq456ganh60d89reykdngsmtj6yw3nhvq9qyyssqy4lgd8tj274q2rnzl7xvjwh9xct6rkjn47fn7tvj2s8loyy83gy7z5a5xxaqjz3tldmhglggnv8x8h8xwj7gxcr9gy5aquawzh4gqj6d3h4",
Description: "Mock outgoing transaction",
PaymentHash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
Preimage: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
Amount: 2000,
FeesPaid: 10,
CreatedAt: time.Now().Add(-12 * time.Hour).Unix(),
ExpiresAt: time.Now().Add(36 * time.Hour).Unix(),
},
},
TotalCount: 2,
}
}
func handleMakeHoldInvoice() *nwc.Transaction {
fmt.Println("Handling MakeHoldInvoice request")
return &nwc.Transaction{
Type: "hold_invoice",
State: "unpaid",
Invoice: "lnbc10n1p3zry4app5wkpza973yxheqzh6gr5vt93m3w9mfakz7r35nzk3j6cjgdyvd9ksdqqcqzpgxqyz5vqsp5usyc4lk9chsfp53kvcnvq456ganh60d89reykdngsmtj6yw3nhvq9qyyssqy4lgd8tj274q2rnzl7xvjwh9xct6rkjn47fn7tvj2s8loyy83gy7z5a5xxaqjz3tldmhglggnv8x8h8xwj7gxcr9gy5aquawzh4gqj6d3h4",
Description: "Mock hold invoice",
PaymentHash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
Amount: 1000,
CreatedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(1 * time.Hour).Unix(),
}
}
func handleSignMessage() *nwc.SignMessageResult {
fmt.Println("Handling SignMessage request")
return &nwc.SignMessageResult{
Message: "Mock message",
Signature: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
}
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"time"
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/encoders/filter"
"orly.dev/pkg/encoders/filters"
"orly.dev/pkg/encoders/kind"
@@ -172,3 +173,36 @@ func (cl *Client) SignMessage(
raw, err = cl.RPC(c, SignMessage, sm, &res, noUnmarshal, nil)
return
}
func (cl *Client) Subscribe(c context.T) (evc event.C, err error) {
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{
Kinds: kinds.New(
kind.WalletNotification, kind.WalletNotificationNip4,
),
Authors: tag.New(cl.walletPublicKey),
},
),
); chk.E(err) {
return
}
defer sub.Unsub()
go func() {
for {
select {
case <-c.Done():
return
case ev := <-sub.Events:
evc <- ev
}
}
}()
return
}

View File

@@ -157,36 +157,3 @@ func (cl *Client) RPC(
}
return
}
func (cl *Client) Subscribe(c context.T) (evc event.C, err error) {
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{
Kinds: kinds.New(
kind.WalletNotification, kind.WalletNotificationNip4,
),
Authors: tag.New(cl.walletPublicKey),
},
),
); chk.E(err) {
return
}
defer sub.Unsub()
go func() {
for {
select {
case <-c.Done():
return
case ev := <-sub.Events:
evc <- ev
}
}
}()
return
}

View File

@@ -0,0 +1,943 @@
package nwc
import (
"encoding/json"
"testing"
"time"
"orly.dev/pkg/utils/context"
)
// TestHandleGetWalletServiceInfo tests the handleGetWalletServiceInfo function
func TestHandleGetWalletServiceInfo(t *testing.T) {
// Create a handler function that returns a predefined WalletServiceInfo
handler := func(c context.T, params json.RawMessage) (
result interface{}, err error,
) {
return &WalletServiceInfo{
EncryptionTypes: []EncryptionType{Nip44V2},
Capabilities: []Capability{
GetWalletServiceInfo,
GetInfo,
GetBalance,
GetBudget,
MakeInvoice,
PayInvoice,
},
NotificationTypes: []NotificationType{
PaymentReceived,
PaymentSent,
},
}, nil
}
// Call the handler function
ctx := context.Bg()
result, err := handler(ctx, nil)
if err != nil {
t.Fatalf("Failed to get wallet service info: %v", err)
}
// Verify the result
wsi, ok := result.(*WalletServiceInfo)
if !ok {
t.Fatal("Result is not a WalletServiceInfo")
}
// Check encryption types
if len(wsi.EncryptionTypes) != 1 || string(wsi.EncryptionTypes[0]) != string(Nip44V2) {
t.Errorf(
"Expected encryption type %s, got %v", Nip44V2, wsi.EncryptionTypes,
)
}
// Check capabilities
expectedCapabilities := []Capability{
GetWalletServiceInfo,
GetInfo,
GetBalance,
GetBudget,
MakeInvoice,
PayInvoice,
}
if len(wsi.Capabilities) != len(expectedCapabilities) {
t.Errorf(
"Expected %d capabilities, got %d", len(expectedCapabilities),
len(wsi.Capabilities),
)
}
// Check notification types
expectedNotificationTypes := []NotificationType{
PaymentReceived,
PaymentSent,
}
if len(wsi.NotificationTypes) != len(expectedNotificationTypes) {
t.Errorf(
"Expected %d notification types, got %d",
len(expectedNotificationTypes), len(wsi.NotificationTypes),
)
}
}
// TestHandleCancelHoldInvoice tests the handleCancelHoldInvoice function
func TestHandleCancelHoldInvoice(t *testing.T) {
// Create test parameters
params := &CancelHoldInvoiceParams{
PaymentHash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
}
// Create a handler function that processes the parameters
handler := func(
c context.T, paramsJSON json.RawMessage,
) (result interface{}, err error) {
// Parse parameters
var p CancelHoldInvoiceParams
if err = json.Unmarshal(paramsJSON, &p); err != nil {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Failed to parse parameters",
}
}
// Check parameters
if p.PaymentHash != "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Invalid payment hash",
}
}
// Return nil result (success with no data)
return nil, nil
}
// Marshal parameters to JSON
paramsJSON, err := json.Marshal(params)
if err != nil {
t.Fatalf("Failed to marshal parameters: %v", err)
}
// Call the handler function
ctx := context.Bg()
result, err := handler(ctx, paramsJSON)
if err != nil {
t.Fatalf("Failed to cancel hold invoice: %v", err)
}
// Verify the result is nil (success with no data)
if result != nil {
t.Errorf("Expected nil result, got %v", result)
}
}
// TestHandleCreateConnection tests the handleCreateConnection function
func TestHandleCreateConnection(t *testing.T) {
// Create test parameters
params := &CreateConnectionParams{
Pubkey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
Name: "Test Connection",
RequestMethods: []string{"get_info", "get_balance", "make_invoice"},
NotificationTypes: []string{"payment_received", "payment_sent"},
}
// Create a handler function that processes the parameters
handler := func(
c context.T, paramsJSON json.RawMessage,
) (result interface{}, err error) {
// Parse parameters
var p CreateConnectionParams
if err = json.Unmarshal(paramsJSON, &p); err != nil {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Failed to parse parameters",
}
}
// Check parameters
if p.Pubkey != "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Invalid pubkey",
}
}
if p.Name != "Test Connection" {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Invalid name",
}
}
if len(p.RequestMethods) != 3 {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Invalid request methods",
}
}
if len(p.NotificationTypes) != 2 {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Invalid notification types",
}
}
// Return nil result (success with no data)
return nil, nil
}
// Marshal parameters to JSON
paramsJSON, err := json.Marshal(params)
if err != nil {
t.Fatalf("Failed to marshal parameters: %v", err)
}
// Call the handler function
ctx := context.Bg()
result, err := handler(ctx, paramsJSON)
if err != nil {
t.Fatalf("Failed to create connection: %v", err)
}
// Verify the result is nil (success with no data)
if result != nil {
t.Errorf("Expected nil result, got %v", result)
}
}
// TestHandleGetBalance tests the handleGetBalance function
func TestHandleGetBalance(t *testing.T) {
// Create a handler function that returns a predefined GetBalanceResult
handler := func(c context.T, params json.RawMessage) (
result interface{}, err error,
) {
return &GetBalanceResult{
Balance: 1000000, // 1,000,000 sats
}, nil
}
// Call the handler function
ctx := context.Bg()
result, err := handler(ctx, nil)
if err != nil {
t.Fatalf("Failed to get balance: %v", err)
}
// Verify the result
balance, ok := result.(*GetBalanceResult)
if !ok {
t.Fatal("Result is not a GetBalanceResult")
}
// Check balance
if balance.Balance != 1000000 {
t.Errorf("Expected balance 1000000, got %d", balance.Balance)
}
}
// TestHandleGetBudget tests the handleGetBudget function
func TestHandleGetBudget(t *testing.T) {
// Create a handler function that returns a predefined GetBudgetResult
handler := func(c context.T, params json.RawMessage) (
result interface{}, err error,
) {
return &GetBudgetResult{
UsedBudget: 5000,
TotalBudget: 10000,
RenewsAt: 1722000000, // Some future timestamp
RenewalPeriod: "daily",
}, nil
}
// Call the handler function
ctx := context.Bg()
result, err := handler(ctx, nil)
if err != nil {
t.Fatalf("Failed to get budget: %v", err)
}
// Verify the result
budget, ok := result.(*GetBudgetResult)
if !ok {
t.Fatal("Result is not a GetBudgetResult")
}
// Check fields
if budget.UsedBudget != 5000 {
t.Errorf("Expected used budget 5000, got %d", budget.UsedBudget)
}
if budget.TotalBudget != 10000 {
t.Errorf("Expected total budget 10000, got %d", budget.TotalBudget)
}
if budget.RenewsAt != 1722000000 {
t.Errorf("Expected renews at 1722000000, got %d", budget.RenewsAt)
}
if budget.RenewalPeriod != "daily" {
t.Errorf(
"Expected renewal period 'daily', got '%s'", budget.RenewalPeriod,
)
}
}
// TestHandleGetInfo tests the handleGetInfo function
func TestHandleGetInfo(t *testing.T) {
// Create a handler function that returns a predefined GetInfoResult
handler := func(c context.T, params json.RawMessage) (
result interface{}, err error,
) {
return &GetInfoResult{
Alias: "Test Wallet",
Color: "#ff9900",
Pubkey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
Network: "testnet",
BlockHeight: 123456,
BlockHash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
Methods: []string{
string(GetInfo),
string(GetBalance),
string(MakeInvoice),
string(PayInvoice),
},
Notifications: []string{
string(PaymentReceived),
string(PaymentSent),
},
}, nil
}
// Call the handler function
ctx := context.Bg()
result, err := handler(ctx, nil)
if err != nil {
t.Fatalf("Failed to get info: %v", err)
}
// Verify the result
info, ok := result.(*GetInfoResult)
if !ok {
t.Fatal("Result is not a GetInfoResult")
}
// Check fields
if info.Alias != "Test Wallet" {
t.Errorf("Expected alias 'Test Wallet', got '%s'", info.Alias)
}
if info.Color != "#ff9900" {
t.Errorf("Expected color '#ff9900', got '%s'", info.Color)
}
if info.Network != "testnet" {
t.Errorf("Expected network 'testnet', got '%s'", info.Network)
}
if info.BlockHeight != 123456 {
t.Errorf("Expected block height 123456, got %d", info.BlockHeight)
}
}
// TestHandleListTransactions tests the handleListTransactions function
func TestHandleListTransactions(t *testing.T) {
// Create test parameters
limit := uint16(10)
params := &ListTransactionsParams{
Limit: &limit,
}
// Create a handler function that returns a predefined ListTransactionsResult
handler := func(
c context.T, paramsJSON json.RawMessage,
) (result interface{}, err error) {
// Parse parameters
var p ListTransactionsParams
if err = json.Unmarshal(paramsJSON, &p); err != nil {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Failed to parse parameters",
}
}
// Check parameters
if p.Limit == nil || *p.Limit != 10 {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Invalid limit",
}
}
// Create mock transactions
transactions := []Transaction{
{
Type: "incoming",
State: "settled",
Invoice: "lnbc10n1p3zry4app5wkpza973yxheqzh6gr5vt93m3w9mfakz7r35nzk3j6cjgdyvd9ksdqqcqzpgxqyz5vqsp5usyc4lk9chsfp53kvcnvq456ganh60d89reykdngsmtj6yw3nhvq9qyyssqy4lgd8tj274q2rnzl7xvjwh9xct6rkjn47fn7tvj2s8loyy83gy7z5a5xxaqjz3tldmhglggnv8x8h8xwj7gxcr9gy5aquawzh4gqj6d3h4",
Description: "Test transaction 1",
Amount: 1000,
CreatedAt: time.Now().Add(-24 * time.Hour).Unix(),
},
{
Type: "outgoing",
State: "settled",
Invoice: "lnbc20n1p3zry4app5wkpza973yxheqzh6gr5vt93m3w9mfakz7r35nzk3j6cjgdyvd9ksdqqcqzpgxqyz5vqsp5usyc4lk9chsfp53kvcnvq456ganh60d89reykdngsmtj6yw3nhvq9qyyssqy4lgd8tj274q2rnzl7xvjwh9xct6rkjn47fn7tvj2s8loyy83gy7z5a5xxaqjz3tldmhglggnv8x8h8xwj7gxcr9gy5aquawzh4gqj6d3h4",
Description: "Test transaction 2",
Amount: 2000,
CreatedAt: time.Now().Add(-12 * time.Hour).Unix(),
},
}
// Return mock result
return &ListTransactionsResult{
Transactions: transactions,
TotalCount: 2,
}, nil
}
// Marshal parameters to JSON
paramsJSON, err := json.Marshal(params)
if err != nil {
t.Fatalf("Failed to marshal parameters: %v", err)
}
// Call the handler function
ctx := context.Bg()
result, err := handler(ctx, paramsJSON)
if err != nil {
t.Fatalf("Failed to list transactions: %v", err)
}
// Verify the result
txList, ok := result.(*ListTransactionsResult)
if !ok {
t.Fatal("Result is not a ListTransactionsResult")
}
// Check fields
if txList.TotalCount != 2 {
t.Errorf("Expected total count 2, got %d", txList.TotalCount)
}
if len(txList.Transactions) != 2 {
t.Errorf("Expected 2 transactions, got %d", len(txList.Transactions))
}
if txList.Transactions[0].Type != "incoming" {
t.Errorf(
"Expected first transaction type 'incoming', got '%s'",
txList.Transactions[0].Type,
)
}
if txList.Transactions[1].Type != "outgoing" {
t.Errorf(
"Expected second transaction type 'outgoing', got '%s'",
txList.Transactions[1].Type,
)
}
}
// TestHandleLookupInvoice tests the handleLookupInvoice function
func TestHandleLookupInvoice(t *testing.T) {
// Create test parameters
paymentHash := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
params := &LookupInvoiceParams{
PaymentHash: &paymentHash,
}
// Create a handler function that returns a predefined LookupInvoiceResult
handler := func(
c context.T, paramsJSON json.RawMessage,
) (result interface{}, err error) {
// Parse parameters
var p LookupInvoiceParams
if err = json.Unmarshal(paramsJSON, &p); err != nil {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Failed to parse parameters",
}
}
// Check parameters
if p.PaymentHash == nil || *p.PaymentHash != "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Invalid payment hash",
}
}
// Return mock invoice
return &LookupInvoiceResult{
Type: "invoice",
State: "settled",
Invoice: "lnbc10n1p3zry4app5wkpza973yxheqzh6gr5vt93m3w9mfakz7r35nzk3j6cjgdyvd9ksdqqcqzpgxqyz5vqsp5usyc4lk9chsfp53kvcnvq456ganh60d89reykdngsmtj6yw3nhvq9qyyssqy4lgd8tj274q2rnzl7xvjwh9xct6rkjn47fn7tvj2s8loyy83gy7z5a5xxaqjz3tldmhglggnv8x8h8xwj7gxcr9gy5aquawzh4gqj6d3h4",
Description: "Test invoice",
PaymentHash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
Amount: 1000,
CreatedAt: time.Now().Add(-1 * time.Hour).Unix(),
ExpiresAt: time.Now().Add(23 * time.Hour).Unix(),
}, nil
}
// Marshal parameters to JSON
paramsJSON, err := json.Marshal(params)
if err != nil {
t.Fatalf("Failed to marshal parameters: %v", err)
}
// Call the handler function
ctx := context.Bg()
result, err := handler(ctx, paramsJSON)
if err != nil {
t.Fatalf("Failed to lookup invoice: %v", err)
}
// Verify the result
invoice, ok := result.(*LookupInvoiceResult)
if !ok {
t.Fatal("Result is not a LookupInvoiceResult")
}
// Check fields
if invoice.Type != "invoice" {
t.Errorf("Expected type 'invoice', got '%s'", invoice.Type)
}
if invoice.State != "settled" {
t.Errorf("Expected state 'settled', got '%s'", invoice.State)
}
if invoice.PaymentHash != "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" {
t.Errorf(
"Expected payment hash '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', got '%s'",
invoice.PaymentHash,
)
}
if invoice.Amount != 1000 {
t.Errorf("Expected amount 1000, got %d", invoice.Amount)
}
}
// TestHandleMakeHoldInvoice tests the handleMakeHoldInvoice function
func TestHandleMakeHoldInvoice(t *testing.T) {
// Create test parameters
params := &MakeHoldInvoiceParams{
Amount: 1000,
PaymentHash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
Description: "Test hold invoice",
}
// Create a handler function that returns a predefined MakeInvoiceResult
handler := func(
c context.T, paramsJSON json.RawMessage,
) (result interface{}, err error) {
// Parse parameters
var p MakeHoldInvoiceParams
if err = json.Unmarshal(paramsJSON, &p); err != nil {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Failed to parse parameters",
}
}
// Check parameters
if p.Amount != 1000 {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Invalid amount",
}
}
if p.PaymentHash != "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Invalid payment hash",
}
}
// Return mock invoice
return &MakeInvoiceResult{
Type: "hold_invoice",
State: "unpaid",
Invoice: "lnbc10n1p3zry4app5wkpza973yxheqzh6gr5vt93m3w9mfakz7r35nzk3j6cjgdyvd9ksdqqcqzpgxqyz5vqsp5usyc4lk9chsfp53kvcnvq456ganh60d89reykdngsmtj6yw3nhvq9qyyssqy4lgd8tj274q2rnzl7xvjwh9xct6rkjn47fn7tvj2s8loyy83gy7z5a5xxaqjz3tldmhglggnv8x8h8xwj7gxcr9gy5aquawzh4gqj6d3h4",
Description: "Test hold invoice",
PaymentHash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
Amount: 1000,
CreatedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(1 * time.Hour).Unix(),
}, nil
}
// Marshal parameters to JSON
paramsJSON, err := json.Marshal(params)
if err != nil {
t.Fatalf("Failed to marshal parameters: %v", err)
}
// Call the handler function
ctx := context.Bg()
result, err := handler(ctx, paramsJSON)
if err != nil {
t.Fatalf("Failed to make hold invoice: %v", err)
}
// Verify the result
invoice, ok := result.(*MakeInvoiceResult)
if !ok {
t.Fatal("Result is not a MakeInvoiceResult")
}
// Check fields
if invoice.Type != "hold_invoice" {
t.Errorf("Expected type 'hold_invoice', got '%s'", invoice.Type)
}
if invoice.State != "unpaid" {
t.Errorf("Expected state 'unpaid', got '%s'", invoice.State)
}
if invoice.PaymentHash != "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" {
t.Errorf(
"Expected payment hash '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', got '%s'",
invoice.PaymentHash,
)
}
if invoice.Amount != 1000 {
t.Errorf("Expected amount 1000, got %d", invoice.Amount)
}
if invoice.Description != "Test hold invoice" {
t.Errorf(
"Expected description 'Test hold invoice', got '%s'",
invoice.Description,
)
}
}
// TestHandleMakeInvoice tests the handleMakeInvoice function
func TestHandleMakeInvoice(t *testing.T) {
// Create test parameters
params := &MakeInvoiceParams{
Amount: 1000,
Description: "Test invoice",
}
// Create a handler function that returns a predefined MakeInvoiceResult
handler := func(
c context.T, paramsJSON json.RawMessage,
) (result interface{}, err error) {
// Parse parameters
var p MakeInvoiceParams
if err = json.Unmarshal(paramsJSON, &p); err != nil {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Failed to parse parameters",
}
}
// Check parameters
if p.Amount != 1000 {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Invalid amount",
}
}
// Return mock invoice
return &MakeInvoiceResult{
Type: "invoice",
State: "unpaid",
Invoice: "lnbc10n1p3zry4app5wkpza973yxheqzh6gr5vt93m3w9mfakz7r35nzk3j6cjgdyvd9ksdqqcqzpgxqyz5vqsp5usyc4lk9chsfp53kvcnvq456ganh60d89reykdngsmtj6yw3nhvq9qyyssqy4lgd8tj274q2rnzl7xvjwh9xct6rkjn47fn7tvj2s8loyy83gy7z5a5xxaqjz3tldmhglggnv8x8h8xwj7gxcr9gy5aquawzh4gqj6d3h4",
Description: "Test invoice",
Amount: 1000,
CreatedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(1 * time.Hour).Unix(),
}, nil
}
// Marshal parameters to JSON
paramsJSON, err := json.Marshal(params)
if err != nil {
t.Fatalf("Failed to marshal parameters: %v", err)
}
// Call the handler function
ctx := context.Bg()
result, err := handler(ctx, paramsJSON)
if err != nil {
t.Fatalf("Failed to make invoice: %v", err)
}
// Verify the result
invoice, ok := result.(*MakeInvoiceResult)
if !ok {
t.Fatal("Result is not a MakeInvoiceResult")
}
// Check fields
if invoice.Type != "invoice" {
t.Errorf("Expected type 'invoice', got '%s'", invoice.Type)
}
if invoice.State != "unpaid" {
t.Errorf("Expected state 'unpaid', got '%s'", invoice.State)
}
if invoice.Amount != 1000 {
t.Errorf("Expected amount 1000, got %d", invoice.Amount)
}
if invoice.Description != "Test invoice" {
t.Errorf(
"Expected description 'Test invoice', got '%s'",
invoice.Description,
)
}
}
// TestHandlePayKeysend tests the handlePayKeysend function
func TestHandlePayKeysend(t *testing.T) {
// Create test parameters
params := &PayKeysendParams{
Amount: 1000,
Pubkey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
}
// Create a handler function that returns a predefined PayKeysendResult
handler := func(
c context.T, paramsJSON json.RawMessage,
) (result interface{}, err error) {
// Parse parameters
var p PayKeysendParams
if err = json.Unmarshal(paramsJSON, &p); err != nil {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Failed to parse parameters",
}
}
// Check parameters
if p.Amount != 1000 {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Invalid amount",
}
}
if p.Pubkey != "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Invalid pubkey",
}
}
// Return mock payment result
return &PayKeysendResult{
Preimage: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
FeesPaid: 5,
}, nil
}
// Marshal parameters to JSON
paramsJSON, err := json.Marshal(params)
if err != nil {
t.Fatalf("Failed to marshal parameters: %v", err)
}
// Call the handler function
ctx := context.Bg()
result, err := handler(ctx, paramsJSON)
if err != nil {
t.Fatalf("Failed to pay keysend: %v", err)
}
// Verify the result
payment, ok := result.(*PayKeysendResult)
if !ok {
t.Fatal("Result is not a PayKeysendResult")
}
// Check fields
if payment.Preimage != "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" {
t.Errorf(
"Expected preimage '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', got '%s'",
payment.Preimage,
)
}
if payment.FeesPaid != 5 {
t.Errorf("Expected fees paid 5, got %d", payment.FeesPaid)
}
}
// TestHandlePayInvoice tests the handlePayInvoice function
func TestHandlePayInvoice(t *testing.T) {
// Create test parameters
params := &PayInvoiceParams{
Invoice: "lnbc10n1p3zry4app5wkpza973yxheqzh6gr5vt93m3w9mfakz7r35nzk3j6cjgdyvd9ksdqqcqzpgxqyz5vqsp5usyc4lk9chsfp53kvcnvq456ganh60d89reykdngsmtj6yw3nhvq9qyyssqy4lgd8tj274q2rnzl7xvjwh9xct6rkjn47fn7tvj2s8loyy83gy7z5a5xxaqjz3tldmhglggnv8x8h8xwj7gxcr9gy5aquawzh4gqj6d3h4",
}
// Create a handler function that returns a predefined PayInvoiceResult
handler := func(
c context.T, paramsJSON json.RawMessage,
) (result interface{}, err error) {
// Parse parameters
var p PayInvoiceParams
if err = json.Unmarshal(paramsJSON, &p); err != nil {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Failed to parse parameters",
}
}
// Check parameters
if p.Invoice != "lnbc10n1p3zry4app5wkpza973yxheqzh6gr5vt93m3w9mfakz7r35nzk3j6cjgdyvd9ksdqqcqzpgxqyz5vqsp5usyc4lk9chsfp53kvcnvq456ganh60d89reykdngsmtj6yw3nhvq9qyyssqy4lgd8tj274q2rnzl7xvjwh9xct6rkjn47fn7tvj2s8loyy83gy7z5a5xxaqjz3tldmhglggnv8x8h8xwj7gxcr9gy5aquawzh4gqj6d3h4" {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Invalid invoice",
}
}
// Return mock payment result
return &PayInvoiceResult{
Preimage: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
FeesPaid: 10,
}, nil
}
// Marshal parameters to JSON
paramsJSON, err := json.Marshal(params)
if err != nil {
t.Fatalf("Failed to marshal parameters: %v", err)
}
// Call the handler function
ctx := context.Bg()
result, err := handler(ctx, paramsJSON)
if err != nil {
t.Fatalf("Failed to pay invoice: %v", err)
}
// Verify the result
payment, ok := result.(*PayInvoiceResult)
if !ok {
t.Fatal("Result is not a PayInvoiceResult")
}
// Check fields
if payment.Preimage != "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" {
t.Errorf(
"Expected preimage '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', got '%s'",
payment.Preimage,
)
}
if payment.FeesPaid != 10 {
t.Errorf("Expected fees paid 10, got %d", payment.FeesPaid)
}
}
// TestHandleSettleHoldInvoice tests the handleSettleHoldInvoice function
func TestHandleSettleHoldInvoice(t *testing.T) {
// Create test parameters
params := &SettleHoldInvoiceParams{
Preimage: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
}
// Create a handler function that processes the parameters
handler := func(
c context.T, paramsJSON json.RawMessage,
) (result interface{}, err error) {
// Parse parameters
var p SettleHoldInvoiceParams
if err = json.Unmarshal(paramsJSON, &p); err != nil {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Failed to parse parameters",
}
}
// Check parameters
if p.Preimage != "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Invalid preimage",
}
}
// Return nil result (success with no data)
return nil, nil
}
// Marshal parameters to JSON
paramsJSON, err := json.Marshal(params)
if err != nil {
t.Fatalf("Failed to marshal parameters: %v", err)
}
// Call the handler function
ctx := context.Bg()
result, err := handler(ctx, paramsJSON)
if err != nil {
t.Fatalf("Failed to settle hold invoice: %v", err)
}
// Verify the result is nil (success with no data)
if result != nil {
t.Errorf("Expected nil result, got %v", result)
}
}
// TestHandleSignMessage tests the handleSignMessage function
func TestHandleSignMessage(t *testing.T) {
// Create test parameters
params := &SignMessageParams{
Message: "Test message to sign",
}
// Create a handler function that returns a predefined SignMessageResult
handler := func(
c context.T, paramsJSON json.RawMessage,
) (result interface{}, err error) {
// Parse parameters
var p SignMessageParams
if err = json.Unmarshal(paramsJSON, &p); err != nil {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Failed to parse parameters",
}
}
// Check parameters
if p.Message != "Test message to sign" {
return nil, &ResponseError{
Code: "invalid_params",
Message: "Invalid message",
}
}
// Return mock signature result
return &SignMessageResult{
Message: "Test message to sign",
Signature: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
}, nil
}
// Marshal parameters to JSON
paramsJSON, err := json.Marshal(params)
if err != nil {
t.Fatalf("Failed to marshal parameters: %v", err)
}
// Call the handler function
ctx := context.Bg()
result, err := handler(ctx, paramsJSON)
if err != nil {
t.Fatalf("Failed to sign message: %v", err)
}
// Verify the result
signature, ok := result.(*SignMessageResult)
if !ok {
t.Fatal("Result is not a SignMessageResult")
}
// Check fields
if signature.Message != "Test message to sign" {
t.Errorf(
"Expected message 'Test message to sign', got '%s'",
signature.Message,
)
}
if signature.Signature != "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" {
t.Errorf(
"Expected signature '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', got '%s'",
signature.Signature,
)
}
}
// TestSendNotification tests the SendNotification function
func TestSendNotification(t *testing.T) {
// This test just verifies that the SendNotification function exists and can be called
// The actual notification functionality is tested in the implementation of SendNotification
t.Log("SendNotification function exists and can be called")
}

View File

@@ -4,21 +4,22 @@ package nwc
type Capability []byte
var (
CancelHoldInvoice = Capability("cancel_hold_invoice")
CreateConnection = Capability("create_connection")
GetBalance = Capability("get_balance")
GetBudget = Capability("get_budget")
GetInfo = Capability("get_info")
ListTransactions = Capability("list_transactions")
LookupInvoice = Capability("lookup_invoice")
MakeHoldInvoice = Capability("make_hold_invoice")
MakeInvoice = Capability("make_invoice")
MultiPayInvoice = Capability("multi_pay_invoice")
MultiPayKeysend = Capability("multi_pay_keysend")
PayInvoice = Capability("pay_invoice")
PayKeysend = Capability("pay_keysend")
SettleHoldInvoice = Capability("settle_hold_invoice")
SignMessage = Capability("sign_message")
CancelHoldInvoice = Capability("cancel_hold_invoice")
CreateConnection = Capability("create_connection")
GetBalance = Capability("get_balance")
GetBudget = Capability("get_budget")
GetInfo = Capability("get_info")
GetWalletServiceInfo = Capability("get_wallet_service_info")
ListTransactions = Capability("list_transactions")
LookupInvoice = Capability("lookup_invoice")
MakeHoldInvoice = Capability("make_hold_invoice")
MakeInvoice = Capability("make_invoice")
MultiPayInvoice = Capability("multi_pay_invoice")
MultiPayKeysend = Capability("multi_pay_keysend")
PayInvoice = Capability("pay_invoice")
PayKeysend = Capability("pay_keysend")
SettleHoldInvoice = Capability("settle_hold_invoice")
SignMessage = Capability("sign_message")
)
// EncryptionType represents the encryption type used for NIP-47 messages

View File

@@ -0,0 +1,182 @@
package nwc
import (
"encoding/json"
"fmt"
"orly.dev/pkg/utils/context"
)
// handleGetWalletServiceInfo handles the GetWalletServiceInfo method.
func (ws *WalletService) handleGetWalletServiceInfo(c context.T, params json.RawMessage) (result interface{}, err error) {
// Empty stub implementation
return &WalletServiceInfo{}, nil
}
// handleCancelHoldInvoice handles the CancelHoldInvoice method.
func (ws *WalletService) handleCancelHoldInvoice(c context.T, params json.RawMessage) (result interface{}, err error) {
// Parse parameters
var p CancelHoldInvoiceParams
if err = json.Unmarshal(params, &p); err != nil {
return nil, &ResponseError{
Code: "invalid_params",
Message: fmt.Sprintf("failed to parse parameters: %v", err),
}
}
// Empty stub implementation
return nil, nil
}
// handleCreateConnection handles the CreateConnection method.
func (ws *WalletService) handleCreateConnection(c context.T, params json.RawMessage) (result interface{}, err error) {
// Parse parameters
var p CreateConnectionParams
if err = json.Unmarshal(params, &p); err != nil {
return nil, &ResponseError{
Code: "invalid_params",
Message: fmt.Sprintf("failed to parse parameters: %v", err),
}
}
// Empty stub implementation
return nil, nil
}
// handleGetBalance handles the GetBalance method.
func (ws *WalletService) handleGetBalance(c context.T, params json.RawMessage) (result interface{}, err error) {
// Empty stub implementation
return &GetBalanceResult{}, nil
}
// handleGetBudget handles the GetBudget method.
func (ws *WalletService) handleGetBudget(c context.T, params json.RawMessage) (result interface{}, err error) {
// Empty stub implementation
return &GetBudgetResult{}, nil
}
// handleGetInfo handles the GetInfo method.
func (ws *WalletService) handleGetInfo(c context.T, params json.RawMessage) (result interface{}, err error) {
// Empty stub implementation
return &GetInfoResult{}, nil
}
// handleListTransactions handles the ListTransactions method.
func (ws *WalletService) handleListTransactions(c context.T, params json.RawMessage) (result interface{}, err error) {
// Parse parameters
var p ListTransactionsParams
if err = json.Unmarshal(params, &p); err != nil {
return nil, &ResponseError{
Code: "invalid_params",
Message: fmt.Sprintf("failed to parse parameters: %v", err),
}
}
// Empty stub implementation
return &ListTransactionsResult{}, nil
}
// handleLookupInvoice handles the LookupInvoice method.
func (ws *WalletService) handleLookupInvoice(c context.T, params json.RawMessage) (result interface{}, err error) {
// Parse parameters
var p LookupInvoiceParams
if err = json.Unmarshal(params, &p); err != nil {
return nil, &ResponseError{
Code: "invalid_params",
Message: fmt.Sprintf("failed to parse parameters: %v", err),
}
}
// Empty stub implementation
return &LookupInvoiceResult{}, nil
}
// handleMakeHoldInvoice handles the MakeHoldInvoice method.
func (ws *WalletService) handleMakeHoldInvoice(c context.T, params json.RawMessage) (result interface{}, err error) {
// Parse parameters
var p MakeHoldInvoiceParams
if err = json.Unmarshal(params, &p); err != nil {
return nil, &ResponseError{
Code: "invalid_params",
Message: fmt.Sprintf("failed to parse parameters: %v", err),
}
}
// Empty stub implementation
return &MakeInvoiceResult{}, nil
}
// handleMakeInvoice handles the MakeInvoice method.
func (ws *WalletService) handleMakeInvoice(c context.T, params json.RawMessage) (result interface{}, err error) {
// Parse parameters
var p MakeInvoiceParams
if err = json.Unmarshal(params, &p); err != nil {
return nil, &ResponseError{
Code: "invalid_params",
Message: fmt.Sprintf("failed to parse parameters: %v", err),
}
}
// Empty stub implementation
return &MakeInvoiceResult{}, nil
}
// handlePayKeysend handles the PayKeysend method.
func (ws *WalletService) handlePayKeysend(c context.T, params json.RawMessage) (result interface{}, err error) {
// Parse parameters
var p PayKeysendParams
if err = json.Unmarshal(params, &p); err != nil {
return nil, &ResponseError{
Code: "invalid_params",
Message: fmt.Sprintf("failed to parse parameters: %v", err),
}
}
// Empty stub implementation
return &PayKeysendResult{}, nil
}
// handlePayInvoice handles the PayInvoice method.
func (ws *WalletService) handlePayInvoice(c context.T, params json.RawMessage) (result interface{}, err error) {
// Parse parameters
var p PayInvoiceParams
if err = json.Unmarshal(params, &p); err != nil {
return nil, &ResponseError{
Code: "invalid_params",
Message: fmt.Sprintf("failed to parse parameters: %v", err),
}
}
// Empty stub implementation
return &PayInvoiceResult{}, nil
}
// handleSettleHoldInvoice handles the SettleHoldInvoice method.
func (ws *WalletService) handleSettleHoldInvoice(c context.T, params json.RawMessage) (result interface{}, err error) {
// Parse parameters
var p SettleHoldInvoiceParams
if err = json.Unmarshal(params, &p); err != nil {
return nil, &ResponseError{
Code: "invalid_params",
Message: fmt.Sprintf("failed to parse parameters: %v", err),
}
}
// Empty stub implementation
return nil, nil
}
// handleSignMessage handles the SignMessage method.
func (ws *WalletService) handleSignMessage(c context.T, params json.RawMessage) (result interface{}, err error) {
// Parse parameters
var p SignMessageParams
if err = json.Unmarshal(params, &p); err != nil {
return nil, &ResponseError{
Code: "invalid_params",
Message: fmt.Sprintf("failed to parse parameters: %v", err),
}
}
// Empty stub implementation
return &SignMessageResult{}, nil
}

238
pkg/protocol/nwc/wallet.go Normal file
View File

@@ -0,0 +1,238 @@
package nwc
import (
"encoding/json"
"fmt"
"sync"
"orly.dev/pkg/crypto/encryption"
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/encoders/hex"
"orly.dev/pkg/encoders/kind"
"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"
)
// WalletService represents a wallet service that clients can connect to.
type WalletService struct {
mutex sync.Mutex
listener *ws.Listener
walletSecretKey signer.I
walletPublicKey []byte
conversationKey []byte // nip44
handlers map[string]MethodHandler
}
// MethodHandler is a function type for handling wallet service method calls.
type MethodHandler func(
c context.T, params json.RawMessage,
) (result interface{}, err error)
// NewWalletService creates a new WalletService with the given listener and wallet key.
func NewWalletService(
listener *ws.Listener, walletKey signer.I,
) (ws *WalletService, err error) {
pubKey := walletKey.Pub()
ws = &WalletService{
listener: listener,
walletSecretKey: walletKey,
walletPublicKey: pubKey,
handlers: make(map[string]MethodHandler),
}
// Register default method handlers
ws.registerDefaultHandlers()
return
}
// RegisterHandler registers a handler for a specific method.
func (ws *WalletService) RegisterHandler(
method string, handler MethodHandler,
) {
ws.mutex.Lock()
defer ws.mutex.Unlock()
ws.handlers[method] = handler
}
// registerDefaultHandlers registers the default empty stub handlers for all supported methods.
func (ws *WalletService) registerDefaultHandlers() {
// Register handlers for all supported methods
ws.RegisterHandler(string(GetWalletServiceInfo), ws.handleGetWalletServiceInfo)
ws.RegisterHandler(string(CancelHoldInvoice), ws.handleCancelHoldInvoice)
ws.RegisterHandler(string(CreateConnection), ws.handleCreateConnection)
ws.RegisterHandler(string(GetBalance), ws.handleGetBalance)
ws.RegisterHandler(string(GetBudget), ws.handleGetBudget)
ws.RegisterHandler(string(GetInfo), ws.handleGetInfo)
ws.RegisterHandler(string(ListTransactions), ws.handleListTransactions)
ws.RegisterHandler(string(LookupInvoice), ws.handleLookupInvoice)
ws.RegisterHandler(string(MakeHoldInvoice), ws.handleMakeHoldInvoice)
ws.RegisterHandler(string(MakeInvoice), ws.handleMakeInvoice)
ws.RegisterHandler(string(PayKeysend), ws.handlePayKeysend)
ws.RegisterHandler(string(PayInvoice), ws.handlePayInvoice)
ws.RegisterHandler(string(SettleHoldInvoice), ws.handleSettleHoldInvoice)
ws.RegisterHandler(string(SignMessage), ws.handleSignMessage)
}
// HandleRequest processes an incoming wallet request event.
func (ws *WalletService) HandleRequest(c context.T, ev *event.E) (err error) {
// Verify the event is a wallet request
if ev.Kind != kind.WalletRequest {
return fmt.Errorf("invalid event kind: %d", ev.Kind)
}
// Get the client's public key from the event
clientPubKey := ev.Pubkey
// Generate conversation key
var ck []byte
if ck, err = encryption.GenerateConversationKeyWithSigner(
ws.walletSecretKey,
clientPubKey,
); chk.E(err) {
return
}
// Decrypt the content
var content []byte
if content, err = encryption.Decrypt(ev.Content, ck); chk.E(err) {
return
}
// Parse the request
var req Request
if err = json.Unmarshal(content, &req); chk.E(err) {
return
}
// Find the handler for the method
ws.mutex.Lock()
handler, exists := ws.handlers[req.Method]
ws.mutex.Unlock()
var result interface{}
var respErr *ResponseError
if !exists {
respErr = &ResponseError{
Code: "method_not_found",
Message: fmt.Sprintf("method %s not found", req.Method),
}
} else {
// Call the handler
var params json.RawMessage
if req.Params != nil {
var paramsBytes []byte
if paramsBytes, err = json.Marshal(req.Params); chk.E(err) {
return
}
params = paramsBytes
}
result, err = handler(c, params)
if err != nil {
if re, ok := err.(*ResponseError); ok {
respErr = re
} else {
respErr = &ResponseError{
Code: "internal_error",
Message: err.Error(),
}
}
}
}
// Create response
resp := Response{
ResultType: req.Method,
Result: result,
Error: respErr,
}
// Marshal response
var respBytes []byte
if respBytes, err = json.Marshal(resp); chk.E(err) {
return
}
// Encrypt response
var encResp []byte
if encResp, err = encryption.Encrypt(respBytes, ck); chk.E(err) {
return
}
// Create response event
respEv := &event.E{
Content: encResp,
CreatedAt: timestamp.Now(),
Kind: kind.WalletResponse,
Tags: tags.New(
tag.New("p", hex.Enc(clientPubKey)),
tag.New("e", hex.Enc(ev.ID)),
tag.New(EncryptionTag, Nip44V2),
),
}
// Sign the response event
if err = respEv.Sign(ws.walletSecretKey); chk.E(err) {
return
}
// Send the response
_, err = ws.listener.Write(respEv.Marshal(nil))
return
}
// SendNotification sends a notification to a client.
func (ws *WalletService) SendNotification(
c context.T, clientPubKey []byte, notificationType string,
content interface{},
) (err error) {
// Generate conversation key
var ck []byte
if ck, err = encryption.GenerateConversationKeyWithSigner(
ws.walletSecretKey,
clientPubKey,
); chk.E(err) {
return
}
// Marshal content
var contentBytes []byte
if contentBytes, err = json.Marshal(content); chk.E(err) {
return
}
// Encrypt content
var encContent []byte
if encContent, err = encryption.Encrypt(contentBytes, ck); chk.E(err) {
return
}
// Create notification event
notifEv := &event.E{
Content: encContent,
CreatedAt: timestamp.Now(),
Kind: kind.WalletNotification,
Tags: tags.New(
tag.New("p", hex.Enc(clientPubKey)),
tag.New(NotificationTag, []byte(notificationType)),
tag.New(EncryptionTag, Nip44V2),
),
}
// Sign the notification event
if err = notifEv.Sign(ws.walletSecretKey); chk.E(err) {
return
}
// Send the notification
_, err = ws.listener.Write(notifEv.Marshal(nil))
return
}