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/README.md b/README.md index 6e3faf54..99d2087c 100644 --- a/README.md +++ b/README.md @@ -179,3 +179,34 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light ❌ `multi_pay_invoice` ❌ `multi_pay_keysend (TBC)` + +### Strike OAuth API + +✅ `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 + +⚠️ `make_invoice` +- ⚠️ expiry in request not supported +- ⚠️ paymentHash in response missing (TODO) + +⚠️ `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 + +❌ `pay_keysend` + +❌ `list_transactions` + +❌ `multi_pay_invoice (TBC)` + +❌ `multi_pay_keysend (TBC)` diff --git a/alby.go b/alby.go index 7a0e0f9d..07544401 100644 --- a/alby.go +++ b/alby.go @@ -26,8 +26,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{ @@ -135,7 +135,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 nil, err @@ -225,7 +225,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 nil, err @@ -320,7 +320,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 @@ -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 { @@ -497,7 +497,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 @@ -583,7 +583,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 @@ -659,7 +659,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 2dca673b..5e17b3eb 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( 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 @@ -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 e2b7c9ad..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= 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/models.go b/models.go index 4f56a251..5e3047a2 100644 --- a/models.go +++ b/models.go @@ -311,3 +311,65 @@ type Nip47ListTransactionsParams struct { type Nip47ListTransactionsResponse struct { Transactions []Nip47Transaction `json:"transactions"` } + +type StrikePayRequest struct { + LnInvoice string `json:"lnInvoice"` + SourceCurrency string `json:"sourceCurrency"` +} + +type StrikeAmount struct { + Amount string `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 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"` + Expiry int64 `json:"expirationInSec"` +} + +type StrikeBalanceResponse struct { + Currency string `json:"currency"` + Outgoing string `json:"outgoing"` + Available string `json:"available"` + Total string `json:"total"` +} + +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 StrikeError struct { + Status bool `json:"status"` + Code int `json:"code"` + Message string `json:"message"` +} + +type StrikeErrorResponse struct { + Data StrikeError `json:"data"` +} 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/strike.go b/strike.go new file mode 100644 index 00000000..dfd970fc --- /dev/null +++ b/strike.go @@ -0,0 +1,638 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "net/http" + "strconv" + "time" + + "github.com/golang-jwt/jwt" + "github.com/google/uuid" + "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{"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, + 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("/strike/callback", 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() + // TODO: Parse token for id, if implementing get_info + 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 (*StrikeOAuthService) GetInfo(ctx context.Context, senderPubkey string) (info *NodeInfo, err error) { + return &NodeInfo{ + Alias: "strike.com", + Color: "", + Pubkey: "", + Network: "mainnet", + BlockHeight: 0, + BlockHash: "", + }, nil +} + +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) 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 []Nip47Transaction{}, nil +} + +func (svc *StrikeOAuthService) LookupInvoice(ctx context.Context, senderPubkey string, paymentHash string) (transaction *Nip47Transaction, 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 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) { + 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 nil, err + } + + correlationId := uuid.New() + // amount provided in msat, but Strike API currently only supports BTC value. + amountBTC := (float64(amount) / math.Pow(10, 11)) // 3 + 8 + 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 nil, 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 nil, err + } + client := svc.oauthConf.Client(ctx, tok) + + body := bytes.NewBuffer([]byte{}) + payloadAmount := &StrikeAmount{ + Amount: strconv.FormatFloat(amountBTC, 'f', -1, 64), + 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 nil, 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 nil, 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 nil, errors.New(errorPayload.Data.Message) + } + + responsePayload := &StrikeInvoiceQuoteResponse{} + err = json.NewDecoder(resp.Body).Decode(responsePayload) + if err != nil { + return nil, err + } + + // 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", invoiceId) + return nil, 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, + "invoiceId": invoiceId, + "expiry": expiry, + "appId": app.ID, + "userId": app.User.ID, + }).Errorf("Failed to make invoice: %v", err) + return nil, err + } + + if resp.StatusCode < 300 { + responsePayload := &StrikeMakeInvoiceResponse{} + err = json.NewDecoder(resp.Body).Decode(responsePayload) + if err != nil { + return nil, 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, + "invoiceId": invoiceId, + // "paymentHash": "paymentHash", + }).Info("Make invoice successful") + // Payment hash is unsupported + expiresAt := time.Unix(responsePayload.Expiry, 0) + return &Nip47Transaction{ + Type: "incoming", + Invoice: responsePayload.LnInvoice, + Description: description, + DescriptionHash: descriptionHash, + // Passing invoiceId as paymentHash for now + PaymentHash: invoiceId, + ExpiresAt: &expiresAt, + Amount: amount, + }, 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 nil, errors.New(errorPayload.Data.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/balances", svc.cfg.OAuthAPIURL), nil) + if err != nil { + svc.Logger.WithError(err).Error("Error creating request /balances") + 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 { + 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 balance, nil + } + + 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.Data.Message)) + return 0, errors.New(errorPayload.Data.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 := &StrikePayRequest{ + LnInvoice: payReq, + SourceCurrency: "BTC", + } + err = json.NewEncoder(body).Encode(payload) + + 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 /payment-quotes/lightning") + 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 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, + "bolt11": payReq, + "appId": app.ID, + "userId": app.User.ID, + }).Errorf("Failed to pay invoice: %v", err) + return "", err + } + + if resp.StatusCode < 300 { + responsePayload := &StrikePaymentResponse{} + 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, + "paymentId": responsePayload.PaymentId, + }).Info("Payment successful") + // What to return here? + return "preimage", nil + } + + 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) +} + +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 + } + } + + 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)), + ) + + return c.Redirect(302, url) +} + +func (svc *StrikeOAuthService) CallbackHandler(c echo.Context) error { + code := c.QueryParam("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 + } + + token, _, err := new(jwt.Parser).ParseUnverified(tok.AccessToken, jwt.MapClaims{}) + if err != nil { + err := errors.New("Error parsing token") + svc.Logger.WithError(err) + return err + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + err := errors.New("Error parsing token claims") + svc.Logger.WithError(err) + return err + } + + user := User{} + // 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 = claims["email"].(string) + svc.db.Save(&user) + + 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..2cc73364 --- /dev/null +++ b/views/backends/strike/index.html @@ -0,0 +1,46 @@ +{{define "body"}} + +