From bea04312a9170c979ce9b52aecb63a5fa59ae1c9 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Tue, 12 Dec 2023 18:47:38 +0530 Subject: [PATCH 01/11] feat: add strike backend --- .env.alby | 15 + .env.lnd | 9 + .env.strike | 15 + alby.go | 14 +- config.go | 17 +- echo_handlers.go | 1 + go.mod | 18 +- go.sum | 44 +-- main.go | 6 + strike.go | 484 +++++++++++++++++++++++++++++++ views/backends/strike/index.html | 49 ++++ 11 files changed, 618 insertions(+), 54 deletions(-) create mode 100644 .env.alby create mode 100644 .env.lnd create mode 100644 .env.strike create mode 100644 strike.go create mode 100644 views/backends/strike/index.html diff --git a/.env.alby b/.env.alby new file mode 100644 index 00000000..033ff018 --- /dev/null +++ b/.env.alby @@ -0,0 +1,15 @@ +DATABASE_URI=postgresql://user@localhost:5432/nostrwalletconnect_lnd?sslmode=disable +NOSTR_PRIVKEY= +LN_BACKEND_TYPE=ALBY + +CLIENT_ID= +CLIENT_SECRET= +OAUTH_AUTH_URL=https://getalby.com/oauth +OAUTH_API_URL=https://api.getalby.com +OAUTH_TOKEN_URL=https://api.getalby.com/oauth/token +OAUTH_REDIRECT_URL=http://nwc.example.com:8080/alby/callback + +COOKIE_SECRET=secretsecret +COOKIE_DOMAIN=.example.com +RELAY=wss://relay.getalby.com/v1 +PORT=8080 \ No newline at end of file diff --git a/.env.lnd b/.env.lnd new file mode 100644 index 00000000..86f006f4 --- /dev/null +++ b/.env.lnd @@ -0,0 +1,9 @@ +DATABASE_URI=postgresql://user@localhost:5432/nostrwalletconnect_lnd?sslmode=disable +NOSTR_PRIVKEY= +LND_ADDRESS=rpc.lnd3.regtest.getalby.com:443 +LND_MACAROON_FILE=testnet_lnd3.macaroon +LN_BACKEND_TYPE=LND +COOKIE_SECRET=secretsecret +COOKIE_DOMAIN=.example.com +RELAY=wss://relay.getalby.com/v1 +PORT=8080 \ No newline at end of file diff --git a/.env.strike b/.env.strike new file mode 100644 index 00000000..36807a36 --- /dev/null +++ b/.env.strike @@ -0,0 +1,15 @@ +DATABASE_URI=postgresql://user@localhost:5432/nostrwalletconnect_lnd?sslmode=disable +NOSTR_PRIVKEY= +LN_BACKEND_TYPE=STRIKE + +CLIENT_ID= +CLIENT_SECRET= +OAUTH_AUTH_URL=https://auth.strike.me/connect/authorize +OAUTH_API_URL=https://api.strike.me/v1 +OAUTH_TOKEN_URL=https://auth.strike.me/connect/token +OAUTH_REDIRECT_URL=http://localhost:3000/api/auth/callback/strike + +COOKIE_SECRET=secretsecret +COOKIE_DOMAIN=.example.com +RELAY=wss://relay.getalby.com/v1 +PORT=3000 \ No newline at end of file diff --git a/alby.go b/alby.go index 255bd324..67f55d49 100644 --- a/alby.go +++ b/alby.go @@ -24,8 +24,8 @@ type AlbyOAuthService struct { func NewAlbyOauthService(svc *Service, e *echo.Echo) (result *AlbyOAuthService, err error) { conf := &oauth2.Config{ - ClientID: svc.cfg.AlbyClientId, - ClientSecret: svc.cfg.AlbyClientSecret, + ClientID: svc.cfg.ClientId, + ClientSecret: svc.cfg.ClientSecret, //Todo: do we really need all these permissions? Scopes: []string{"account:read", "payments:send", "invoices:read", "transactions:read", "invoices:create", "balance:read"}, Endpoint: oauth2.Endpoint{ @@ -133,7 +133,7 @@ func (svc *AlbyOAuthService) MakeInvoice(ctx context.Context, senderPubkey strin err = json.NewEncoder(body).Encode(payload) // TODO: move to a shared function - req, err := http.NewRequest("POST", fmt.Sprintf("%s/invoices", svc.cfg.AlbyAPIURL), body) + req, err := http.NewRequest("POST", fmt.Sprintf("%s/invoices", svc.cfg.OAuthAPIURL), body) if err != nil { svc.Logger.WithError(err).Error("Error creating request /invoices") return "", "", err @@ -220,7 +220,7 @@ func (svc *AlbyOAuthService) LookupInvoice(ctx context.Context, senderPubkey str body := bytes.NewBuffer([]byte{}) // TODO: move to a shared function - req, err := http.NewRequest("GET", fmt.Sprintf("%s/invoices/%s", svc.cfg.AlbyAPIURL, paymentHash), body) + req, err := http.NewRequest("GET", fmt.Sprintf("%s/invoices/%s", svc.cfg.OAuthAPIURL, paymentHash), body) if err != nil { svc.Logger.WithError(err).Errorf("Error creating request /invoices/%s", paymentHash) return "", false, err @@ -286,7 +286,7 @@ func (svc *AlbyOAuthService) GetBalance(ctx context.Context, senderPubkey string } client := svc.oauthConf.Client(ctx, tok) - req, err := http.NewRequest("GET", fmt.Sprintf("%s/balance", svc.cfg.AlbyAPIURL), nil) + req, err := http.NewRequest("GET", fmt.Sprintf("%s/balance", svc.cfg.OAuthAPIURL), nil) if err != nil { svc.Logger.WithError(err).Error("Error creating request /balance") return 0, err @@ -359,7 +359,7 @@ func (svc *AlbyOAuthService) SendPaymentSync(ctx context.Context, senderPubkey, } err = json.NewEncoder(body).Encode(payload) - req, err := http.NewRequest("POST", fmt.Sprintf("%s/payments/bolt11", svc.cfg.AlbyAPIURL), body) + req, err := http.NewRequest("POST", fmt.Sprintf("%s/payments/bolt11", svc.cfg.OAuthAPIURL), body) if err != nil { svc.Logger.WithError(err).Error("Error creating request /payments/bolt11") return "", err @@ -434,7 +434,7 @@ func (svc *AlbyOAuthService) CallbackHandler(c echo.Context) error { } client := svc.oauthConf.Client(c.Request().Context(), tok) - req, err := http.NewRequest("GET", fmt.Sprintf("%s/user/me", svc.cfg.AlbyAPIURL), nil) + req, err := http.NewRequest("GET", fmt.Sprintf("%s/user/me", svc.cfg.OAuthAPIURL), nil) if err != nil { svc.Logger.WithError(err).Error("Error creating request /me") return err diff --git a/config.go b/config.go index 0567f780..157280fc 100644 --- a/config.go +++ b/config.go @@ -1,9 +1,10 @@ package main const ( - AlbyBackendType = "ALBY" - LNDBackendType = "LND" - CookieName = "alby_nwc_session" + AlbyBackendType = "ALBY" + LNDBackendType = "LND" + StrikeBackendType = "STRIKE" + CookieName = "alby_nwc_session" ) type Config struct { @@ -17,12 +18,12 @@ type Config struct { LNDAddress string `envconfig:"LND_ADDRESS"` LNDCertFile string `envconfig:"LND_CERT_FILE"` LNDMacaroonFile string `envconfig:"LND_MACAROON_FILE"` - AlbyAPIURL string `envconfig:"ALBY_API_URL" default:"https://api.getalby.com"` - AlbyClientId string `envconfig:"ALBY_CLIENT_ID"` - AlbyClientSecret string `envconfig:"ALBY_CLIENT_SECRET"` + ClientId string `envconfig:"CLIENT_ID"` + ClientSecret string `envconfig:"CLIENT_SECRET"` + OAuthAPIURL string `envconfig:"OAUTH_API_URL"` OAuthRedirectUrl string `envconfig:"OAUTH_REDIRECT_URL"` - OAuthAuthUrl string `envconfig:"OAUTH_AUTH_URL" default:"https://getalby.com/oauth"` - OAuthTokenUrl string `envconfig:"OAUTH_TOKEN_URL" default:"https://api.getalby.com/oauth/token"` + OAuthAuthUrl string `envconfig:"OAUTH_AUTH_URL"` + OAuthTokenUrl string `envconfig:"OAUTH_TOKEN_URL"` Port string `envconfig:"PORT" default:"8080"` DatabaseUri string `envconfig:"DATABASE_URI" default:"nostr-wallet-connect.db"` DatabaseMaxConns int `envconfig:"DATABASE_MAX_CONNS" default:"10"` diff --git a/echo_handlers.go b/echo_handlers.go index fd1bce83..b5b8ac65 100644 --- a/echo_handlers.go +++ b/echo_handlers.go @@ -56,6 +56,7 @@ func (svc *Service) RegisterSharedRoutes(e *echo.Echo) { templates["about.html"] = template.Must(template.ParseFS(embeddedViews, "views/about.html", "views/layout.html")) templates["404.html"] = template.Must(template.ParseFS(embeddedViews, "views/404.html", "views/layout.html")) templates["lnd/index.html"] = template.Must(template.ParseFS(embeddedViews, "views/backends/lnd/index.html", "views/layout.html")) + templates["strike/index.html"] = template.Must(template.ParseFS(embeddedViews, "views/backends/strike/index.html", "views/layout.html")) e.Renderer = &TemplateRegistry{ templates: templates, } diff --git a/go.mod b/go.mod index 665369b7..5e17b3eb 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,14 @@ go 1.20 require ( github.com/davrux/echo-logrus/v4 v4.0.3 + github.com/go-gormigrate/gormigrate/v2 v2.1.1 github.com/gorilla/sessions v1.2.1 github.com/labstack/echo-contrib v0.14.1 github.com/labstack/echo/v4 v4.10.2 github.com/nbd-wtf/go-nostr v0.25.5 github.com/nbd-wtf/ln-decodepay v1.11.1 github.com/stretchr/testify v1.8.2 - golang.org/x/oauth2 v0.4.0 + golang.org/x/oauth2 v0.15.0 google.golang.org/grpc v1.53.0 gopkg.in/DataDog/dd-trace-go.v1 v1.47.0 gopkg.in/macaroon.v2 v2.1.0 @@ -55,7 +56,6 @@ require ( github.com/fergusstrange/embedded-postgres v1.19.0 // indirect github.com/glebarez/go-sqlite v1.20.3 // indirect github.com/go-errors/errors v1.4.2 // indirect - github.com/go-gormigrate/gormigrate/v2 v2.1.1 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect @@ -160,18 +160,18 @@ require ( go.uber.org/zap v1.24.0 // indirect go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect - golang.org/x/crypto v0.9.0 // indirect + golang.org/x/crypto v0.16.0 // indirect golang.org/x/mod v0.8.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/term v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/term v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.6.0 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5 // indirect - google.golang.org/protobuf v1.29.1 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/errgo.v1 v1.0.1 // indirect gopkg.in/macaroon-bakery.v2 v2.3.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect @@ -186,7 +186,6 @@ require ( ) require ( - github.com/SaveTheRbtz/generic-sync-map-go v0.0.0-20220414055132-a37292614db8 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect @@ -199,7 +198,6 @@ require ( github.com/lightningnetwork/lnd v0.15.5-beta.rc2 github.com/mattn/go-sqlite3 v1.14.17 // indirect github.com/sirupsen/logrus v1.9.0 - github.com/valyala/fastjson v1.6.3 // indirect golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect gorm.io/driver/postgres v1.5.2 ) diff --git a/go.sum b/go.sum index a26ad40e..caa0c4de 100644 --- a/go.sum +++ b/go.sum @@ -20,7 +20,7 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v1.15.1 h1:7UGq3QknM33pw5xATlpzeoomNxsacIVvTqTTvbfajmE= +cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= @@ -57,8 +57,6 @@ github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpz github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/SaveTheRbtz/generic-sync-map-go v0.0.0-20220414055132-a37292614db8 h1:Xa6tp8DPDhdV+k23uiTC/GrAYOe4IdyJVKtob4KW3GA= -github.com/SaveTheRbtz/generic-sync-map-go v0.0.0-20220414055132-a37292614db8/go.mod h1:ihkm1viTbO/LOsgdGoFPBSvzqvx7ibvkMzYp3CgtHik= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= @@ -194,20 +192,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davrux/echo-logrus/v4 v4.0.3 h1:V5bM43A+3PNdpiGC2TS8HKAeaUWQph/j8utG7/mwQ5w= github.com/davrux/echo-logrus/v4 v4.0.3/go.mod h1:+1y03d0joOKfwnPN4GSFhh/ViG3newZtYZfAPB6yf+g= -github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/decred/dcrd/lru v1.1.1 h1:kWFDaW0OWx6AD6Ki342c+JPmHbiVdE6rK81pT3fuo/Y= github.com/decred/dcrd/lru v1.1.1/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/denisenkom/go-mssqldb v0.11.0 h1:9rHa233rhdOyrz2GcP9NM+gi2psgJZ4GWDpL/7ND8HI= -github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= @@ -635,8 +629,6 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nbd-wtf/go-nostr v0.13.2 h1:w/TgXbkWqkZQsPRZffPZpvR/uskOSSUCGYhtW6I3xPI= -github.com/nbd-wtf/go-nostr v0.13.2/go.mod h1:qFFTIxh15H5GGN0WsBI/P73DteqsevnhSEW/yk8nEf4= github.com/nbd-wtf/go-nostr v0.25.5 h1:CqjicJePCLwPjBNo9N98UfYFnUGrxc7Ts8zwIZgzzwg= github.com/nbd-wtf/go-nostr v0.25.5/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0= github.com/nbd-wtf/ln-decodepay v1.11.1 h1:MPiT4a4qZ2cKY27Aj0dI8sLFrLz5Ycu72Z3EG1HfPjk= @@ -788,8 +780,6 @@ github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oW github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc= -github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= @@ -922,8 +912,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -934,8 +924,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20221106115401-f9659909a136 h1:Fq7F/w7MAa1KJ5bt2aJ62ihqp9HDcRuyILskkpIAurw= -golang.org/x/exp v0.0.0-20221106115401-f9659909a136/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= @@ -1012,16 +1000,16 @@ golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M= -golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= +golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= +golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1106,13 +1094,13 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1122,8 +1110,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1297,8 +1285,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM= -google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/DataDog/dd-trace-go.v1 v1.47.0 h1:w3mHEgOR1o52mkyCbkTM+El8DG732+Fnug4FAGhIpsk= gopkg.in/DataDog/dd-trace-go.v1 v1.47.0/go.mod h1:aHb6c4hPRANXnB64LDAKyfWotKgfRjlHv23MnahM8AI= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= @@ -1345,8 +1333,6 @@ gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8= gorm.io/driver/sqlserver v1.0.4 h1:V15fszi0XAo7fbx3/cF50ngshDSN4QT0MXpWTylyPTY= gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= -gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU= -gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw= gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= diff --git a/main.go b/main.go index 151b56b2..7d035bfc 100644 --- a/main.go +++ b/main.go @@ -141,6 +141,12 @@ func main() { ctx, _ = signal.NotifyContext(ctx, os.Interrupt) var wg sync.WaitGroup switch cfg.LNBackendType { + case StrikeBackendType: + strikeClient, err := NewStrikeOauthService(svc, e) + if err != nil { + svc.Logger.Fatal(err) + } + svc.lnClient = strikeClient case LNDBackendType: lndClient, err := NewLNDService(ctx, svc, e) if err != nil { diff --git a/strike.go b/strike.go new file mode 100644 index 00000000..55faec41 --- /dev/null +++ b/strike.go @@ -0,0 +1,484 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/labstack/echo-contrib/session" + "github.com/labstack/echo/v4" + "github.com/sirupsen/logrus" + "golang.org/x/oauth2" + "gorm.io/gorm" +) + +type StrikeOAuthService struct { + cfg *Config + oauthConf *oauth2.Config + db *gorm.DB + Logger *logrus.Logger +} + +func NewStrikeOauthService(svc *Service, e *echo.Echo) (result *StrikeOAuthService, err error) { + conf := &oauth2.Config{ + ClientID: svc.cfg.ClientId, + ClientSecret: svc.cfg.ClientSecret, + Scopes: []string{"partner.invoice.read"}, + // Scopes: []string{"partner.account.profile.read", "partner.balances.read", "partner.payment-quote.lightning.create", "partner.payment-quote.execute"}, + Endpoint: oauth2.Endpoint{ + TokenURL: svc.cfg.OAuthTokenUrl, + AuthURL: svc.cfg.OAuthAuthUrl, + AuthStyle: 2, // use HTTP Basic Authorization https://pkg.go.dev/golang.org/x/oauth2#AuthStyle + }, + RedirectURL: svc.cfg.OAuthRedirectUrl, + } + + strikeSvc := &StrikeOAuthService{ + cfg: svc.cfg, + oauthConf: conf, + db: svc.db, + Logger: svc.Logger, + } + + e.GET("/strike/auth", strikeSvc.AuthHandler) + e.GET("/api/auth/callback/strike", strikeSvc.CallbackHandler) + + return strikeSvc, err +} + +func (svc *StrikeOAuthService) FetchUserToken(ctx context.Context, app App) (token *oauth2.Token, err error) { + user := app.User + tok, err := svc.oauthConf.TokenSource(ctx, &oauth2.Token{ + AccessToken: user.AccessToken, + RefreshToken: user.RefreshToken, + Expiry: user.Expiry, + }).Token() + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": app.NostrPubkey, + "appId": app.ID, + "userId": app.User.ID, + }).Errorf("Token error: %v", err) + return nil, err + } + // we always update the user's token for future use + // the oauth library handles the token refreshing + user.AccessToken = tok.AccessToken + user.RefreshToken = tok.RefreshToken + user.Expiry = tok.Expiry // TODO; probably needs some calculation + err = svc.db.Save(&user).Error + if err != nil { + svc.Logger.WithError(err).Error("Error saving user") + return nil, err + } + return tok, nil +} + +func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey string, amount int64, description string, descriptionHash string, expiry int64) (invoice string, paymentHash string, err error) { + // TODO: move to a shared function + app := App{} + err = svc.db.Preload("User").First(&app, &App{ + NostrPubkey: senderPubkey, + }).Error + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "description": description, + "descriptionHash": descriptionHash, + "expiry": expiry, + }).Errorf("App not found: %v", err) + return "", "", err + } + + // amount provided in msat, but Alby API currently only supports sats. Will get truncated to a whole sat value + var amountSat int64 = amount / 1000 + // make sure amount is not converted to 0 + if amount > 0 && amountSat == 0 { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "description": description, + "descriptionHash": descriptionHash, + "expiry": expiry, + }).Errorf("amount must be 1000 msat or greater") + return "", "", errors.New("amount must be 1000 msat or greater") + } + + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "description": description, + "descriptionHash": descriptionHash, + "expiry": expiry, + "appId": app.ID, + "userId": app.User.ID, + }).Info("Processing make invoice request") + tok, err := svc.FetchUserToken(ctx, app) + if err != nil { + return "", "", err + } + client := svc.oauthConf.Client(ctx, tok) + + body := bytes.NewBuffer([]byte{}) + payload := &MakeInvoiceRequest{ + Amount: amountSat, + Description: description, + DescriptionHash: descriptionHash, + // TODO: support expiry + } + err = json.NewEncoder(body).Encode(payload) + + // TODO: move to a shared function + req, err := http.NewRequest("POST", fmt.Sprintf("%s/invoices", svc.cfg.OAuthAPIURL), body) + if err != nil { + svc.Logger.WithError(err).Error("Error creating request /invoices") + return "", "", err + } + + req.Header.Set("User-Agent", "NWC") + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "description": description, + "descriptionHash": descriptionHash, + "expiry": expiry, + "appId": app.ID, + "userId": app.User.ID, + }).Errorf("Failed to make invoice: %v", err) + return "", "", err + } + + if resp.StatusCode < 300 { + responsePayload := &MakeInvoiceResponse{} + err = json.NewDecoder(resp.Body).Decode(responsePayload) + if err != nil { + return "", "", err + } + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "description": description, + "descriptionHash": descriptionHash, + "expiry": expiry, + "appId": app.ID, + "userId": app.User.ID, + "paymentRequest": responsePayload.PaymentRequest, + "paymentHash": responsePayload.PaymentHash, + }).Info("Make invoice successful") + return responsePayload.PaymentRequest, responsePayload.PaymentHash, nil + } + + errorPayload := &ErrorResponse{} + err = json.NewDecoder(resp.Body).Decode(errorPayload) + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "description": description, + "descriptionHash": descriptionHash, + "expiry": expiry, + "appId": app.ID, + "userId": app.User.ID, + "APIHttpStatus": resp.StatusCode, + }).Errorf("Make invoice failed %s", string(errorPayload.Message)) + return "", "", errors.New(errorPayload.Message) +} + +func (svc *StrikeOAuthService) LookupInvoice(ctx context.Context, senderPubkey string, paymentHash string) (invoice string, paid bool, err error) { + // TODO: move to a shared function + app := App{} + err = svc.db.Preload("User").First(&app, &App{ + NostrPubkey: senderPubkey, + }).Error + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "paymentHash": paymentHash, + }).Errorf("App not found: %v", err) + return "", false, err + } + + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "paymentHash": paymentHash, + "appId": app.ID, + "userId": app.User.ID, + }).Info("Processing lookup invoice request") + tok, err := svc.FetchUserToken(ctx, app) + if err != nil { + return "", false, err + } + client := svc.oauthConf.Client(ctx, tok) + + body := bytes.NewBuffer([]byte{}) + + // TODO: move to a shared function + req, err := http.NewRequest("GET", fmt.Sprintf("%s/invoices/%s", svc.cfg.OAuthAPIURL, paymentHash), body) + if err != nil { + svc.Logger.WithError(err).Errorf("Error creating request /invoices/%s", paymentHash) + return "", false, err + } + + req.Header.Set("User-Agent", "NWC") + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "paymentHash": paymentHash, + "appId": app.ID, + "userId": app.User.ID, + }).Errorf("Failed to lookup invoice: %v", err) + return "", false, err + } + + if resp.StatusCode < 300 { + responsePayload := &LookupInvoiceResponse{} + err = json.NewDecoder(resp.Body).Decode(responsePayload) + if err != nil { + return "", false, err + } + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "paymentHash": paymentHash, + "appId": app.ID, + "userId": app.User.ID, + "paymentRequest": responsePayload.PaymentRequest, + "settled": responsePayload.Settled, + }).Info("Lookup invoice successful") + return responsePayload.PaymentRequest, responsePayload.Settled, nil + } + + errorPayload := &ErrorResponse{} + err = json.NewDecoder(resp.Body).Decode(errorPayload) + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "paymentHash": paymentHash, + "appId": app.ID, + "userId": app.User.ID, + "APIHttpStatus": resp.StatusCode, + }).Errorf("Lookup invoice failed %s", string(errorPayload.Message)) + return "", false, errors.New(errorPayload.Message) +} + +func (svc *StrikeOAuthService) GetBalance(ctx context.Context, senderPubkey string) (balance int64, err error) { + app := App{} + err = svc.db.Preload("User").First(&app, &App{ + NostrPubkey: senderPubkey, + }).Error + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + }).Errorf("App not found: %v", err) + return 0, err + } + tok, err := svc.FetchUserToken(ctx, app) + if err != nil { + return 0, err + } + client := svc.oauthConf.Client(ctx, tok) + + req, err := http.NewRequest("GET", fmt.Sprintf("%s/balance", svc.cfg.OAuthAPIURL), nil) + if err != nil { + svc.Logger.WithError(err).Error("Error creating request /balance") + return 0, err + } + + req.Header.Set("User-Agent", "NWC") + + resp, err := client.Do(req) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "appId": app.ID, + "userId": app.User.ID, + }).Errorf("Failed to fetch balance: %v", err) + return 0, err + } + + if resp.StatusCode < 300 { + responsePayload := &BalanceResponse{} + err = json.NewDecoder(resp.Body).Decode(responsePayload) + if err != nil { + return 0, err + } + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "appId": app.ID, + "userId": app.User.ID, + }).Info("Balance fetch successful") + return int64(responsePayload.Balance), nil + } + + errorPayload := &ErrorResponse{} + err = json.NewDecoder(resp.Body).Decode(errorPayload) + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "appId": app.ID, + "userId": app.User.ID, + "APIHttpStatus": resp.StatusCode, + }).Errorf("Balance fetch failed %s", string(errorPayload.Message)) + return 0, errors.New(errorPayload.Message) +} + +func (svc *StrikeOAuthService) SendPaymentSync(ctx context.Context, senderPubkey, payReq string) (preimage string, err error) { + app := App{} + err = svc.db.Preload("User").First(&app, &App{ + NostrPubkey: senderPubkey, + }).Error + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "bolt11": payReq, + }).Errorf("App not found: %v", err) + return "", err + } + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "bolt11": payReq, + "appId": app.ID, + "userId": app.User.ID, + }).Info("Processing payment request") + tok, err := svc.FetchUserToken(ctx, app) + if err != nil { + return "", err + } + client := svc.oauthConf.Client(ctx, tok) + + body := bytes.NewBuffer([]byte{}) + payload := &PayRequest{ + Invoice: payReq, + } + err = json.NewEncoder(body).Encode(payload) + + req, err := http.NewRequest("POST", fmt.Sprintf("%s/payments/bolt11", svc.cfg.OAuthAPIURL), body) + if err != nil { + svc.Logger.WithError(err).Error("Error creating request /payments/bolt11") + return "", err + } + + req.Header.Set("User-Agent", "NWC") + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "bolt11": payReq, + "appId": app.ID, + "userId": app.User.ID, + }).Errorf("Failed to pay invoice: %v", err) + return "", err + } + + if resp.StatusCode < 300 { + responsePayload := &PayResponse{} + err = json.NewDecoder(resp.Body).Decode(responsePayload) + if err != nil { + return "", err + } + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "bolt11": payReq, + "appId": app.ID, + "userId": app.User.ID, + "paymentHash": responsePayload.PaymentHash, + }).Info("Payment successful") + return responsePayload.Preimage, nil + } + + errorPayload := &ErrorResponse{} + err = json.NewDecoder(resp.Body).Decode(errorPayload) + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "bolt11": payReq, + "appId": app.ID, + "userId": app.User.ID, + "APIHttpStatus": resp.StatusCode, + }).Errorf("Payment failed %s", string(errorPayload.Message)) + return "", errors.New(errorPayload.Message) +} + +func (svc *StrikeOAuthService) AuthHandler(c echo.Context) error { + appName := c.QueryParam("c") // c - for client + // clear current session + sess, _ := session.Get(CookieName, c) + if sess.Values["user_id"] != nil { + delete(sess.Values, "user_id") + sess.Options.MaxAge = 0 + sess.Options.SameSite = http.SameSiteLaxMode + if svc.cfg.CookieDomain != "" { + sess.Options.Domain = svc.cfg.CookieDomain + } + sess.Save(c.Request(), c.Response()) + } + + cv := oauth2.GenerateVerifier() + + url := svc.oauthConf.AuthCodeURL( + appName, + oauth2.SetAuthURLParam("code_challenge_method", "S256"), + oauth2.SetAuthURLParam("code_challenge", oauth2.S256ChallengeFromVerifier(cv)), + ) + + return c.Redirect(302, url) +} + +func (svc *StrikeOAuthService) CallbackHandler(c echo.Context) error { + fmt.Println("heyyy") + code := c.QueryParam("code") + fmt.Println(code) + tok, err := svc.oauthConf.Exchange(c.Request().Context(), code) + if err != nil { + svc.Logger.WithError(err).Error("Failed to exchange token") + return err + } + client := svc.oauthConf.Client(c.Request().Context(), tok) + + req, err := http.NewRequest("GET", fmt.Sprintf("%s/user/me", svc.cfg.OAuthAPIURL), nil) + if err != nil { + svc.Logger.WithError(err).Error("Error creating request /me") + return err + } + + req.Header.Set("User-Agent", "NWC") + + res, err := client.Do(req) + if err != nil { + svc.Logger.WithError(err).Error("Failed to fetch /me") + return err + } + me := AlbyMe{} + err = json.NewDecoder(res.Body).Decode(&me) + if err != nil { + svc.Logger.WithError(err).Error("Failed to decode API response") + return err + } + + user := User{} + svc.db.FirstOrInit(&user, User{AlbyIdentifier: me.Identifier}) + user.AccessToken = tok.AccessToken + user.RefreshToken = tok.RefreshToken + user.Expiry = tok.Expiry // TODO; probably needs some calculation + user.Email = me.Email + user.LightningAddress = me.LightningAddress + svc.db.Save(&user) + + sess, _ := session.Get(CookieName, c) + sess.Options.MaxAge = 0 + sess.Options.SameSite = http.SameSiteLaxMode + if svc.cfg.CookieDomain != "" { + sess.Options.Domain = svc.cfg.CookieDomain + } + sess.Values["user_id"] = user.ID + sess.Save(c.Request(), c.Response()) + return c.Redirect(302, "/") +} diff --git a/views/backends/strike/index.html b/views/backends/strike/index.html new file mode 100644 index 00000000..595726d2 --- /dev/null +++ b/views/backends/strike/index.html @@ -0,0 +1,49 @@ +{{define "body"}} + +
+ Nostr Wallet Connect logo + +

+ Nostr Wallet Connect +

+ +

+ Securely connect your Strike Account to Nostr clients and applications. +

+ +

+ + + Log in with Strike Account + +

+ +

+ How does it work? +

+
+ + + +{{end}} From e54022c983c823cbc888ed5584f0d74da8e94dc5 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Thu, 14 Dec 2023 21:18:45 +0530 Subject: [PATCH 02/11] feat: add support for pay_invoice and get_balance --- models.go | 33 ++++++ strike.go | 335 +++++++++++++++++------------------------------------- 2 files changed, 137 insertions(+), 231 deletions(-) diff --git a/models.go b/models.go index aa5d3352..7ae61e9f 100644 --- a/models.go +++ b/models.go @@ -121,17 +121,40 @@ type PayRequest struct { Invoice string `json:"invoice"` } +type StrikePayRequest struct { + LnInvoice string `json:"lnInvoice"` + SourceCurrency string `json:"sourceCurrency"` +} + type BalanceResponse struct { Balance int64 `json:"balance"` Currency string `json:"currency"` Unit string `json:"unit"` } +type StrikeBalanceResponse struct { + Currency string `json:"currency"` + Outgoing string `json:"outgoing"` + Available string `json:"available"` + Total string `json:"total"` +} + type PayResponse struct { Preimage string `json:"payment_preimage"` PaymentHash string `json:"payment_hash"` } +type StrikePaymentQuoteResponse struct { + PaymentQuoteId string `json:"paymentQuoteId"` +} + +type StrikePaymentResponse struct { + PaymentId string `json:"paymentId"` + State string `json:"state"` + Completed string `json:"completed"` + Delivered string `json:"delivered"` +} + type MakeInvoiceRequest struct { Amount int64 `json:"amount"` Description string `json:"description"` @@ -154,6 +177,16 @@ type ErrorResponse struct { Message string `json:"message"` } +type StrikeError struct { + Status bool `json:"status"` + Code int `json:"code"` + Message string `json:"message"` +} + +type StrikeErrorResponse struct { + Data StrikeError `json:"data"` +} + type Identity struct { gorm.Model Privkey string diff --git a/strike.go b/strike.go index 55faec41..e1599737 100644 --- a/strike.go +++ b/strike.go @@ -6,8 +6,12 @@ import ( "encoding/json" "errors" "fmt" + "io" + "math" "net/http" + "strconv" + "github.com/golang-jwt/jwt" "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" "github.com/sirupsen/logrus" @@ -26,8 +30,7 @@ func NewStrikeOauthService(svc *Service, e *echo.Echo) (result *StrikeOAuthServi conf := &oauth2.Config{ ClientID: svc.cfg.ClientId, ClientSecret: svc.cfg.ClientSecret, - Scopes: []string{"partner.invoice.read"}, - // Scopes: []string{"partner.account.profile.read", "partner.balances.read", "partner.payment-quote.lightning.create", "partner.payment-quote.execute"}, + Scopes: []string{"offline_access", "partner.account.profile.read", "partner.balances.read", "partner.payment-quote.lightning.create", "partner.payment-quote.execute"}, Endpoint: oauth2.Endpoint{ TokenURL: svc.cfg.OAuthTokenUrl, AuthURL: svc.cfg.OAuthAuthUrl, @@ -44,7 +47,7 @@ func NewStrikeOauthService(svc *Service, e *echo.Echo) (result *StrikeOAuthServi } e.GET("/strike/auth", strikeSvc.AuthHandler) - e.GET("/api/auth/callback/strike", strikeSvc.CallbackHandler) + e.GET("/strike/callback", strikeSvc.CallbackHandler) return strikeSvc, err } @@ -56,6 +59,7 @@ func (svc *StrikeOAuthService) FetchUserToken(ctx context.Context, app App) (tok RefreshToken: user.RefreshToken, Expiry: user.Expiry, }).Token() + // TODO: Parse token for id, if implementing get_info if err != nil { svc.Logger.WithFields(logrus.Fields{ "senderPubkey": app.NostrPubkey, @@ -77,196 +81,12 @@ func (svc *StrikeOAuthService) FetchUserToken(ctx context.Context, app App) (tok return tok, nil } -func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey string, amount int64, description string, descriptionHash string, expiry int64) (invoice string, paymentHash string, err error) { - // TODO: move to a shared function - app := App{} - err = svc.db.Preload("User").First(&app, &App{ - NostrPubkey: senderPubkey, - }).Error - if err != nil { - svc.Logger.WithFields(logrus.Fields{ - "senderPubkey": senderPubkey, - "amount": amount, - "description": description, - "descriptionHash": descriptionHash, - "expiry": expiry, - }).Errorf("App not found: %v", err) - return "", "", err - } - - // amount provided in msat, but Alby API currently only supports sats. Will get truncated to a whole sat value - var amountSat int64 = amount / 1000 - // make sure amount is not converted to 0 - if amount > 0 && amountSat == 0 { - svc.Logger.WithFields(logrus.Fields{ - "senderPubkey": senderPubkey, - "amount": amount, - "description": description, - "descriptionHash": descriptionHash, - "expiry": expiry, - }).Errorf("amount must be 1000 msat or greater") - return "", "", errors.New("amount must be 1000 msat or greater") - } - - svc.Logger.WithFields(logrus.Fields{ - "senderPubkey": senderPubkey, - "amount": amount, - "description": description, - "descriptionHash": descriptionHash, - "expiry": expiry, - "appId": app.ID, - "userId": app.User.ID, - }).Info("Processing make invoice request") - tok, err := svc.FetchUserToken(ctx, app) - if err != nil { - return "", "", err - } - client := svc.oauthConf.Client(ctx, tok) - - body := bytes.NewBuffer([]byte{}) - payload := &MakeInvoiceRequest{ - Amount: amountSat, - Description: description, - DescriptionHash: descriptionHash, - // TODO: support expiry - } - err = json.NewEncoder(body).Encode(payload) - - // TODO: move to a shared function - req, err := http.NewRequest("POST", fmt.Sprintf("%s/invoices", svc.cfg.OAuthAPIURL), body) - if err != nil { - svc.Logger.WithError(err).Error("Error creating request /invoices") - return "", "", err - } - - req.Header.Set("User-Agent", "NWC") - req.Header.Set("Content-Type", "application/json") - - resp, err := client.Do(req) - if err != nil { - svc.Logger.WithFields(logrus.Fields{ - "senderPubkey": senderPubkey, - "amount": amount, - "description": description, - "descriptionHash": descriptionHash, - "expiry": expiry, - "appId": app.ID, - "userId": app.User.ID, - }).Errorf("Failed to make invoice: %v", err) - return "", "", err - } - - if resp.StatusCode < 300 { - responsePayload := &MakeInvoiceResponse{} - err = json.NewDecoder(resp.Body).Decode(responsePayload) - if err != nil { - return "", "", err - } - svc.Logger.WithFields(logrus.Fields{ - "senderPubkey": senderPubkey, - "amount": amount, - "description": description, - "descriptionHash": descriptionHash, - "expiry": expiry, - "appId": app.ID, - "userId": app.User.ID, - "paymentRequest": responsePayload.PaymentRequest, - "paymentHash": responsePayload.PaymentHash, - }).Info("Make invoice successful") - return responsePayload.PaymentRequest, responsePayload.PaymentHash, nil - } - - errorPayload := &ErrorResponse{} - err = json.NewDecoder(resp.Body).Decode(errorPayload) - svc.Logger.WithFields(logrus.Fields{ - "senderPubkey": senderPubkey, - "amount": amount, - "description": description, - "descriptionHash": descriptionHash, - "expiry": expiry, - "appId": app.ID, - "userId": app.User.ID, - "APIHttpStatus": resp.StatusCode, - }).Errorf("Make invoice failed %s", string(errorPayload.Message)) - return "", "", errors.New(errorPayload.Message) -} - func (svc *StrikeOAuthService) LookupInvoice(ctx context.Context, senderPubkey string, paymentHash string) (invoice string, paid bool, err error) { - // TODO: move to a shared function - app := App{} - err = svc.db.Preload("User").First(&app, &App{ - NostrPubkey: senderPubkey, - }).Error - if err != nil { - svc.Logger.WithFields(logrus.Fields{ - "senderPubkey": senderPubkey, - "paymentHash": paymentHash, - }).Errorf("App not found: %v", err) - return "", false, err - } - - svc.Logger.WithFields(logrus.Fields{ - "senderPubkey": senderPubkey, - "paymentHash": paymentHash, - "appId": app.ID, - "userId": app.User.ID, - }).Info("Processing lookup invoice request") - tok, err := svc.FetchUserToken(ctx, app) - if err != nil { - return "", false, err - } - client := svc.oauthConf.Client(ctx, tok) - - body := bytes.NewBuffer([]byte{}) - - // TODO: move to a shared function - req, err := http.NewRequest("GET", fmt.Sprintf("%s/invoices/%s", svc.cfg.OAuthAPIURL, paymentHash), body) - if err != nil { - svc.Logger.WithError(err).Errorf("Error creating request /invoices/%s", paymentHash) - return "", false, err - } - - req.Header.Set("User-Agent", "NWC") - req.Header.Set("Content-Type", "application/json") - - resp, err := client.Do(req) - if err != nil { - svc.Logger.WithFields(logrus.Fields{ - "senderPubkey": senderPubkey, - "paymentHash": paymentHash, - "appId": app.ID, - "userId": app.User.ID, - }).Errorf("Failed to lookup invoice: %v", err) - return "", false, err - } - - if resp.StatusCode < 300 { - responsePayload := &LookupInvoiceResponse{} - err = json.NewDecoder(resp.Body).Decode(responsePayload) - if err != nil { - return "", false, err - } - svc.Logger.WithFields(logrus.Fields{ - "senderPubkey": senderPubkey, - "paymentHash": paymentHash, - "appId": app.ID, - "userId": app.User.ID, - "paymentRequest": responsePayload.PaymentRequest, - "settled": responsePayload.Settled, - }).Info("Lookup invoice successful") - return responsePayload.PaymentRequest, responsePayload.Settled, nil - } + return "", false, errors.New("not implemented") +} - errorPayload := &ErrorResponse{} - err = json.NewDecoder(resp.Body).Decode(errorPayload) - svc.Logger.WithFields(logrus.Fields{ - "senderPubkey": senderPubkey, - "paymentHash": paymentHash, - "appId": app.ID, - "userId": app.User.ID, - "APIHttpStatus": resp.StatusCode, - }).Errorf("Lookup invoice failed %s", string(errorPayload.Message)) - return "", false, errors.New(errorPayload.Message) +func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey string, amount int64, description string, descriptionHash string, expiry int64) (invoice string, paymentHash string, err error) { + return "", "", errors.New("not implemented") } func (svc *StrikeOAuthService) GetBalance(ctx context.Context, senderPubkey string) (balance int64, err error) { @@ -286,9 +106,9 @@ func (svc *StrikeOAuthService) GetBalance(ctx context.Context, senderPubkey stri } client := svc.oauthConf.Client(ctx, tok) - req, err := http.NewRequest("GET", fmt.Sprintf("%s/balance", svc.cfg.OAuthAPIURL), nil) + req, err := http.NewRequest("GET", fmt.Sprintf("%s/balances", svc.cfg.OAuthAPIURL), nil) if err != nil { - svc.Logger.WithError(err).Error("Error creating request /balance") + svc.Logger.WithError(err).Error("Error creating request /balances") return 0, err } @@ -305,28 +125,36 @@ func (svc *StrikeOAuthService) GetBalance(ctx context.Context, senderPubkey stri } if resp.StatusCode < 300 { - responsePayload := &BalanceResponse{} - err = json.NewDecoder(resp.Body).Decode(responsePayload) + var responsePayload []StrikeBalanceResponse + responseBody, _ := io.ReadAll(resp.Body) + err = json.Unmarshal([]byte(responseBody), &responsePayload) if err != nil { return 0, err } + // Will this always exist? + for _, balanceResp := range responsePayload { + if balanceResp.Currency == "BTC" { + available, _ := strconv.ParseFloat(balanceResp.Available, 64) + balance = int64(available * math.Pow10(8)) + } + } svc.Logger.WithFields(logrus.Fields{ "senderPubkey": senderPubkey, "appId": app.ID, "userId": app.User.ID, }).Info("Balance fetch successful") - return int64(responsePayload.Balance), nil + return balance, nil } - errorPayload := &ErrorResponse{} + errorPayload := &StrikeErrorResponse{} err = json.NewDecoder(resp.Body).Decode(errorPayload) svc.Logger.WithFields(logrus.Fields{ "senderPubkey": senderPubkey, "appId": app.ID, "userId": app.User.ID, "APIHttpStatus": resp.StatusCode, - }).Errorf("Balance fetch failed %s", string(errorPayload.Message)) - return 0, errors.New(errorPayload.Message) + }).Errorf("Balance fetch failed %s", string(errorPayload.Data.Message)) + return 0, errors.New(errorPayload.Data.Message) } func (svc *StrikeOAuthService) SendPaymentSync(ctx context.Context, senderPubkey, payReq string) (preimage string, err error) { @@ -354,14 +182,15 @@ func (svc *StrikeOAuthService) SendPaymentSync(ctx context.Context, senderPubkey client := svc.oauthConf.Client(ctx, tok) body := bytes.NewBuffer([]byte{}) - payload := &PayRequest{ - Invoice: payReq, + payload := &StrikePayRequest{ + LnInvoice: payReq, + SourceCurrency: "BTC", } err = json.NewEncoder(body).Encode(payload) - req, err := http.NewRequest("POST", fmt.Sprintf("%s/payments/bolt11", svc.cfg.OAuthAPIURL), body) + req, err := http.NewRequest("POST", fmt.Sprintf("%s/payment-quotes/lightning", svc.cfg.OAuthAPIURL), body) if err != nil { - svc.Logger.WithError(err).Error("Error creating request /payments/bolt11") + svc.Logger.WithError(err).Error("Error creating request /payment-quotes/lightning") return "", err } @@ -369,6 +198,45 @@ func (svc *StrikeOAuthService) SendPaymentSync(ctx context.Context, senderPubkey req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "bolt11": payReq, + "appId": app.ID, + "userId": app.User.ID, + }).Errorf("Failed to create quote: %v", err) + return "", err + } + + if resp.StatusCode >= 300 { + errorPayload := &StrikeErrorResponse{} + err = json.NewDecoder(resp.Body).Decode(errorPayload) + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "bolt11": payReq, + "appId": app.ID, + "userId": app.User.ID, + "APIHttpStatus": resp.StatusCode, + }).Errorf("Payment failed %s", string(errorPayload.Data.Message)) + return "", errors.New(errorPayload.Data.Message) + } + + responsePayload := &StrikePaymentQuoteResponse{} + err = json.NewDecoder(resp.Body).Decode(responsePayload) + if err != nil { + return "", err + } + + req, err = http.NewRequest("PATCH", fmt.Sprintf("%s/payment-quotes/%s/execute", svc.cfg.OAuthAPIURL, responsePayload.PaymentQuoteId), nil) + if err != nil { + svc.Logger.WithError(err).Errorf("Error creating request /payment-quotes/%s/execute", responsePayload.PaymentQuoteId) + return "", err + } + + req.Header.Set("User-Agent", "NWC") + req.Header.Set("Content-Type", "application/json") + + resp, err = client.Do(req) if err != nil { svc.Logger.WithFields(logrus.Fields{ "senderPubkey": senderPubkey, @@ -380,7 +248,7 @@ func (svc *StrikeOAuthService) SendPaymentSync(ctx context.Context, senderPubkey } if resp.StatusCode < 300 { - responsePayload := &PayResponse{} + responsePayload := &StrikePaymentResponse{} err = json.NewDecoder(resp.Body).Decode(responsePayload) if err != nil { return "", err @@ -390,12 +258,13 @@ func (svc *StrikeOAuthService) SendPaymentSync(ctx context.Context, senderPubkey "bolt11": payReq, "appId": app.ID, "userId": app.User.ID, - "paymentHash": responsePayload.PaymentHash, + "paymentId": responsePayload.PaymentId, }).Info("Payment successful") - return responsePayload.Preimage, nil + // What to return here? + return "preimage", nil } - errorPayload := &ErrorResponse{} + errorPayload := &StrikeErrorResponse{} err = json.NewDecoder(resp.Body).Decode(errorPayload) svc.Logger.WithFields(logrus.Fields{ "senderPubkey": senderPubkey, @@ -403,8 +272,8 @@ func (svc *StrikeOAuthService) SendPaymentSync(ctx context.Context, senderPubkey "appId": app.ID, "userId": app.User.ID, "APIHttpStatus": resp.StatusCode, - }).Errorf("Payment failed %s", string(errorPayload.Message)) - return "", errors.New(errorPayload.Message) + }).Errorf("Payment failed %s", string(errorPayload.Data.Message)) + return "", errors.New(errorPayload.Data.Message) } func (svc *StrikeOAuthService) AuthHandler(c echo.Context) error { @@ -418,11 +287,13 @@ func (svc *StrikeOAuthService) AuthHandler(c echo.Context) error { if svc.cfg.CookieDomain != "" { sess.Options.Domain = svc.cfg.CookieDomain } - sess.Save(c.Request(), c.Response()) } cv := oauth2.GenerateVerifier() + sess.Values["code_verifier"] = cv + sess.Save(c.Request(), c.Response()) + url := svc.oauthConf.AuthCodeURL( appName, oauth2.SetAuthURLParam("code_challenge_method", "S256"), @@ -433,46 +304,48 @@ func (svc *StrikeOAuthService) AuthHandler(c echo.Context) error { } func (svc *StrikeOAuthService) CallbackHandler(c echo.Context) error { - fmt.Println("heyyy") code := c.QueryParam("code") - fmt.Println(code) - tok, err := svc.oauthConf.Exchange(c.Request().Context(), code) + sess, _ := session.Get(CookieName, c) + cv, ok := sess.Values["code_verifier"].(string) + if !ok { + err := errors.New("Code verifier not found in session") + svc.Logger.WithError(err) + return err + } + tok, err := svc.oauthConf.Exchange( + c.Request().Context(), + code, + oauth2.SetAuthURLParam("code_verifier", cv), + oauth2.SetAuthURLParam("redirect_uri", svc.cfg.OAuthRedirectUrl), + ) if err != nil { svc.Logger.WithError(err).Error("Failed to exchange token") return err } - client := svc.oauthConf.Client(c.Request().Context(), tok) - req, err := http.NewRequest("GET", fmt.Sprintf("%s/user/me", svc.cfg.OAuthAPIURL), nil) + token, _, err := new(jwt.Parser).ParseUnverified(tok.AccessToken, jwt.MapClaims{}) if err != nil { - svc.Logger.WithError(err).Error("Error creating request /me") + err := errors.New("Error parsing token") + svc.Logger.WithError(err) return err } - req.Header.Set("User-Agent", "NWC") - - res, err := client.Do(req) - if err != nil { - svc.Logger.WithError(err).Error("Failed to fetch /me") - return err - } - me := AlbyMe{} - err = json.NewDecoder(res.Body).Decode(&me) - if err != nil { - svc.Logger.WithError(err).Error("Failed to decode API response") + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + err := errors.New("Error parsing token claims") + svc.Logger.WithError(err) return err } user := User{} - svc.db.FirstOrInit(&user, User{AlbyIdentifier: me.Identifier}) + // Should we change to NodeIdentifier? + svc.db.FirstOrInit(&user, User{AlbyIdentifier: claims["sub"].(string)}) user.AccessToken = tok.AccessToken user.RefreshToken = tok.RefreshToken user.Expiry = tok.Expiry // TODO; probably needs some calculation - user.Email = me.Email - user.LightningAddress = me.LightningAddress + user.Email = claims["email"].(string) svc.db.Save(&user) - sess, _ := session.Get(CookieName, c) sess.Options.MaxAge = 0 sess.Options.SameSite = http.SameSiteLaxMode if svc.cfg.CookieDomain != "" { From 8238fb8a7e5304921d17a4a66793730bb8347d0b Mon Sep 17 00:00:00 2001 From: im-adithya Date: Thu, 14 Dec 2023 21:25:13 +0530 Subject: [PATCH 03/11] chore: update README --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 1916457f..c7458d2e 100644 --- a/README.md +++ b/README.md @@ -175,3 +175,24 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light ❌ `multi_pay_invoice (TBC)` ❌ `multi_pay_keysend (TBC)` + +### Strike OAuth API + +✅ `get_balance` + +⚠️ `pay_invoice` +- ⚠️ preimage in response missing + +❌ `get_info` + +❌ `pay_keysend` + +❌ `make_invoice` + +❌ `lookup_invoice` + +❌ `list_transactions` + +❌ `multi_pay_invoice (TBC)` + +❌ `multi_pay_keysend (TBC)` From 5aa855719a5bce35a6ccc5d1bf24a6dbc53e7171 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Thu, 14 Dec 2023 21:33:04 +0530 Subject: [PATCH 04/11] chore: fixes --- alby.go | 2 +- strike.go | 24 ++++++++++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/alby.go b/alby.go index f4f24a7d..f29ec225 100644 --- a/alby.go +++ b/alby.go @@ -474,7 +474,7 @@ func (svc *AlbyOAuthService) SendKeysend(ctx context.Context, senderPubkey strin err = json.NewEncoder(body).Encode(payload) // here we don't use the preimage from params - req, err := http.NewRequest("POST", fmt.Sprintf("%s/payments/keysend", svc.cfg.AlbyAPIURL), body) + req, err := http.NewRequest("POST", fmt.Sprintf("%s/payments/keysend", svc.cfg.OAuthAPIURL), body) if err != nil { svc.Logger.WithError(err).Error("Error creating request /payments/keysend") return "", err diff --git a/strike.go b/strike.go index e1599737..6fe6fb2f 100644 --- a/strike.go +++ b/strike.go @@ -30,7 +30,7 @@ func NewStrikeOauthService(svc *Service, e *echo.Echo) (result *StrikeOAuthServi conf := &oauth2.Config{ ClientID: svc.cfg.ClientId, ClientSecret: svc.cfg.ClientSecret, - Scopes: []string{"offline_access", "partner.account.profile.read", "partner.balances.read", "partner.payment-quote.lightning.create", "partner.payment-quote.execute"}, + Scopes: []string{"offline_access", "partner.account.profile.read", "partner.balances.read", "partner.payment-quote.lightning.create", "partner.payment-quote.execute"}, Endpoint: oauth2.Endpoint{ TokenURL: svc.cfg.OAuthTokenUrl, AuthURL: svc.cfg.OAuthAuthUrl, @@ -81,6 +81,14 @@ func (svc *StrikeOAuthService) FetchUserToken(ctx context.Context, app App) (tok return tok, nil } +func (*StrikeOAuthService) GetInfo(ctx context.Context, senderPubkey string) (info *NodeInfo, err error) { + return nil, errors.New("not implemented") +} + +func (*StrikeOAuthService) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination string, preimage string, custom_records []TLVRecord) (preImage string, err error) { + return "", errors.New("not implemented") +} + func (svc *StrikeOAuthService) LookupInvoice(ctx context.Context, senderPubkey string, paymentHash string) (invoice string, paid bool, err error) { return "", false, errors.New("not implemented") } @@ -288,16 +296,16 @@ func (svc *StrikeOAuthService) AuthHandler(c echo.Context) error { sess.Options.Domain = svc.cfg.CookieDomain } } - + cv := oauth2.GenerateVerifier() sess.Values["code_verifier"] = cv sess.Save(c.Request(), c.Response()) url := svc.oauthConf.AuthCodeURL( - appName, - oauth2.SetAuthURLParam("code_challenge_method", "S256"), - oauth2.SetAuthURLParam("code_challenge", oauth2.S256ChallengeFromVerifier(cv)), + appName, + oauth2.SetAuthURLParam("code_challenge_method", "S256"), + oauth2.SetAuthURLParam("code_challenge", oauth2.S256ChallengeFromVerifier(cv)), ) return c.Redirect(302, url) @@ -308,9 +316,9 @@ func (svc *StrikeOAuthService) CallbackHandler(c echo.Context) error { sess, _ := session.Get(CookieName, c) cv, ok := sess.Values["code_verifier"].(string) if !ok { - err := errors.New("Code verifier not found in session") - svc.Logger.WithError(err) - return err + err := errors.New("Code verifier not found in session") + svc.Logger.WithError(err) + return err } tok, err := svc.oauthConf.Exchange( c.Request().Context(), From 585472b7ecbfe779212f546434981b0b758f16f0 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Fri, 15 Dec 2023 15:14:53 +0530 Subject: [PATCH 05/11] feat: add get_info adn make_invoice --- README.md | 13 +++- models.go | 21 +++++++ strike.go | 175 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 203 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c7458d2e..a6b1f444 100644 --- a/README.md +++ b/README.md @@ -180,15 +180,22 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light ✅ `get_balance` +⚠️ `get_info` +- ⚠️ block_hash not supported +- ⚠️ block_height not supported +- ⚠️ pubkey not supported +- ⚠️ color not supported +- ⚠️ network is always `mainnet` + ⚠️ `pay_invoice` - ⚠️ preimage in response missing -❌ `get_info` +⚠️ `make_invoice` +- ⚠️ expiry in request not supported +- ⚠️ paymentHash in response missing (TODO) ❌ `pay_keysend` -❌ `make_invoice` - ❌ `lookup_invoice` ❌ `list_transactions` diff --git a/models.go b/models.go index bdb14afd..99d094ee 100644 --- a/models.go +++ b/models.go @@ -260,6 +260,27 @@ type StrikePayRequest struct { SourceCurrency string `json:"sourceCurrency"` } +type StrikeAmount struct { + Amount float64 `json:"amount"` + Currency string `json:"currency"` +} + +type StrikeInvoiceQuoteRequest struct { + CorrelationId string `json:"correlationId"` + Description string `json:"description"` + Amount StrikeAmount `json:"amount"` +} + +type StrikeInvoiceQuoteResponse struct { + InvoiceId string `json:"invoiceId"` +} + +type StrikeMakeInvoiceResponse struct { + QuoteId string `json:"quoteId"` + LnInvoice string `json:"lnInvoice"` + Expiry int64 `json:"expirationInSec"` +} + type StrikeBalanceResponse struct { Currency string `json:"currency"` Outgoing string `json:"outgoing"` diff --git a/strike.go b/strike.go index 6fe6fb2f..7dce1579 100644 --- a/strike.go +++ b/strike.go @@ -12,6 +12,7 @@ import ( "strconv" "github.com/golang-jwt/jwt" + "github.com/google/uuid" "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" "github.com/sirupsen/logrus" @@ -30,7 +31,7 @@ func NewStrikeOauthService(svc *Service, e *echo.Echo) (result *StrikeOAuthServi conf := &oauth2.Config{ ClientID: svc.cfg.ClientId, ClientSecret: svc.cfg.ClientSecret, - Scopes: []string{"offline_access", "partner.account.profile.read", "partner.balances.read", "partner.payment-quote.lightning.create", "partner.payment-quote.execute"}, + Scopes: []string{"offline_access", "partner.account.profile.read", "partner.balances.read", "partner.payment-quote.lightning.create", "partner.payment-quote.execute", "partner.invoice.create", "partner.invoice.quote.generate"}, Endpoint: oauth2.Endpoint{ TokenURL: svc.cfg.OAuthTokenUrl, AuthURL: svc.cfg.OAuthAuthUrl, @@ -82,7 +83,14 @@ func (svc *StrikeOAuthService) FetchUserToken(ctx context.Context, app App) (tok } func (*StrikeOAuthService) GetInfo(ctx context.Context, senderPubkey string) (info *NodeInfo, err error) { - return nil, errors.New("not implemented") + return &NodeInfo{ + Alias: "strike.com", + Color: "", + Pubkey: "", + Network: "mainnet", + BlockHeight: 0, + BlockHash: "", + }, errors.New("not implemented") } func (*StrikeOAuthService) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination string, preimage string, custom_records []TLVRecord) (preImage string, err error) { @@ -94,7 +102,168 @@ func (svc *StrikeOAuthService) LookupInvoice(ctx context.Context, senderPubkey s } func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey string, amount int64, description string, descriptionHash string, expiry int64) (invoice string, paymentHash string, err error) { - return "", "", errors.New("not implemented") + app := App{} + err = svc.db.Preload("User").First(&app, &App{ + NostrPubkey: senderPubkey, + }).Error + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "description": description, + "descriptionHash": descriptionHash, + "expiry": expiry, + }).Errorf("App not found: %v", err) + return "", "", err + } + + correlationId := uuid.New() + fmt.Println(correlationId) + // amount provided in msat, but Strike API currently only supports BTC value. + amountBTC := (float64(amount) / math.Pow(10, 11)) // 3 + 8 + fmt.Println("amountBTC") + fmt.Println(amountBTC) + fmt.Println(amount) + if amount < 0 { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "description": description, + "descriptionHash": descriptionHash, + "expiry": expiry, + }).Errorf("amount must be 1000 msat or greater") + return "", "", errors.New("amount must be 1000 msat or greater") + } + + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "description": description, + "descriptionHash": descriptionHash, + "expiry": expiry, + "appId": app.ID, + "userId": app.User.ID, + }).Info("Processing make invoice request") + tok, err := svc.FetchUserToken(ctx, app) + if err != nil { + return "", "", err + } + client := svc.oauthConf.Client(ctx, tok) + + body := bytes.NewBuffer([]byte{}) + payloadAmount := &StrikeAmount{ + Amount: amountBTC, + Currency: "BTC", + } + payload := &StrikeInvoiceQuoteRequest{ + Amount: *payloadAmount, + Description: description, + CorrelationId: correlationId.String(), + } + err = json.NewEncoder(body).Encode(payload) + + req, err := http.NewRequest("POST", fmt.Sprintf("%s/invoices", svc.cfg.OAuthAPIURL), body) + if err != nil { + svc.Logger.WithError(err).Error("Error creating request /invoices") + return "", "", err + } + + req.Header.Set("User-Agent", "NWC") + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "description": description, + "descriptionHash": descriptionHash, + "expiry": expiry, + "appId": app.ID, + "userId": app.User.ID, + }).Errorf("Failed to make invoice: %v", err) + return "", "", err + } + + if resp.StatusCode >= 300 { + errorPayload := &StrikeErrorResponse{} + err = json.NewDecoder(resp.Body).Decode(errorPayload) + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "description": description, + "descriptionHash": descriptionHash, + "expiry": expiry, + "appId": app.ID, + "userId": app.User.ID, + "APIHttpStatus": resp.StatusCode, + }).Errorf("Make invoice failed %s", string(errorPayload.Data.Message)) + return "", "", errors.New(errorPayload.Data.Message) + } + + responsePayload := &StrikeInvoiceQuoteResponse{} + err = json.NewDecoder(resp.Body).Decode(responsePayload) + if err != nil { + return "", "", err + } + + req, err = http.NewRequest("POST", fmt.Sprintf("%s/invoices/%s/quote", svc.cfg.OAuthAPIURL, responsePayload.InvoiceId), nil) + if err != nil { + svc.Logger.WithError(err).Errorf("Error creating request /invoices/%s/quote", responsePayload.InvoiceId) + return "", "", err + } + + req.Header.Set("User-Agent", "NWC") + req.Header.Set("Content-Type", "application/json") + + resp, err = client.Do(req) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "description": description, + "descriptionHash": descriptionHash, + "expiry": expiry, + "appId": app.ID, + "userId": app.User.ID, + }).Errorf("Failed to make invoice: %v", err) + return "", "", err + } + + if resp.StatusCode < 300 { + responsePayload := &StrikeMakeInvoiceResponse{} + err = json.NewDecoder(resp.Body).Decode(responsePayload) + if err != nil { + return "", "", err + } + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "description": description, + "descriptionHash": descriptionHash, + "expiry": expiry, + "appId": app.ID, + "userId": app.User.ID, + "paymentRequest": responsePayload.LnInvoice, + "paymentHash": "paymentHash", + }).Info("Make invoice successful") + // Payment hash is unsupported + return responsePayload.LnInvoice, "paymentHash", nil + } + + errorPayload := &StrikeErrorResponse{} + err = json.NewDecoder(resp.Body).Decode(errorPayload) + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "amount": amount, + "description": description, + "descriptionHash": descriptionHash, + "expiry": expiry, + "appId": app.ID, + "userId": app.User.ID, + "APIHttpStatus": resp.StatusCode, + }).Errorf("Make invoice failed %s", string(errorPayload.Data.Message)) + return "", "", errors.New(errorPayload.Data.Message) } func (svc *StrikeOAuthService) GetBalance(ctx context.Context, senderPubkey string) (balance int64, err error) { From 5ed57c966a4a79a487f8b572a25d5e85db5fd2f3 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Fri, 15 Dec 2023 15:29:38 +0530 Subject: [PATCH 06/11] chore: change colors on button --- public/css/application.css | 136 ++++++++++++------------------- public/images/strike-logo.svg | 5 ++ views/backends/strike/index.html | 7 +- 3 files changed, 61 insertions(+), 87 deletions(-) create mode 100644 public/images/strike-logo.svg diff --git a/public/css/application.css b/public/css/application.css index 93a30a6f..14803d41 100644 --- a/public/css/application.css +++ b/public/css/application.css @@ -235,9 +235,7 @@ Use the modern Firefox focus style for all focusable elements. :-moz-focusring { outline: auto; } -/* -Css for logout button -*/ + /* Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) */ @@ -749,9 +747,6 @@ select { .col-span-2 { grid-column: span 2 / span 2; } -.col-span-3 { - grid-column: span 3 / span 3; -} .mx-auto { margin-left: auto; @@ -828,20 +823,6 @@ select { margin-top: 5rem; } -.mt-25{ - margin-top: 6.75rem; -} - -.mt-30{ - margin-top: 7.5rem; -} -.mt-40{ - margin-top: 10rem; -} -.right{ - right: 0px; -} - .mt-4 { margin-top: 1rem; } @@ -857,12 +838,7 @@ select { .block { display: block; } -.absolute{ - position: absolute; -} -.relative{ - position: relative; -} + .inline { display: inline; } @@ -886,18 +862,10 @@ select { .hidden { display: none; } -/* to rotate arrow 180 deg */ -.rotate-180 { - transform: rotate(180deg); - margin-top: -6px; -} .h-4 { height: 1rem; } -.h-10{ - height: 2.5rem; -} .w-16 { width: 4rem; @@ -955,6 +923,11 @@ select { table-layout: fixed; } +.rotate-180 { + --tw-rotate: 180deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + .cursor-pointer { cursor: pointer; } @@ -1000,9 +973,7 @@ select { .justify-center { justify-content: center; } -.justify-left{ - justify-content: left; -} + .justify-between { justify-content: space-between; } @@ -1058,9 +1029,7 @@ select { border-top-left-radius: 0.5rem; border-top-right-radius: 0.5rem; } -.rounded-circle{ - border-radius: 50px; -} + .border { border-width: 1px; } @@ -1095,15 +1064,6 @@ select { border-color: rgb(209 213 219 / var(--tw-border-opacity)); } -.border-gray-400 { - --tw-border-opacity: 1; - border-color: rgb(156 163 175 / var(--tw-border-opacity)); - } - -.border-gray-900{ ---tw-text-opacity: 1; - border-color: rgb(17 24 39 / var(--tw-text-opacity)); -} .border-purple-600 { --tw-border-opacity: 1; border-color: rgb(147 51 234 / var(--tw-border-opacity)); @@ -1154,13 +1114,24 @@ select { background-color: rgb(255 255 255 / var(--tw-bg-opacity)); } +.bg-gradient-to-tr { + background-image: linear-gradient(to top right, var(--tw-gradient-stops)); +} + +.from-gray-900 { + --tw-gradient-from: #111827; + --tw-gradient-to: rgb(17 24 39 / 0); + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); +} + +.to-gray-700 { + --tw-gradient-to: #374151; +} + .bg-origin-border { background-origin: border-box; } -.p-1{ - padding: 0.3rem; -} .p-2 { padding: 0.5rem; } @@ -1177,10 +1148,7 @@ select { padding-left: 2.5rem; padding-right: 2.5rem; } -.px-2{ - padding-left: 0.5rem; - padding-right: 0.5rem; -} + .px-3 { padding-left: 0.75rem; padding-right: 0.75rem; @@ -1306,9 +1274,6 @@ select { font-size: 0.75rem; line-height: 1rem; } -.font-normal{ - font-weight: 400; -} .font-bold { font-weight: 700; @@ -1318,6 +1283,10 @@ select { font-weight: 500; } +.font-normal { + font-weight: 400; +} + .font-semibold { font-weight: 600; } @@ -1369,10 +1338,6 @@ select { color: rgb(17 24 39 / var(--tw-text-opacity)); } -.text-red-500 { - --tw-text-opacity: 1; - color: rgb(248 113 113 / var(--tw-text-opacity)); -} .text-green-500 { --tw-text-opacity: 1; color: rgb(34 197 94 / var(--tw-text-opacity)); @@ -1383,6 +1348,11 @@ select { color: rgb(126 34 206 / var(--tw-text-opacity)); } +.text-red-500 { + --tw-text-opacity: 1; + color: rgb(239 68 68 / var(--tw-text-opacity)); +} + .text-white { --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity)); @@ -1483,12 +1453,16 @@ select { .dark\:divide-white\/10 > :not([hidden]) ~ :not([hidden]) { border-color: rgb(255 255 255 / 0.1); } - .dark\:border-gray-200{ - --tw-border-opacity: 0.3; + + .dark\:border-gray-200 { + --tw-border-opacity: 1; border-color: rgb(229 231 235 / var(--tw-border-opacity)); } - .dark\: - + + .dark\:border-gray-400 { + --tw-border-opacity: 1; + border-color: rgb(156 163 175 / var(--tw-border-opacity)); + } .dark\:border-gray-700 { --tw-border-opacity: 1; @@ -1627,9 +1601,6 @@ select { --tw-ring-opacity: 1; --tw-ring-color: rgb(147 51 234 / var(--tw-ring-opacity)); } - .dark\:fill{ - fill:rgb(156 163 175 / var(--tw-border-opacity)); - } } @media (min-width: 640px) { @@ -1645,6 +1616,10 @@ select { margin-top: 0px; } + .sm\:inline { + display: inline; + } + .sm\:w-\[250px\] { width: 250px; } @@ -1653,18 +1628,10 @@ select { flex-direction: row; } - .sm\:inline{ - display: inline; - } - .sm\:justify-center { justify-content: center; } - .sm\:text-base { - font-size: 1rem; - line-height: 1.5rem; - } .sm\:text-2xl { font-size: 1.5rem; line-height: 2rem; @@ -1674,6 +1641,11 @@ select { font-size: 2.25rem; line-height: 2.5rem; } + + .sm\:text-base { + font-size: 1rem; + line-height: 1.5rem; + } } @media (min-width: 768px) { @@ -1685,14 +1657,14 @@ select { display: flex; } - .md\:hidden{ - display: none; - } - .md\:table-cell { display: table-cell; } + .md\:hidden { + display: none; + } + .md\:grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); } diff --git a/public/images/strike-logo.svg b/public/images/strike-logo.svg new file mode 100644 index 00000000..6fdc65e6 --- /dev/null +++ b/public/images/strike-logo.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/views/backends/strike/index.html b/views/backends/strike/index.html index 595726d2..2cc73364 100644 --- a/views/backends/strike/index.html +++ b/views/backends/strike/index.html @@ -19,14 +19,11 @@

Date: Fri, 15 Dec 2023 15:54:46 +0530 Subject: [PATCH 07/11] chore: keep .env.example --- .env.alby | 15 --------------- .env.example | 47 +++++++++++++++++++++++++++++++++-------------- .env.lnd | 9 --------- .env.strike | 15 --------------- strike.go | 4 ---- 5 files changed, 33 insertions(+), 57 deletions(-) delete mode 100644 .env.alby delete mode 100644 .env.lnd delete mode 100644 .env.strike diff --git a/.env.alby b/.env.alby deleted file mode 100644 index 033ff018..00000000 --- a/.env.alby +++ /dev/null @@ -1,15 +0,0 @@ -DATABASE_URI=postgresql://user@localhost:5432/nostrwalletconnect_lnd?sslmode=disable -NOSTR_PRIVKEY= -LN_BACKEND_TYPE=ALBY - -CLIENT_ID= -CLIENT_SECRET= -OAUTH_AUTH_URL=https://getalby.com/oauth -OAUTH_API_URL=https://api.getalby.com -OAUTH_TOKEN_URL=https://api.getalby.com/oauth/token -OAUTH_REDIRECT_URL=http://nwc.example.com:8080/alby/callback - -COOKIE_SECRET=secretsecret -COOKIE_DOMAIN=.example.com -RELAY=wss://relay.getalby.com/v1 -PORT=8080 \ No newline at end of file diff --git a/.env.example b/.env.example index e0e7c094..40464fd6 100644 --- a/.env.example +++ b/.env.example @@ -1,18 +1,37 @@ -DATABASE_URI=file:nwc.db -NOSTR_PRIVKEY= +DATABASE_URI=postgresql://user@localhost/nostr-wallet-connect +NOSTR_PRIVKEY="your priv key" COOKIE_SECRET=secretsecret +COOKIE_DOMAIN= RELAY=wss://relay.getalby.com/v1 -PUBLIC_RELAY= PORT=8080 -# Polar LND Client -#LN_BACKEND_TYPE=LND -#LND_CERT_FILE=/home/YOUR_USERNAME/.polar/networks/1/volumes/lnd/alice/tls.cert -#LND_ADDRESS=127.0.0.1:10001 -#LND_MACAROON_FILE=/home/YOUR_USERNAME/.polar/networks/1/volumes/lnd/alice/data/chain/bitcoin/regtest/admin.macaroon - -# Alby Wallet API Client -#LN_BACKEND_TYPE=ALBY -#ALBY_CLIENT_SECRET= -#ALBY_CLIENT_ID= -#OAUTH_REDIRECT_URL=http://localhost:8080/alby/callback \ No newline at end of file +# Backend specific config + +# ALBY +LN_BACKEND_TYPE=ALBY + +CLIENT_ID="alby-id" +CLIENT_SECRET="alby-secret" +OAUTH_AUTH_URL=https://getalby.com/oauth +OAUTH_API_URL=https://api.getalby.com +OAUTH_TOKEN_URL=https://api.getalby.com/oauth/token +OAUTH_REDIRECT_URL="redirect-uri" + + +# STRIKE +# LN_BACKEND_TYPE=STRIKE + +# CLIENT_ID="strike-id" +# CLIENT_SECRET="strike-secret" +# OAUTH_AUTH_URL=https://auth.strike.me/connect/authorize +# OAUTH_API_URL=https://api.strike.me/v1 +# OAUTH_TOKEN_URL=https://auth.strike.me/connect/token +# OAUTH_REDIRECT_URL="redirect-uri" + + +# LND +# LN_BACKEND_TYPE=LND + +# LND_ADDRESS=127.0.0.1:10001 +# LND_CERT_FILE=path/to/tls.cert +# LND_MACAROON_FILE=path/to/admin.macaroon \ No newline at end of file diff --git a/.env.lnd b/.env.lnd deleted file mode 100644 index 86f006f4..00000000 --- a/.env.lnd +++ /dev/null @@ -1,9 +0,0 @@ -DATABASE_URI=postgresql://user@localhost:5432/nostrwalletconnect_lnd?sslmode=disable -NOSTR_PRIVKEY= -LND_ADDRESS=rpc.lnd3.regtest.getalby.com:443 -LND_MACAROON_FILE=testnet_lnd3.macaroon -LN_BACKEND_TYPE=LND -COOKIE_SECRET=secretsecret -COOKIE_DOMAIN=.example.com -RELAY=wss://relay.getalby.com/v1 -PORT=8080 \ No newline at end of file diff --git a/.env.strike b/.env.strike deleted file mode 100644 index 36807a36..00000000 --- a/.env.strike +++ /dev/null @@ -1,15 +0,0 @@ -DATABASE_URI=postgresql://user@localhost:5432/nostrwalletconnect_lnd?sslmode=disable -NOSTR_PRIVKEY= -LN_BACKEND_TYPE=STRIKE - -CLIENT_ID= -CLIENT_SECRET= -OAUTH_AUTH_URL=https://auth.strike.me/connect/authorize -OAUTH_API_URL=https://api.strike.me/v1 -OAUTH_TOKEN_URL=https://auth.strike.me/connect/token -OAUTH_REDIRECT_URL=http://localhost:3000/api/auth/callback/strike - -COOKIE_SECRET=secretsecret -COOKIE_DOMAIN=.example.com -RELAY=wss://relay.getalby.com/v1 -PORT=3000 \ No newline at end of file diff --git a/strike.go b/strike.go index 7dce1579..ab925f43 100644 --- a/strike.go +++ b/strike.go @@ -118,12 +118,8 @@ func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey str } correlationId := uuid.New() - fmt.Println(correlationId) // amount provided in msat, but Strike API currently only supports BTC value. amountBTC := (float64(amount) / math.Pow(10, 11)) // 3 + 8 - fmt.Println("amountBTC") - fmt.Println(amountBTC) - fmt.Println(amount) if amount < 0 { svc.Logger.WithFields(logrus.Fields{ "senderPubkey": senderPubkey, From 52a17e92fbf5bada45f5871005271a17b4f572f2 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Thu, 4 Jan 2024 16:59:16 +0530 Subject: [PATCH 08/11] fix: don't return error in getinfo --- strike.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/strike.go b/strike.go index ab925f43..848244a7 100644 --- a/strike.go +++ b/strike.go @@ -90,7 +90,7 @@ func (*StrikeOAuthService) GetInfo(ctx context.Context, senderPubkey string) (in Network: "mainnet", BlockHeight: 0, BlockHash: "", - }, errors.New("not implemented") + }, nil } func (*StrikeOAuthService) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination string, preimage string, custom_records []TLVRecord) (preImage string, err error) { From ae9acaa726620b69753aed4ea03179a54643504b Mon Sep 17 00:00:00 2001 From: im-adithya Date: Thu, 4 Jan 2024 18:32:52 +0530 Subject: [PATCH 09/11] chore: add list transactions and lookup invoice --- alby.go | 2 +- strike.go | 46 +++++++++++++++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/alby.go b/alby.go index 1a88e707..07544401 100644 --- a/alby.go +++ b/alby.go @@ -404,7 +404,7 @@ func (svc *AlbyOAuthService) ListTransactions(ctx context.Context, senderPubkey endpoint += "/outgoing" } - requestUrl := fmt.Sprintf("%s%s?%s", svc.cfg.AlbyAPIURL, endpoint, urlParams.Encode()) + requestUrl := fmt.Sprintf("%s%s?%s", svc.cfg.OAuthAPIURL, endpoint, urlParams.Encode()) req, err := http.NewRequest("GET", requestUrl, nil) if err != nil { diff --git a/strike.go b/strike.go index 848244a7..ddf880cf 100644 --- a/strike.go +++ b/strike.go @@ -10,6 +10,7 @@ import ( "math" "net/http" "strconv" + "time" "github.com/golang-jwt/jwt" "github.com/google/uuid" @@ -97,11 +98,17 @@ func (*StrikeOAuthService) SendKeysend(ctx context.Context, senderPubkey string, return "", errors.New("not implemented") } -func (svc *StrikeOAuthService) LookupInvoice(ctx context.Context, senderPubkey string, paymentHash string) (invoice string, paid bool, err error) { - return "", false, errors.New("not implemented") +func (svc *StrikeOAuthService) ListTransactions(ctx context.Context, senderPubkey string, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []Nip47Transaction, err error) { + // return empty array for now + return transactions, nil } -func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey string, amount int64, description string, descriptionHash string, expiry int64) (invoice string, paymentHash string, err error) { +func (svc *StrikeOAuthService) LookupInvoice(ctx context.Context, senderPubkey string, paymentHash string) (transaction *Nip47Transaction, err error) { + // return empty transaction for now + return transaction, nil +} + +func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey string, amount int64, description string, descriptionHash string, expiry int64) (transaction *Nip47Transaction, err error) { app := App{} err = svc.db.Preload("User").First(&app, &App{ NostrPubkey: senderPubkey, @@ -114,7 +121,7 @@ func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey str "descriptionHash": descriptionHash, "expiry": expiry, }).Errorf("App not found: %v", err) - return "", "", err + return nil, err } correlationId := uuid.New() @@ -128,7 +135,7 @@ func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey str "descriptionHash": descriptionHash, "expiry": expiry, }).Errorf("amount must be 1000 msat or greater") - return "", "", errors.New("amount must be 1000 msat or greater") + return nil, errors.New("amount must be 1000 msat or greater") } svc.Logger.WithFields(logrus.Fields{ @@ -142,7 +149,7 @@ func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey str }).Info("Processing make invoice request") tok, err := svc.FetchUserToken(ctx, app) if err != nil { - return "", "", err + return nil, err } client := svc.oauthConf.Client(ctx, tok) @@ -161,7 +168,7 @@ func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey str req, err := http.NewRequest("POST", fmt.Sprintf("%s/invoices", svc.cfg.OAuthAPIURL), body) if err != nil { svc.Logger.WithError(err).Error("Error creating request /invoices") - return "", "", err + return nil, err } req.Header.Set("User-Agent", "NWC") @@ -178,7 +185,7 @@ func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey str "appId": app.ID, "userId": app.User.ID, }).Errorf("Failed to make invoice: %v", err) - return "", "", err + return nil, err } if resp.StatusCode >= 300 { @@ -194,19 +201,19 @@ func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey str "userId": app.User.ID, "APIHttpStatus": resp.StatusCode, }).Errorf("Make invoice failed %s", string(errorPayload.Data.Message)) - return "", "", errors.New(errorPayload.Data.Message) + return nil, errors.New(errorPayload.Data.Message) } responsePayload := &StrikeInvoiceQuoteResponse{} err = json.NewDecoder(resp.Body).Decode(responsePayload) if err != nil { - return "", "", err + return nil, err } req, err = http.NewRequest("POST", fmt.Sprintf("%s/invoices/%s/quote", svc.cfg.OAuthAPIURL, responsePayload.InvoiceId), nil) if err != nil { svc.Logger.WithError(err).Errorf("Error creating request /invoices/%s/quote", responsePayload.InvoiceId) - return "", "", err + return nil, err } req.Header.Set("User-Agent", "NWC") @@ -223,14 +230,14 @@ func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey str "appId": app.ID, "userId": app.User.ID, }).Errorf("Failed to make invoice: %v", err) - return "", "", err + return nil, err } if resp.StatusCode < 300 { responsePayload := &StrikeMakeInvoiceResponse{} err = json.NewDecoder(resp.Body).Decode(responsePayload) if err != nil { - return "", "", err + return nil, err } svc.Logger.WithFields(logrus.Fields{ "senderPubkey": senderPubkey, @@ -244,7 +251,16 @@ func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey str "paymentHash": "paymentHash", }).Info("Make invoice successful") // Payment hash is unsupported - return responsePayload.LnInvoice, "paymentHash", nil + expiresAt := time.Unix(responsePayload.Expiry, 0) + return &Nip47Transaction{ + Type: "incoming", + Invoice: responsePayload.LnInvoice, + Description: description, + DescriptionHash: descriptionHash, + PaymentHash: "paymentHash", + ExpiresAt: &expiresAt, + Amount: amount, + }, nil } errorPayload := &StrikeErrorResponse{} @@ -259,7 +275,7 @@ func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey str "userId": app.User.ID, "APIHttpStatus": resp.StatusCode, }).Errorf("Make invoice failed %s", string(errorPayload.Data.Message)) - return "", "", errors.New(errorPayload.Data.Message) + return nil, errors.New(errorPayload.Data.Message) } func (svc *StrikeOAuthService) GetBalance(ctx context.Context, senderPubkey string) (balance int64, err error) { From 3219d93afe8f6ef33a576bc37713a35e875f6409 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Thu, 4 Jan 2024 18:49:26 +0530 Subject: [PATCH 10/11] fix: return empty values --- strike.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/strike.go b/strike.go index ddf880cf..faa3f1fe 100644 --- a/strike.go +++ b/strike.go @@ -100,12 +100,12 @@ func (*StrikeOAuthService) SendKeysend(ctx context.Context, senderPubkey string, func (svc *StrikeOAuthService) ListTransactions(ctx context.Context, senderPubkey string, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []Nip47Transaction, err error) { // return empty array for now - return transactions, nil + return []Nip47Transaction{}, nil } func (svc *StrikeOAuthService) LookupInvoice(ctx context.Context, senderPubkey string, paymentHash string) (transaction *Nip47Transaction, err error) { // return empty transaction for now - return transaction, nil + return &Nip47Transaction{}, nil } func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey string, amount int64, description string, descriptionHash string, expiry int64) (transaction *Nip47Transaction, err error) { From d3869dfe2d9fe714606b638282d571b280f5982f Mon Sep 17 00:00:00 2001 From: im-adithya Date: Fri, 5 Jan 2024 18:59:16 +0530 Subject: [PATCH 11/11] feat: add lookup invoice --- README.md | 7 +++- models.go | 12 +++++- strike.go | 112 +++++++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 117 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 978e3c29..99d2087c 100644 --- a/README.md +++ b/README.md @@ -198,9 +198,12 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light - ⚠️ expiry in request not supported - ⚠️ paymentHash in response missing (TODO) -❌ `pay_keysend` +⚠️ `lookup_invoice` +- ⚠️ paymentHash in request not supported (takes in invoiceId) +- ⚠️ only description, amount and createdAt are returned in response +- ⚠️ settledAt is wrong but is only set when it is settled -❌ `lookup_invoice` +❌ `pay_keysend` ❌ `list_transactions` diff --git a/models.go b/models.go index 5eaf37e2..5e3047a2 100644 --- a/models.go +++ b/models.go @@ -318,8 +318,8 @@ type StrikePayRequest struct { } type StrikeAmount struct { - Amount float64 `json:"amount"` - Currency string `json:"currency"` + Amount string `json:"amount"` + Currency string `json:"currency"` } type StrikeInvoiceQuoteRequest struct { @@ -332,6 +332,14 @@ type StrikeInvoiceQuoteResponse struct { InvoiceId string `json:"invoiceId"` } +type StrikeLookupInvoiceResponse struct { + Amount StrikeAmount `json:"amount"` + Created string `json:"created"` + Description string `json:"description"` + InvoiceId string `json:"invoiceId"` + State string `json:"state"` +} + type StrikeMakeInvoiceResponse struct { QuoteId string `json:"quoteId"` LnInvoice string `json:"lnInvoice"` diff --git a/strike.go b/strike.go index faa3f1fe..dfd970fc 100644 --- a/strike.go +++ b/strike.go @@ -32,7 +32,7 @@ func NewStrikeOauthService(svc *Service, e *echo.Echo) (result *StrikeOAuthServi conf := &oauth2.Config{ ClientID: svc.cfg.ClientId, ClientSecret: svc.cfg.ClientSecret, - Scopes: []string{"offline_access", "partner.account.profile.read", "partner.balances.read", "partner.payment-quote.lightning.create", "partner.payment-quote.execute", "partner.invoice.create", "partner.invoice.quote.generate"}, + Scopes: []string{"offline_access", "partner.account.profile.read", "partner.balances.read", "partner.invoice.read", "partner.invoice.create", "partner.invoice.quote.generate", "partner.payment-quote.lightning.create", "partner.payment-quote.execute"}, Endpoint: oauth2.Endpoint{ TokenURL: svc.cfg.OAuthTokenUrl, AuthURL: svc.cfg.OAuthAuthUrl, @@ -104,8 +104,95 @@ func (svc *StrikeOAuthService) ListTransactions(ctx context.Context, senderPubke } func (svc *StrikeOAuthService) LookupInvoice(ctx context.Context, senderPubkey string, paymentHash string) (transaction *Nip47Transaction, err error) { - // return empty transaction for now - return &Nip47Transaction{}, nil + // TODO: move to a shared function + app := App{} + err = svc.db.Preload("User").First(&app, &App{ + NostrPubkey: senderPubkey, + }).Error + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "paymentHash": paymentHash, + }).Errorf("App not found: %v", err) + return nil, err + } + + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "paymentHash": paymentHash, + "appId": app.ID, + "userId": app.User.ID, + }).Info("Processing lookup invoice request") + tok, err := svc.FetchUserToken(ctx, app) + if err != nil { + return nil, err + } + client := svc.oauthConf.Client(ctx, tok) + + // paymentHash is actually invoiceId + req, err := http.NewRequest("GET", fmt.Sprintf("%s/invoices/%s", svc.cfg.OAuthAPIURL, paymentHash), nil) + if err != nil { + svc.Logger.WithError(err).Errorf("Error creating request /invoices/%s", paymentHash) + return nil, err + } + + req.Header.Set("User-Agent", "NWC") + + resp, err := client.Do(req) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "appId": app.ID, + "userId": app.User.ID, + }).Errorf("Failed to lookup invoice: %v", err) + return nil, err + } + + if resp.StatusCode < 300 { + responsePayload := &StrikeLookupInvoiceResponse{} + err = json.NewDecoder(resp.Body).Decode(responsePayload) + if err != nil { + return nil, err + } + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "paymentHash": paymentHash, + "appId": app.ID, + "userId": app.User.ID, + "settled": responsePayload.State == "PAID", + }).Info("Lookup invoice successful") + + createdAt, _ := time.Parse(time.RFC3339, responsePayload.Created) + amountInBTC, _ := strconv.ParseFloat(responsePayload.Amount.Amount, 64) + transaction = &Nip47Transaction{ + // TODO: replace with bolt11 (currently not returned in response) + Invoice: "sampleinvoice", + Description: responsePayload.Description, + PaymentHash: paymentHash, + Preimage: "samplepreimage", + Amount: int64(amountInBTC * math.Pow(10, 8)), + CreatedAt: createdAt, + } + + if responsePayload.State == "PAID" { + // TODO: replace with actual settledAt (currently not returned in response) + timeNow := time.Now() + transaction.SettledAt = &timeNow + } + fmt.Println(transaction) + return transaction, nil + } + + errorPayload := &StrikeErrorResponse{} + err = json.NewDecoder(resp.Body).Decode(errorPayload) + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "paymentHash": paymentHash, + "appId": app.ID, + "userId": app.User.ID, + "APIHttpStatus": resp.StatusCode, + }).Errorf("Lookup invoice failed %s", string(errorPayload.Data.Message)) + return nil, errors.New(errorPayload.Data.Message) } func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey string, amount int64, description string, descriptionHash string, expiry int64) (transaction *Nip47Transaction, err error) { @@ -155,7 +242,7 @@ func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey str body := bytes.NewBuffer([]byte{}) payloadAmount := &StrikeAmount{ - Amount: amountBTC, + Amount: strconv.FormatFloat(amountBTC, 'f', -1, 64), Currency: "BTC", } payload := &StrikeInvoiceQuoteRequest{ @@ -210,9 +297,11 @@ func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey str return nil, err } - req, err = http.NewRequest("POST", fmt.Sprintf("%s/invoices/%s/quote", svc.cfg.OAuthAPIURL, responsePayload.InvoiceId), nil) + // this is similar to paymentHash + invoiceId := responsePayload.InvoiceId + req, err = http.NewRequest("POST", fmt.Sprintf("%s/invoices/%s/quote", svc.cfg.OAuthAPIURL, invoiceId), nil) if err != nil { - svc.Logger.WithError(err).Errorf("Error creating request /invoices/%s/quote", responsePayload.InvoiceId) + svc.Logger.WithError(err).Errorf("Error creating request /invoices/%s/quote", invoiceId) return nil, err } @@ -226,6 +315,7 @@ func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey str "amount": amount, "description": description, "descriptionHash": descriptionHash, + "invoiceId": invoiceId, "expiry": expiry, "appId": app.ID, "userId": app.User.ID, @@ -248,7 +338,8 @@ func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey str "appId": app.ID, "userId": app.User.ID, "paymentRequest": responsePayload.LnInvoice, - "paymentHash": "paymentHash", + "invoiceId": invoiceId, + // "paymentHash": "paymentHash", }).Info("Make invoice successful") // Payment hash is unsupported expiresAt := time.Unix(responsePayload.Expiry, 0) @@ -257,9 +348,10 @@ func (svc *StrikeOAuthService) MakeInvoice(ctx context.Context, senderPubkey str Invoice: responsePayload.LnInvoice, Description: description, DescriptionHash: descriptionHash, - PaymentHash: "paymentHash", - ExpiresAt: &expiresAt, - Amount: amount, + // Passing invoiceId as paymentHash for now + PaymentHash: invoiceId, + ExpiresAt: &expiresAt, + Amount: amount, }, nil }