Skip to content

Commit

Permalink
Merge pull request #101 from elnosh/nut17-wallet
Browse files Browse the repository at this point in the history
NUT-17 websockets client support
  • Loading branch information
elnosh authored Jan 14, 2025
2 parents ee81d0d + ab15ff4 commit 3bf5f9a
Show file tree
Hide file tree
Showing 6 changed files with 518 additions and 28 deletions.
45 changes: 25 additions & 20 deletions cashu/nuts/nut06/nut06.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package nut06

import (
"encoding/json"

"github.com/elnosh/gonuts/cashu/nuts/nut17"
)

type MintInfo struct {
Expand Down Expand Up @@ -78,33 +80,35 @@ type Supported struct {
}

type Nuts struct {
Nut04 NutSetting `json:"4"`
Nut05 NutSetting `json:"5"`
Nut07 Supported `json:"7"`
Nut08 Supported `json:"8"`
Nut09 Supported `json:"9"`
Nut10 Supported `json:"10"`
Nut11 Supported `json:"11"`
Nut12 Supported `json:"12"`
Nut14 Supported `json:"14"`
Nut15 *NutSetting `json:"15,omitempty"`
Nut04 NutSetting `json:"4"`
Nut05 NutSetting `json:"5"`
Nut07 Supported `json:"7"`
Nut08 Supported `json:"8"`
Nut09 Supported `json:"9"`
Nut10 Supported `json:"10"`
Nut11 Supported `json:"11"`
Nut12 Supported `json:"12"`
Nut14 Supported `json:"14"`
Nut15 *NutSetting `json:"15,omitempty"`
Nut17 nut17.InfoSetting `json:"17"`
}

// custom unmarshaller because format to signal support for nut-15 changed.
// So it will first try to Unmarshal to new format and if there is an error
// it will try old format
func (nuts *Nuts) UnmarshalJSON(data []byte) error {
var tempNuts struct {
Nut04 NutSetting `json:"4"`
Nut05 NutSetting `json:"5"`
Nut07 Supported `json:"7"`
Nut08 Supported `json:"8"`
Nut09 Supported `json:"9"`
Nut10 Supported `json:"10"`
Nut11 Supported `json:"11"`
Nut12 Supported `json:"12"`
Nut14 Supported `json:"14"`
Nut15 json.RawMessage `json:"15,omitempty"`
Nut04 NutSetting `json:"4"`
Nut05 NutSetting `json:"5"`
Nut07 Supported `json:"7"`
Nut08 Supported `json:"8"`
Nut09 Supported `json:"9"`
Nut10 Supported `json:"10"`
Nut11 Supported `json:"11"`
Nut12 Supported `json:"12"`
Nut14 Supported `json:"14"`
Nut15 json.RawMessage `json:"15,omitempty"`
Nut17 nut17.InfoSetting `json:"17"`
}

if err := json.Unmarshal(data, &tempNuts); err != nil {
Expand All @@ -120,6 +124,7 @@ func (nuts *Nuts) UnmarshalJSON(data []byte) error {
nuts.Nut11 = tempNuts.Nut11
nuts.Nut12 = tempNuts.Nut12
nuts.Nut14 = tempNuts.Nut14
nuts.Nut17 = tempNuts.Nut17

if err := json.Unmarshal(tempNuts.Nut15, &nuts.Nut15); err != nil {
var nut15Methods []MethodSetting
Expand Down
166 changes: 166 additions & 0 deletions cashu/nuts/nut17/nut17.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package nut17

import (
"encoding/json"
"errors"
)

type SubscriptionKind int

const (
Bolt11MintQuote SubscriptionKind = iota
Bolt11MeltQuote
ProofState
Unknown
)

func (kind SubscriptionKind) String() string {
switch kind {
case Bolt11MintQuote:
return "bolt11_mint_quote"
case Bolt11MeltQuote:
return "bolt11_melt_quote"
case ProofState:
return "proof_state"
default:
return "unknown"
}
}

func StringToKind(kind string) SubscriptionKind {
switch kind {
case "bolt11_mint_quote":
return Bolt11MintQuote
case "bolt11_melt_quote":
return Bolt11MeltQuote
case "proof_state":
return ProofState
}
return Unknown
}

type WsRequest struct {
JsonRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params RequestParams `json:"params"`
Id int `json:"id"`
}

type RequestParams struct {
Kind string `json:"kind"`
SubId string `json:"subId"`
Filters []string `json:"filters"`
}

type WsResponse struct {
JsonRPC string `json:"jsonrpc"`
Result Result `json:"result"`
Id int `json:"id"`
}

func (r *WsResponse) UnmarshalJSON(data []byte) error {
var tempResponse struct {
JsonRPC string `json:"jsonrpc"`
Result *Result `json:"result"`
Id int `json:"id"`
}

if err := json.Unmarshal(data, &tempResponse); err != nil {
return err
}

if tempResponse.Result == nil {
return errors.New("result field not present in WsResponse")
}

r.JsonRPC = tempResponse.JsonRPC
r.Result = *tempResponse.Result
r.Id = tempResponse.Id

return nil
}

type Result struct {
Status string `json:"status"`
SubId string `json:"subId"`
}

type WsNotification struct {
JsonRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params NotificationParams `json:"params"`
}

func (n *WsNotification) UnmarshalJSON(data []byte) error {
var tempNotif struct {
JsonRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params *NotificationParams `json:"params"`
}

if err := json.Unmarshal(data, &tempNotif); err != nil {
return err
}

if tempNotif.Params == nil {
return errors.New("params field not present in WsNotification")
}

n.JsonRPC = tempNotif.JsonRPC
n.Method = tempNotif.Method
n.Params = *tempNotif.Params

return nil
}

type NotificationParams struct {
SubId string `json:"subId"`
Payload json.RawMessage `json:"payload"`
}

type WsError struct {
JsonRPC string `json:"jsonrpc"`
ErrResponse ErrorResponse `json:"error"`
Id int `json:"id"`
}

func (e *WsError) UnmarshalJSON(data []byte) error {
var tempError struct {
JsonRPC string `json:"jsonrpc"`
ErrResponse *ErrorResponse `json:"error"`
Id int `json:"id"`
}

if err := json.Unmarshal(data, &tempError); err != nil {
return err
}

if tempError.ErrResponse == nil {
return errors.New("error field not present in WsError")
}

e.JsonRPC = tempError.JsonRPC
e.ErrResponse = *tempError.ErrResponse
e.Id = tempError.Id

return nil
}

func (e WsError) Error() string {
return e.ErrResponse.Message
}

type ErrorResponse struct {
Code int `json:"code"`
Message string `json:"message"`
}

type InfoSetting struct {
Supported []SupportedMethod `json:"supported"`
}

type SupportedMethod struct {
Method string `json:"method"`
Unit string `json:"unit"`
Commands []string `json:"commands"`
}
64 changes: 62 additions & 2 deletions cmd/nutw/nutw.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,21 @@ import (
"log"
"net/url"
"os"
"os/signal"
"path/filepath"
"slices"
"strconv"
"strings"
"syscall"

"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/elnosh/gonuts/cashu"
"github.com/elnosh/gonuts/cashu/nuts/nut04"
"github.com/elnosh/gonuts/cashu/nuts/nut05"
"github.com/elnosh/gonuts/cashu/nuts/nut11"
"github.com/elnosh/gonuts/cashu/nuts/nut17"
"github.com/elnosh/gonuts/wallet"
"github.com/elnosh/gonuts/wallet/submanager"
"github.com/joho/godotenv"
decodepay "github.com/nbd-wtf/ln-decodepay"
"github.com/urfave/cli/v2"
Expand Down Expand Up @@ -285,8 +290,63 @@ func requestMint(amountStr string) error {
}

fmt.Printf("invoice: %v\n\n", mintResponse.Request)
fmt.Println("after paying the invoice you can redeem the ecash using the --invoice flag")
return nil

subMananger, err := submanager.NewSubscriptionManager(nutw.CurrentMint())
if err != nil {
return err
}
defer subMananger.Close()

errChan := make(chan error)
go subMananger.Run(errChan)

subscription, err := subMananger.Subscribe(nut17.Bolt11MintQuote, []string{mintResponse.Quote})
if err != nil {
return err
}

fmt.Println("checking if invoice gets paid...")

sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, os.Kill, syscall.SIGTERM)

go func() {
select {
case err := <-errChan:
fmt.Printf("error reading from websocket connection: %v\n\n", err)
fmt.Println("after paying the invoice you can redeem the ecash by doing 'nutw mint --invoice [invoice]'")
os.Exit(1)
case <-sigChan:
fmt.Println("\nterminating... after paying the invoice you can also redeem the ecash by doing 'nutw mint --invoice [invoice]'")
os.Exit(0)
}
}()

for {
notification, err := subscription.Read()
if err != nil {
return err
}

var mintQuote nut04.PostMintQuoteBolt11Response
if err := json.Unmarshal(notification.Params.Payload, &mintQuote); err != nil {
return err
}

if mintQuote.State == nut04.Paid {
mintedAmount, err := nutw.MintTokens(mintResponse.Quote)
if err != nil {
return err
}

fmt.Printf("%v sats successfully minted\n", mintedAmount)

if err := subMananger.CloseSubscripton(subscription.SubId()); err != nil {
return err
}
return nil
}
}
}

func mintTokens(paymentRequest string) error {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/fxamacker/cbor/v2 v2.7.0
github.com/golang-migrate/migrate/v4 v4.17.1
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.5.3
github.com/joho/godotenv v1.5.1
github.com/lightningnetwork/lnd v0.18.2-beta
github.com/mattn/go-sqlite3 v1.14.22
Expand Down Expand Up @@ -77,7 +78,6 @@ require (
github.com/google/btree v1.0.1 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
Expand Down
7 changes: 2 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,6 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elnosh/btc-docker-test v0.0.0-20241218205605-f01bfb027b58 h1:BSbbEDaBqmBpbrK43aZKz4IGZ7zb9gHXHaMPAlE79dI=
github.com/elnosh/btc-docker-test v0.0.0-20241218205605-f01bfb027b58/go.mod h1:ZiuureJ3zkNfxqqTKzgOT+Y140uAXtqpUl3LTuwFtIc=
github.com/elnosh/btc-docker-test v0.0.0-20241223164556-146e52a0433b h1:JbZVAqKBVRkvHuZZJsf8MvO+I7HGaVNCMQvp7WMFGqs=
github.com/elnosh/btc-docker-test v0.0.0-20241223164556-146e52a0433b/go.mod h1:4PlP53czOHN+XvjyQZh+zgrzkI7BYFvJajxKK2zquyE=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
Expand Down Expand Up @@ -311,8 +309,9 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
Expand Down Expand Up @@ -808,8 +807,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
Expand Down
Loading

0 comments on commit 3bf5f9a

Please sign in to comment.