diff --git a/.env.example b/.env.example index db1e3049..e0e7c094 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,18 @@ DATABASE_URI=file:nwc.db NOSTR_PRIVKEY= -LN_BACKEND_TYPE=ALBY -ALBY_CLIENT_SECRET= -ALBY_CLIENT_ID= -OAUTH_REDIRECT_URL=http://localhost:8080/alby/callback COOKIE_SECRET=secretsecret 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 diff --git a/.env.swp b/.env.swp new file mode 100644 index 00000000..3dd710f6 Binary files /dev/null and b/.env.swp differ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..ca1751b9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.defaultFormatter": "golang.go" +} \ No newline at end of file diff --git a/README.md b/README.md index 38458b27..628ae67b 100644 --- a/README.md +++ b/README.md @@ -117,3 +117,60 @@ Want to support the work on Alby? Support the Alby team ⚡️hello@getalby.com You can also contribute to our [bounty program](https://github.com/getAlby/lightning-browser-extension/wiki/Bounties): ⚡️bounties@getalby.com + + +## NIP-47 Supported Methods + +✅ NIP-47 info event + +### LND + +✅ `get_info` + +✅ `get_balance` + +✅ `pay_invoice` + +⚠️ `make_invoice` +- ⚠️ invoice in response missing (TODO) + +⚠️ `lookup_invoice` +- ⚠️ invoice in response missing (TODO) +- ⚠️ response does not match spec, missing fields + +❌ `pay_keysend` + +❌ `list_transactions` + +❌ `multi_pay_invoice (TBC)` + +❌ `multi_pay_keysend (TBC)` + +### Alby OAuth API + +✅ `get_info` +- ⚠️ block_hash not supported +- ⚠️ block_height not supported +- ⚠️ pubkey not supported +- ⚠️ color not supported +- ⚠️ network is always `mainnet` + +✅ `get_balance` + +✅ `pay_invoice` + +⚠️ `make_invoice` +- ⚠️ expiry in request not supported +- ⚠️ invoice in response missing (TODO) + +⚠️ `lookup_invoice` +- ⚠️ invoice in response missing (TODO) +- ⚠️ response does not match spec, missing fields (TODO) + +❌ `pay_keysend` + +❌ `list_transactions` + +❌ `multi_pay_invoice (TBC)` + +❌ `multi_pay_keysend (TBC)` diff --git a/alby.go b/alby.go index 4510f4aa..e3a4c949 100644 --- a/alby.go +++ b/alby.go @@ -140,6 +140,7 @@ func (svc *AlbyOAuthService) MakeInvoice(ctx context.Context, senderPubkey strin return "", "", err } + // TODO: move to creation of HTTP client req.Header.Set("User-Agent", "NWC") req.Header.Set("Content-Type", "application/json") @@ -270,6 +271,33 @@ func (svc *AlbyOAuthService) LookupInvoice(ctx context.Context, senderPubkey str return "", false, errors.New(errorPayload.Message) } +func (svc *AlbyOAuthService) GetInfo(ctx context.Context, senderPubkey string) (info *NodeInfo, 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 nil, err + } + + svc.Logger.WithFields(logrus.Fields{ + "senderPubkey": senderPubkey, + "appId": app.ID, + "userId": app.User.ID, + }).Info("Info fetch successful") + return &NodeInfo{ + Alias: "getalby.com", + Color: "", + Pubkey: "", + Network: "mainnet", + BlockHeight: 0, + BlockHash: "", + }, err +} + func (svc *AlbyOAuthService) GetBalance(ctx context.Context, senderPubkey string) (balance int64, err error) { app := App{} err = svc.db.Preload("User").First(&app, &App{ diff --git a/go.mod b/go.mod index e78b76d2..665369b7 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( 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.13.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 @@ -49,7 +49,7 @@ require ( github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/lru v1.1.1 // indirect - github.com/dgraph-io/ristretto v0.1.0 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvyukov/go-fuzz v0.0.0-20220726122315-1d375ef9f9f6 // indirect github.com/fergusstrange/embedded-postgres v1.19.0 // indirect @@ -59,6 +59,9 @@ require ( 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 + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/gobwas/ws v1.2.0 // indirect github.com/gofrs/uuid v4.2.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect @@ -117,6 +120,7 @@ require ( github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.40.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect + github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 // indirect github.com/rogpeppe/fastuuid v1.2.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect @@ -125,6 +129,9 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect github.com/tinylib/msgp v1.1.6 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect @@ -182,8 +189,8 @@ 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.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect + github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/glebarez/sqlite v1.5.0 github.com/gorilla/websocket v1.5.0 // indirect github.com/jackc/pgx/v5 v5.4.3 @@ -193,6 +200,6 @@ require ( 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-20221106115401-f9659909a136 // 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 b7f055fe..a26ad40e 100644 --- a/go.sum +++ b/go.sum @@ -196,15 +196,21 @@ github.com/davrux/echo-logrus/v4 v4.0.3 h1:V5bM43A+3PNdpiGC2TS8HKAeaUWQph/j8utG7 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= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= @@ -270,6 +276,12 @@ github.com/go-macaroon-bakery/macaroonpb v1.0.0/go.mod h1:UzrGOcbiwTXISFP2XDLDPj github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I= +github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= @@ -625,6 +637,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW 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= github.com/nbd-wtf/ln-decodepay v1.11.1/go.mod h1:xzBXPaCj/7oRRaui+iYSIxy5LYUjoPfAyAGq2WCyNKk= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= @@ -691,6 +705,8 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= +github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 h1:VstopitMQi3hZP0fzvnsLmzXZdQGc4bEcgu24cp+d4M= github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= @@ -755,6 +771,12 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw= github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -914,6 +936,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH 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= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1080,6 +1104,8 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= diff --git a/handle_info_request.go b/handle_info_request.go new file mode 100644 index 00000000..799eed18 --- /dev/null +++ b/handle_info_request.go @@ -0,0 +1,81 @@ +package main + +import ( + "context" + "fmt" + + "github.com/nbd-wtf/go-nostr" + "github.com/sirupsen/logrus" +) + +func (svc *Service) HandleGetInfoEvent(ctx context.Context, request *Nip47Request, event *nostr.Event, app App, ss []byte) (result *nostr.Event, err error) { + + nostrEvent := NostrEvent{App: app, NostrId: event.ID, Content: event.Content, State: "received"} + err = svc.db.Create(&nostrEvent).Error + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + }).Errorf("Failed to save nostr event: %v", err) + return nil, err + } + + hasPermission, code, message := svc.hasPermission(&app, event, request.Method, nil) + + if !hasPermission { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + }).Errorf("App does not have permission: %s %s", code, message) + + return svc.createResponse(event, Nip47Response{ + ResultType: request.Method, + Error: &Nip47Error{ + Code: code, + Message: message, + }}, ss) + } + + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + }).Info("Fetching node info") + + info, err := svc.lnClient.GetInfo(ctx, event.PubKey) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "eventKind": event.Kind, + "appId": app.ID, + }).Infof("Failed to fetch node info: %v", err) + nostrEvent.State = NOSTR_EVENT_STATE_HANDLER_ERROR + svc.db.Save(&nostrEvent) + return svc.createResponse(event, Nip47Response{ + ResultType: request.Method, + Error: &Nip47Error{ + Code: NIP_47_ERROR_INTERNAL, + Message: fmt.Sprintf("Something went wrong while fetching node info: %s", err.Error()), + }, + }, ss) + } + + responsePayload := &Nip47GetInfoResponse{ + Alias: info.Alias, + Color: info.Color, + Pubkey: info.Pubkey, + Network: info.Network, + BlockHeight: info.BlockHeight, + BlockHash: info.BlockHash, + Methods: svc.GetMethods(&app), + } + + nostrEvent.State = NOSTR_EVENT_STATE_HANDLER_EXECUTED + svc.db.Save(&nostrEvent) + return svc.createResponse(event, Nip47Response{ + ResultType: request.Method, + Result: responsePayload, + }, ss) +} diff --git a/lnd.go b/lnd.go index 0b498343..cec5e494 100644 --- a/lnd.go +++ b/lnd.go @@ -19,6 +19,7 @@ type LNClient interface { SendPaymentSync(ctx context.Context, senderPubkey string, payReq string) (preimage string, err error) SendKeysend(ctx context.Context, senderPubkey string, amount int64, destination, preimage string, custom_records []TLVRecord) (preImage string, err error) GetBalance(ctx context.Context, senderPubkey string) (balance int64, err error) + GetInfo(ctx context.Context, senderPubkey string) (info *NodeInfo, err error) MakeInvoice(ctx context.Context, senderPubkey string, amount int64, description string, descriptionHash string, expiry int64) (invoice string, paymentHash string, err error) LookupInvoice(ctx context.Context, senderPubkey string, paymentHash string) (invoice string, paid bool, err error) } @@ -52,6 +53,21 @@ func (svc *LNDService) GetBalance(ctx context.Context, senderPubkey string) (bal return int64(resp.LocalBalance.Sat), nil } +func (svc *LNDService) GetInfo(ctx context.Context, senderPubkey string) (info *NodeInfo, err error) { + resp, err := svc.client.GetInfo(ctx, &lnrpc.GetInfoRequest{}) + if err != nil { + return nil, err + } + return &NodeInfo{ + Alias: resp.Alias, + Color: resp.Color, + Pubkey: resp.IdentityPubkey, + Network: resp.Chains[0].Network, + BlockHeight: resp.BlockHeight, + BlockHash: resp.BlockHash, + }, nil +} + func (svc *LNDService) MakeInvoice(ctx context.Context, senderPubkey string, amount int64, description string, descriptionHash string, expiry int64) (invoice string, paymentHash string, err error) { var descriptionHashBytes []byte diff --git a/main.go b/main.go index 1904741f..151b56b2 100644 --- a/main.go +++ b/main.go @@ -174,7 +174,8 @@ func main() { //connect to the relay svc.Logger.Infof("Connecting to the relay: %s", cfg.Relay) - relay, err := nostr.RelayConnect(ctx, cfg.Relay) + + relay, err := nostr.RelayConnect(ctx, cfg.Relay, nostr.WithNoticeHandler(svc.noticeHandler)) if err != nil { svc.Logger.Fatal(err) } @@ -189,11 +190,14 @@ func main() { //TODO: we can start this loop for multiple relays for { svc.Logger.Info("Subscribing to events") - sub := relay.Subscribe(ctx, svc.createFilters()) + sub, err := relay.Subscribe(ctx, svc.createFilters()) + if err != nil { + svc.Logger.Fatal(err) + } err = svc.StartSubscription(ctx, sub) if err != nil { //err being non-nil means that we have an error on the websocket error channel. In this case we just try to reconnect. - svc.Logger.WithError(err).Error("Got an error from the relay. Reconnecting...") + svc.Logger.WithError(err).Error("Got an error from the relay while listening to subscription. Reconnecting...") relay, err = nostr.RelayConnect(ctx, cfg.Relay) if err != nil { svc.Logger.Fatal(err) @@ -220,3 +224,7 @@ func (svc *Service) createFilters() nostr.Filters { } return []nostr.Filter{filter} } + +func (svc *Service) noticeHandler(notice string) { + svc.Logger.Infof("Received a notice %s", notice) +} diff --git a/models.go b/models.go index d954454d..2bb9827a 100644 --- a/models.go +++ b/models.go @@ -13,6 +13,7 @@ const ( NIP_47_RESPONSE_KIND = 23195 NIP_47_PAY_INVOICE_METHOD = "pay_invoice" NIP_47_GET_BALANCE_METHOD = "get_balance" + NIP_47_GET_INFO_METHOD = "get_info" NIP_47_MAKE_INVOICE_METHOD = "make_invoice" NIP_47_LOOKUP_INVOICE_METHOD = "lookup_invoice" NIP_47_PAY_KEYSEND_METHOD = "pay_keysend" @@ -24,7 +25,7 @@ const ( NIP_47_ERROR_EXPIRED = "EXPIRED" NIP_47_ERROR_RESTRICTED = "RESTRICTED" NIP_47_OTHER = "OTHER" - NIP_47_CAPABILITIES = "pay_invoice,get_balance" + NIP_47_CAPABILITIES = "pay_invoice,get_balance,get_info,make_invoice,lookup_invoice" ) const ( @@ -37,6 +38,7 @@ const ( var nip47MethodDescriptions = map[string]string{ NIP_47_GET_BALANCE_METHOD: "Read your balance", + NIP_47_GET_INFO_METHOD: "Read your node info", NIP_47_PAY_INVOICE_METHOD: "Send payments", NIP_47_MAKE_INVOICE_METHOD: "Create invoices", NIP_47_LOOKUP_INVOICE_METHOD: "Lookup status of invoices", @@ -44,11 +46,13 @@ var nip47MethodDescriptions = map[string]string{ var nip47MethodIcons = map[string]string{ NIP_47_GET_BALANCE_METHOD: "wallet", + NIP_47_GET_INFO_METHOD: "wallet", NIP_47_PAY_INVOICE_METHOD: "lightning", NIP_47_MAKE_INVOICE_METHOD: "invoice", NIP_47_LOOKUP_INVOICE_METHOD: "search", } +// TODO: move to models/Alby type AlbyMe struct { Identifier string `json:"identifier"` NPub string `json:"nostr_pubkey"` @@ -122,6 +126,7 @@ type PayRequest struct { Invoice string `json:"invoice"` } +// TODO: move to models/Alby type KeysendRequest struct { Amount int64 `json:"amount"` Destination string `json:"destination"` @@ -161,6 +166,16 @@ type ErrorResponse struct { Message string `json:"message"` } +// TODO: move to models/LNClient +type NodeInfo struct { + Alias string + Color string + Pubkey string + Network string + BlockHeight uint32 + BlockHash string +} + type Identity struct { gorm.Model Privkey string @@ -190,10 +205,10 @@ type Nip47PayResponse struct { } type Nip47KeysendParams struct { - Amount int64 `json:"amount"` - Pubkey string `json:"pubkey"` - Preimage string `json:"preimage"` - TLVRecords []TLVRecord `json:"tlv_records"` + Amount int64 `json:"amount"` + Pubkey string `json:"pubkey"` + Preimage string `json:"preimage"` + TLVRecords []TLVRecord `json:"tlv_records"` } type TLVRecord struct { @@ -207,6 +222,17 @@ type Nip47BalanceResponse struct { BudgetRenewal string `json:"budget_renewal"` } +// TODO: move to models/Nip47 +type Nip47GetInfoResponse struct { + Alias string `json:"alias"` + Color string `json:"color"` + Pubkey string `json:"pubkey"` + Network string `json:"network"` + BlockHeight uint32 `json:"block_height"` + BlockHash string `json:"block_hash"` + Methods []string `json:"methods"` +} + type Nip47MakeInvoiceParams struct { Amount int64 `json:"amount"` Description string `json:"description"` diff --git a/public/css/application.css b/public/css/application.css index 6a666317..93a30a6f 100644 --- a/public/css/application.css +++ b/public/css/application.css @@ -1,5 +1,5 @@ /* -! tailwindcss v3.3.5 | MIT License | https://tailwindcss.com +! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com */ /* @@ -31,7 +31,6 @@ 3. Use a more readable tab size. 4. Use the user's configured `sans` font-family by default. 5. Use the user's configured `sans` font-feature-settings by default. -6. Use the user's configured `sans` font-variation-settings by default. */ html { @@ -48,8 +47,6 @@ html { /* 4 */ font-feature-settings: normal; /* 5 */ - font-variation-settings: normal; - /* 6 */ } /* @@ -191,10 +188,6 @@ select, textarea { font-family: inherit; /* 1 */ - font-feature-settings: inherit; - /* 1 */ - font-variation-settings: inherit; - /* 1 */ font-size: 100%; /* 1 */ font-weight: inherit; @@ -242,7 +235,9 @@ 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) */ @@ -345,14 +340,6 @@ menu { padding: 0; } -/* -Reset default styling for dialogs. -*/ - -dialog { - padding: 0; -} - /* Prevent resizing textareas horizontally by default. */ @@ -434,7 +421,7 @@ video { display: none; } -[type='text'],input:where(:not([type])),[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select { +[type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select { -webkit-appearance: none; -moz-appearance: none; appearance: none; @@ -451,7 +438,7 @@ video { --tw-shadow: 0 0 #0000; } -[type='text']:focus, input:where(:not([type])):focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus { +[type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus { outline: 2px solid transparent; outline-offset: 2px; --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); @@ -480,11 +467,6 @@ input::placeholder,textarea::placeholder { ::-webkit-date-and-time-value { min-height: 1.5em; - text-align: inherit; -} - -::-webkit-datetime-edit { - display: inline-flex; } ::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field { @@ -502,7 +484,7 @@ select { print-color-adjust: exact; } -[multiple],[size]:where(select:not([size="1"])) { +[multiple] { background-image: initial; background-position: initial; background-repeat: unset; @@ -619,9 +601,6 @@ select { --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; - --tw-gradient-from-position: ; - --tw-gradient-via-position: ; - --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; @@ -669,9 +648,6 @@ select { --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; - --tw-gradient-from-position: ; - --tw-gradient-via-position: ; - --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; @@ -756,7 +732,10 @@ select { } .inset-0 { - inset: 0px; + top: 0px; + right: 0px; + bottom: 0px; + left: 0px; } .left-0 { @@ -770,6 +749,9 @@ select { .col-span-2 { grid-column: span 2 / span 2; } +.col-span-3 { + grid-column: span 3 / span 3; +} .mx-auto { margin-left: auto; @@ -846,6 +828,20 @@ 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; } @@ -861,7 +857,12 @@ select { .block { display: block; } - +.absolute{ + position: absolute; +} +.relative{ + position: relative; +} .inline { display: inline; } @@ -885,10 +886,18 @@ 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; @@ -991,7 +1000,9 @@ select { .justify-center { justify-content: center; } - +.justify-left{ + justify-content: left; +} .justify-between { justify-content: space-between; } @@ -1047,7 +1058,9 @@ select { border-top-left-radius: 0.5rem; border-top-right-radius: 0.5rem; } - +.rounded-circle{ + border-radius: 50px; +} .border { border-width: 1px; } @@ -1082,6 +1095,15 @@ 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)); @@ -1136,6 +1158,9 @@ select { background-origin: border-box; } +.p-1{ + padding: 0.3rem; +} .p-2 { padding: 0.5rem; } @@ -1152,7 +1177,10 @@ 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; @@ -1213,18 +1241,14 @@ select { padding-right: 0.75rem; } -.pt-1 { - padding-top: 0.25rem; +.pt-2 { + padding-top: 0.5rem; } .pt-4 { padding-top: 1rem; } -.pt-2 { - padding-top: 0.5rem; -} - .text-left { text-align: left; } @@ -1282,6 +1306,9 @@ select { font-size: 0.75rem; line-height: 1rem; } +.font-normal{ + font-weight: 400; +} .font-bold { font-weight: 700; @@ -1342,6 +1369,10 @@ 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)); @@ -1452,11 +1483,12 @@ select { .dark\:divide-white\/10 > :not([hidden]) ~ :not([hidden]) { border-color: rgb(255 255 255 / 0.1); } - - .dark\:border-gray-400 { - --tw-border-opacity: 1; - border-color: rgb(156 163 175 / var(--tw-border-opacity)); + .dark\:border-gray-200{ + --tw-border-opacity: 0.3; + border-color: rgb(229 231 235 / var(--tw-border-opacity)); } + .dark\: + .dark\:border-gray-700 { --tw-border-opacity: 1; @@ -1595,6 +1627,9 @@ 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) { @@ -1618,10 +1653,18 @@ 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; @@ -1642,6 +1685,10 @@ select { display: flex; } + .md\:hidden{ + display: none; + } + .md\:table-cell { display: table-cell; } diff --git a/public/images/about.svg b/public/images/about.svg new file mode 100644 index 00000000..b569ed2f --- /dev/null +++ b/public/images/about.svg @@ -0,0 +1,6 @@ + + diff --git a/public/images/caret.svg b/public/images/caret.svg new file mode 100644 index 00000000..97c05541 --- /dev/null +++ b/public/images/caret.svg @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/public/images/logout.svg b/public/images/logout.svg new file mode 100644 index 00000000..c417f2bc --- /dev/null +++ b/public/images/logout.svg @@ -0,0 +1,6 @@ + diff --git a/service.go b/service.go index 9d08294c..546dac2b 100644 --- a/service.go +++ b/service.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "time" "github.com/labstack/echo-contrib/session" @@ -25,6 +26,7 @@ type Service struct { var supportedMethods = map[string]bool{ NIP_47_PAY_INVOICE_METHOD: true, NIP_47_GET_BALANCE_METHOD: true, + NIP_47_GET_INFO_METHOD: true, NIP_47_MAKE_INVOICE_METHOD: true, NIP_47_LOOKUP_INVOICE_METHOD: true, NIP_47_PAY_KEYSEND_METHOD: true, @@ -53,16 +55,17 @@ func (svc *Service) GetUser(c echo.Context) (user *User, err error) { func (svc *Service) StartSubscription(ctx context.Context, sub *nostr.Subscription) error { for { + if sub.Relay.ConnectionError != nil { + return sub.Relay.ConnectionError + } select { - case notice := <-sub.Relay.Notices: - svc.Logger.Infof("Received a notice %s", notice) - case conErr := <-sub.Relay.ConnectionError: - return conErr case <-ctx.Done(): svc.Logger.Info("Exiting subscription.") return nil case <-sub.EndOfStoredEvents: - svc.Logger.Info("Received EOS") + if !svc.ReceivedEOS { + svc.Logger.Info("Received EOS") + } svc.ReceivedEOS = true case event := <-sub.Events: go func() { @@ -74,7 +77,16 @@ func (svc *Service) StartSubscription(ctx context.Context, sub *nostr.Subscripti }).Errorf("Failed to process event: %v", err) } if resp != nil { - status := sub.Relay.Publish(ctx, *resp) + status, err := sub.Relay.Publish(ctx, *resp) + if err != nil { + svc.Logger.WithFields(logrus.Fields{ + "eventId": event.ID, + "status": status, + "replyEventId": resp.ID, + }).Errorf("Failed to publish reply: %v", err) + return + } + nostrEvent := NostrEvent{} result := svc.db.Where("nostr_id = ?", event.ID).First(&nostrEvent) if result.Error != nil { @@ -86,7 +98,7 @@ func (svc *Service) StartSubscription(ctx context.Context, sub *nostr.Subscripti return } nostrEvent.ReplyId = resp.ID - // https://github.com/nbd-wtf/go-nostr/blob/master/relay.go#L321 + if status == nostr.PublishStatusSucceeded { nostrEvent.State = NOSTR_EVENT_STATE_PUBLISH_CONFIRMED nostrEvent.RepliedAt = time.Now() @@ -199,6 +211,8 @@ func (svc *Service) HandleEvent(ctx context.Context, event *nostr.Event) (result return svc.HandleMakeInvoiceEvent(ctx, nip47Request, event, app, ss) case NIP_47_LOOKUP_INVOICE_METHOD: return svc.HandleLookupInvoiceEvent(ctx, nip47Request, event, app, ss) + case NIP_47_GET_INFO_METHOD: + return svc.HandleGetInfoEvent(ctx, nip47Request, event, app, ss) default: return svc.createResponse(event, Nip47Response{ ResultType: nip47Request.Method, @@ -220,7 +234,7 @@ func (svc *Service) createResponse(initialEvent *nostr.Event, content interface{ } resp := &nostr.Event{ PubKey: svc.cfg.IdentityPubkey, - CreatedAt: time.Now(), + CreatedAt: nostr.Now(), Kind: NIP_47_RESPONSE_KIND, Tags: nostr.Tags{[]string{"p", initialEvent.PubKey}, []string{"e", initialEvent.ID}}, Content: msg, @@ -232,6 +246,22 @@ func (svc *Service) createResponse(initialEvent *nostr.Event, content interface{ return resp, nil } +func (svc *Service) GetMethods(app *App) []string { + appPermissions := []AppPermission{} + findPermissionsResult := svc.db.Find(&appPermissions, &AppPermission{ + AppId: app.ID, + }) + if findPermissionsResult.RowsAffected == 0 { + // No permissions created for this app. It can do anything + return strings.Split(NIP_47_CAPABILITIES, ",") + } + requestMethods := make([]string, 0, len(appPermissions)) + for _, appPermission := range appPermissions { + requestMethods = append(requestMethods, appPermission.RequestMethod) + } + return requestMethods +} + func (svc *Service) hasPermission(app *App, event *nostr.Event, requestMethod string, amount int64) (result bool, code string, message string) { // find all permissions for the app appPermissions := []AppPermission{} @@ -294,15 +324,15 @@ func (svc *Service) PublishNip47Info(ctx context.Context, relay *nostr.Relay) er ev := &nostr.Event{} ev.Kind = NIP_47_INFO_EVENT_KIND ev.Content = NIP_47_CAPABILITIES - ev.CreatedAt = time.Now() + ev.CreatedAt = nostr.Now() ev.PubKey = svc.cfg.IdentityPubkey err := ev.Sign(svc.cfg.NostrSecretKey) if err != nil { return err } - status := relay.Publish(ctx, *ev) - if status != nostr.PublishStatusSucceeded { - return fmt.Errorf("Nostr publish not successful: %s", status) + status, err := relay.Publish(ctx, *ev) + if err != nil || status != nostr.PublishStatusSucceeded { + return fmt.Errorf("Nostr publish not successful: %s error: %s", status, err) } return nil } diff --git a/service_test.go b/service_test.go index e44aa50c..b005822c 100644 --- a/service_test.go +++ b/service_test.go @@ -22,6 +22,13 @@ const nip47GetBalanceJson = ` "method": "get_balance" } ` + +const nip47GetInfoJson = ` +{ + "method": "get_info" +} +` + const nip47MakeInvoiceJson = ` { "method": "make_invoice", @@ -67,7 +74,16 @@ const nip47PayJsonNoInvoice = ` const mockInvoice = "lnbc10n1pjdy9aepp5ftvu6fucndg5mp5ww4ghsduqrxgr4rtcwelrln4jzxhem5qw022qhp50kncf9zk35xg4lxewt4974ry6mudygsztsz8qn3ar8pn3mtpe50scqzzsxqyz5vqsp5zyzp3dyn98g7sjlgy4nvujq3rh9xxsagytcyx050mf3rtrx3sn4s9qyyssq7af24ljqf5uzgnz4ualxhvffryh3kpkvvj76ah9yrgdvu494lmfrdf36h5wuhshzemkvrtg2zu70uk0fd9fcmxgl3j9748dvvx9ey9gqpr4jjd" const mockPaymentHash = "4ad9cd27989b514d868e755178378019903a8d78767e3fceb211af9dd00e7a94" // for the above invoice +var mockNodeInfo = NodeInfo{ + Alias: "bob", + Color: "#3399FF", + Pubkey: "123pubkey", + Network: "testnet", + BlockHeight: 12, + BlockHash: "123blockhash", +} +// TODO: split up into individual tests func TestHandleEvent(t *testing.T) { ctx := context.TODO() svc, _ := createTestService(t) @@ -331,14 +347,6 @@ func TestHandleEvent(t *testing.T) { // make invoice: with permission err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("request_method", NIP_47_MAKE_INVOICE_METHOD).Error - assert.NoError(t, err) - appPermission = &AppPermission{ - AppId: app.ID, - App: app, - RequestMethod: NIP_47_MAKE_INVOICE_METHOD, - ExpiresAt: expiresAt, - } - err = svc.db.Create(appPermission).Error res, err = svc.HandleEvent(ctx, &nostr.Event{ ID: "test_event_13", Kind: NIP_47_REQUEST_KIND, @@ -379,15 +387,56 @@ func TestHandleEvent(t *testing.T) { // lookup invoice: with permission err = svc.db.Model(&AppPermission{}).Where("app_id = ?", app.ID).Update("request_method", NIP_47_LOOKUP_INVOICE_METHOD).Error assert.NoError(t, err) + res, err = svc.HandleEvent(ctx, &nostr.Event{ + ID: "test_event_15", + Kind: NIP_47_REQUEST_KIND, + PubKey: senderPubkey, + Content: newPayload, + }) + assert.NoError(t, err) + assert.NotNil(t, res) + decrypted, err = nip04.Decrypt(res.Content, ss) + assert.NoError(t, err) + received = &Nip47Response{ + Result: &Nip47LookupInvoiceResponse{}, + } + err = json.Unmarshal([]byte(decrypted), received) + assert.NoError(t, err) + assert.Equal(t, mockInvoice, received.Result.(*Nip47LookupInvoiceResponse).Invoice) + assert.Equal(t, false, received.Result.(*Nip47LookupInvoiceResponse).Paid) + + // get info: without permission + newPayload, err = nip04.Encrypt(nip47GetInfoJson, ss) + assert.NoError(t, err) + res, err = svc.HandleEvent(ctx, &nostr.Event{ + ID: "test_event_16", + Kind: NIP_47_REQUEST_KIND, + PubKey: senderPubkey, + Content: newPayload, + }) + assert.NoError(t, err) + assert.NotNil(t, res) + decrypted, err = nip04.Decrypt(res.Content, ss) + assert.NoError(t, err) + received = &Nip47Response{} + err = json.Unmarshal([]byte(decrypted), received) + assert.NoError(t, err) + assert.Equal(t, NIP_47_ERROR_RESTRICTED, received.Error.Code) + assert.NotNil(t, res) + + // delete all permissions + svc.db.Exec("delete from app_permissions") + + // lookup invoice: with permission appPermission = &AppPermission{ AppId: app.ID, App: app, - RequestMethod: NIP_47_LOOKUP_INVOICE_METHOD, + RequestMethod: NIP_47_GET_INFO_METHOD, ExpiresAt: expiresAt, } err = svc.db.Create(appPermission).Error res, err = svc.HandleEvent(ctx, &nostr.Event{ - ID: "test_event_15", + ID: "test_event_17", Kind: NIP_47_REQUEST_KIND, PubKey: senderPubkey, Content: newPayload, @@ -397,12 +446,17 @@ func TestHandleEvent(t *testing.T) { decrypted, err = nip04.Decrypt(res.Content, ss) assert.NoError(t, err) received = &Nip47Response{ - Result: &Nip47LookupInvoiceResponse{}, + Result: &Nip47GetInfoResponse{}, } err = json.Unmarshal([]byte(decrypted), received) assert.NoError(t, err) - assert.Equal(t, mockInvoice, received.Result.(*Nip47LookupInvoiceResponse).Invoice) - assert.Equal(t, false, received.Result.(*Nip47LookupInvoiceResponse).Paid) + assert.Equal(t, mockNodeInfo.Alias, received.Result.(*Nip47GetInfoResponse).Alias) + assert.Equal(t, mockNodeInfo.Color, received.Result.(*Nip47GetInfoResponse).Color) + assert.Equal(t, mockNodeInfo.Pubkey, received.Result.(*Nip47GetInfoResponse).Pubkey) + assert.Equal(t, mockNodeInfo.Network, received.Result.(*Nip47GetInfoResponse).Network) + assert.Equal(t, mockNodeInfo.BlockHeight, received.Result.(*Nip47GetInfoResponse).BlockHeight) + assert.Equal(t, mockNodeInfo.BlockHash, received.Result.(*Nip47GetInfoResponse).BlockHash) + assert.Equal(t, []string{"get_info"}, received.Result.(*Nip47GetInfoResponse).Methods) } func createTestService(t *testing.T) (svc *Service, ln *MockLn) { @@ -449,6 +503,10 @@ func (mln *MockLn) GetBalance(ctx context.Context, senderPubkey string) (balance return 21, nil } +func (mln *MockLn) GetInfo(ctx context.Context, senderPubkey string) (info *NodeInfo, err error) { + return &mockNodeInfo, nil +} + func (mln *MockLn) MakeInvoice(ctx context.Context, senderPubkey string, amount int64, description string, descriptionHash string, expiry int64) (invoice string, paymentHash string, err error) { return mockInvoice, mockPaymentHash, nil } diff --git a/views/layout.html b/views/layout.html index 5f1ede58..b9746008 100644 --- a/views/layout.html +++ b/views/layout.html @@ -7,57 +7,71 @@