Merge branch 'task-react' into server-self-host

This commit is contained in:
Roland Bewick 2024-01-15 11:47:41 +07:00
commit c0cce751e2
58 changed files with 788 additions and 631 deletions

2
.dockerignore Normal file
View file

@ -0,0 +1,2 @@
frontend/node_modules
frontend/dist

View file

@ -2,6 +2,7 @@ DATABASE_URI=file:nwc.db
NOSTR_PRIVKEY=
COOKIE_SECRET=secretsecret
RELAY=wss://relay.getalby.com/v1
#RELAY=ws://localhost:7447/v1
PUBLIC_RELAY=
PORT=8080

View file

@ -10,23 +10,23 @@ jobs:
IMAGENAME: ${{ github.event.repository.name }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
name: Check out code
- name: Install Go
uses: actions/setup-go@v1
with:
go-version: 1.20.x
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests
run: go test
- name: Docker build
uses: mr-smithers-excellent/docker-build-push@v6
id: build
with:
image: ${{ env.IMAGENAME }}
registry: ${{ env.REGISTRY }}
multiPlatform: true
platform: linux/amd64,linux/arm64,linux/386
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@v2
name: Check out code
- name: Install Go
uses: actions/setup-go@v1
with:
go-version: 1.20.x
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests
run: mkdir frontend/dist && touch frontend/dist/tmp && go test
- name: Docker build
uses: mr-smithers-excellent/docker-build-push@v6
id: build
with:
image: ${{ env.IMAGENAME }}
registry: ${{ env.REGISTRY }}
multiPlatform: true
platform: linux/amd64,linux/arm64
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

View file

@ -10,55 +10,55 @@ jobs:
IMAGENAME: ${{ github.event.repository.name }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
name: Check out code
- name: Install Go
uses: actions/setup-go@v1
with:
go-version: 1.20.x
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests
run: go test
- name: Docker build
uses: mr-smithers-excellent/docker-build-push@v6
id: build
with:
image: ${{ env.IMAGENAME }}
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout deployment repo
uses: actions/checkout@v2
with:
repository: getalby/alby-deployment
path: infrastructure
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
# Always update dev environment
- name: Update dev environment
if: ${{ github.ref == 'refs/heads/main' }}
uses: fjogeleit/yaml-update-action@v0.7.0
with:
valueFile: 'alby-simnet-deployment/values.yaml'
propertyPath: 'nwc.image.tag'
value: ${{ steps.build.outputs.tags }}
repository: getalby/alby-deployment
branch: main
createPR: false
message: 'CD: Update nwc tag to ${{ steps.build.outputs.tags }}'
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
workDir: infrastructure
# Only update prod environment if this action was triggered by a new tag
- name: Update production environment
if: startsWith(github.ref, 'refs/tags')
uses: fjogeleit/yaml-update-action@v0.7.0
with:
valueFile: 'alby-mainnet-deployment/values.yaml'
propertyPath: 'nwc.image.tag'
value: ${{ steps.build.outputs.tags }}
repository: getalby/alby-deployment
branch: main
createPR: false
message: 'CD: Update nwc tag to ${{ steps.build.outputs.tags }}'
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
workDir: infrastructure
- uses: actions/checkout@v2
name: Check out code
- name: Install Go
uses: actions/setup-go@v1
with:
go-version: 1.20.x
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests
run: mkdir frontend/dist && touch frontend/dist/tmp && go test
- name: Docker build
uses: mr-smithers-excellent/docker-build-push@v6
id: build
with:
image: ${{ env.IMAGENAME }}
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout deployment repo
uses: actions/checkout@v2
with:
repository: getalby/alby-deployment
path: infrastructure
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
# Always update dev environment
- name: Update dev environment
if: ${{ github.ref == 'refs/heads/main' }}
uses: fjogeleit/yaml-update-action@v0.7.0
with:
valueFile: "alby-simnet-deployment/values/nwc.yaml"
propertyPath: "nwc.image.tag"
value: ${{ steps.build.outputs.tags }}
repository: getalby/alby-deployment
branch: main
createPR: false
message: "CD: Update nwc tag to ${{ steps.build.outputs.tags }}"
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
workDir: infrastructure
# Only update prod environment if this action was triggered by a new tag
- name: Update production environment
if: startsWith(github.ref, 'refs/tags')
uses: fjogeleit/yaml-update-action@v0.7.0
with:
valueFile: "alby-mainnet-deployment/values.yaml"
propertyPath: "nwc.image.tag"
value: ${{ steps.build.outputs.tags }}
repository: getalby/alby-deployment
branch: main
createPR: false
message: "CD: Update nwc tag to ${{ steps.build.outputs.tags }}"
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
workDir: infrastructure

View file

@ -1,3 +1,8 @@
FROM node:18-alpine as frontend
WORKDIR /build
COPY frontend ./frontend
RUN cd frontend && yarn install && yarn build
FROM golang:latest as builder
RUN apt-get update && \
@ -18,9 +23,10 @@ RUN go mod download
# Copy the code into the container
COPY . .
RUN go build -o main .
# TODO: build react app?
# Copy frontend dist files into the container
COPY --from=frontend /build/frontend/dist ./frontend/dist
RUN go build -o main .
# Start a new, final image to reduce size.
FROM alpine as final
@ -31,8 +37,5 @@ FROM alpine as final
# Copy the binaries and entrypoint from the builder image.
COPY --from=builder /build/main /bin/
# NOTE: should not be needed - assets should be embedded in the go app
#COPY --from=builder /build/public /public/
#COPY --from=builder /build/views /views/
ENTRYPOINT [ "/bin/main" ]

View file

@ -50,6 +50,21 @@ Go to `/frontend`
Follow standard LND instructions. After logging in, you will be redirected to the wrong port (8080), so manually re-open <http://localhost:5173>.
### Build and run locally
`mkdir tmp`
`go build -o main`
`cp main tmp`
`cp .env tmp`
`cd tmp`
`./main`
### Run dockerfile locally
`docker build . -t nwc-local`
`docker run --env-file .env -p 8080:8080 nwc-local`
### Testing
`go test`
@ -139,7 +154,7 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light
✅ NIP-47 info event
`expires` tag in requests
`expiration` tag in requests
### LND
@ -149,6 +164,8 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light
`pay_invoice`
- ⚠️ amount not supported (for amountless invoices)
`pay_keysend`
`make_invoice`
@ -178,6 +195,8 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light
`pay_invoice`
- ⚠️ amount not supported (for amountless invoices)
`pay_keysend`
- ⚠️ preimage in request not supported
@ -199,3 +218,10 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light
`multi_pay_invoice`
`multi_pay_keysend (TBC)`
## Node Distributions
Run NWC on your own node!
- [https://github.com/getAlby/umbrel-community-app-store](Umbrel)
- [https://github.com/horologger/nostr-wallet-connect-startos](Start9) (WIP)

20
alby.go
View file

@ -24,7 +24,7 @@ type AlbyOAuthService struct {
Logger *logrus.Logger
}
func NewAlbyOauthService(svc *Service, e *echo.Echo) (result *AlbyOAuthService, err error) {
func NewAlbyOauthService(svc *Service, e *echo.Echo) (result LNClient, err error) {
conf := &oauth2.Config{
ClientID: svc.cfg.AlbyClientId,
ClientSecret: svc.cfg.AlbyClientSecret,
@ -709,6 +709,18 @@ func albyInvoiceToTransaction(invoice *AlbyInvoice) *Nip47Transaction {
preimage = invoice.Preimage
}
var expiresAt *int64
if invoice.ExpiresAt != nil {
expiresAtUnix := invoice.ExpiresAt.Unix()
expiresAt = &expiresAtUnix
}
var settledAt *int64
if invoice.SettledAt != nil {
settledAtUnix := invoice.SettledAt.Unix()
settledAt = &settledAtUnix
}
return &Nip47Transaction{
Type: invoice.Type,
Invoice: invoice.PaymentRequest,
@ -718,9 +730,9 @@ func albyInvoiceToTransaction(invoice *AlbyInvoice) *Nip47Transaction {
PaymentHash: invoice.PaymentHash,
Amount: invoice.Amount * 1000,
FeesPaid: 0, // TODO: support fees
CreatedAt: invoice.CreatedAt,
ExpiresAt: invoice.ExpiresAt,
SettledAt: invoice.SettledAt,
CreatedAt: invoice.CreatedAt.Unix(),
ExpiresAt: expiresAt,
SettledAt: settledAt,
Metadata: invoice.Metadata,
}
}

View file

@ -1,18 +1,15 @@
package main
import (
"embed"
"encoding/hex"
"errors"
"fmt"
"io/fs"
"net/http"
"net/url"
"strconv"
"strings"
"time"
echologrus "github.com/davrux/echo-logrus/v4"
// "github.com/getAlby/lndhub.go/lib/responses"
"github.com/getAlby/nostr-wallet-connect/frontend"
"github.com/getAlby/nostr-wallet-connect/models/api"
"github.com/gorilla/sessions"
@ -25,8 +22,21 @@ import (
"gorm.io/gorm"
)
//go:embed frontend/dist/*
var embeddedAssets embed.FS
func (svc *Service) ValidateUserMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user, err := svc.GetUser(c)
if err != nil {
return c.JSON(http.StatusBadRequest, ErrorResponse{
Message: fmt.Sprintf("Bad arguments %s", err.Error()),
})
}
if user == nil {
return c.NoContent(http.StatusUnauthorized)
}
c.Set("user", user)
return next(c)
}
}
func (svc *Service) RegisterSharedRoutes(e *echo.Echo) {
e.HideBanner = true
@ -34,47 +44,75 @@ func (svc *Service) RegisterSharedRoutes(e *echo.Echo) {
e.Use(middleware.Recover())
e.Use(middleware.RequestID())
e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
TokenLookup: "header:X-CSRF-Token,form:_csrf",
TokenLookup: "header:X-CSRF-Token",
}))
e.Use(session.Middleware(sessions.NewCookieStore([]byte(svc.cfg.CookieSecret))))
e.Use(ddEcho.Middleware(ddEcho.WithServiceName("nostr-wallet-connect")))
assetSubdir, _ := fs.Sub(embeddedAssets, "public")
assetHandler := http.FileServer(http.FS(assetSubdir))
e.GET("/public/*", echo.WrapHandler(http.StripPrefix("/public/", assetHandler)))
authMiddleware := svc.ValidateUserMiddleware
e.GET("/api/user/me", svc.UserMeHandler, authMiddleware)
e.GET("/api/apps", svc.AppsListHandler, authMiddleware)
e.GET("/api/apps/:pubkey", svc.AppsShowHandler, authMiddleware)
e.DELETE("/api/apps/:pubkey", svc.AppsDeleteHandler, authMiddleware)
e.POST("/api/apps", svc.AppsCreateHandler, authMiddleware)
e.GET("/api/csrf", svc.CSRFHandler)
e.GET("/api/apps", svc.AppsListHandler)
e.GET("/api/apps/:pubkey", svc.AppsShowHandler)
e.POST("/api/apps", svc.AppsCreateHandler)
e.DELETE("/api/apps/:pubkey", svc.AppsDeleteHandler)
e.GET("/api/user/me", svc.UserMeHandler)
e.GET("/api/info", svc.InfoHandler)
e.POST("/api/logout", svc.LogoutHandler)
frontend.RegisterHandlers(e)
}
func (svc *Service) AboutHandler(c echo.Context) error {
user, err := svc.GetUser(c)
if err != nil {
return err
func (svc *Service) CSRFHandler(c echo.Context) error {
csrf, _ := c.Get(middleware.DefaultCSRFConfig.ContextKey).(string)
if csrf == "" {
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: "CSRF token not available",
})
}
return c.Render(http.StatusOK, "about.html", map[string]interface{}{
"User": user,
})
return c.JSON(http.StatusOK, csrf)
}
func (svc *Service) InfoHandler(c echo.Context) error {
responseBody := &api.InfoResponse{}
responseBody.BackendType = svc.cfg.LNBackendType
if responseBody.BackendType == "" {
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: "Backend type not available",
})
}
return c.JSON(http.StatusOK, responseBody)
}
func (svc *Service) LogoutHandler(c echo.Context) error {
sess, err := session.Get(CookieName, c)
if err != nil {
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: "Failed to get session",
})
}
sess.Options.MaxAge = -1
if svc.cfg.CookieDomain != "" {
sess.Options.Domain = svc.cfg.CookieDomain
}
if err := sess.Save(c.Request(), c.Response()); err != nil {
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: "Failed to save session",
})
}
return c.NoContent(http.StatusNoContent)
}
func (svc *Service) UserMeHandler(c echo.Context) error {
user, _ := c.Get("user").(*User)
responseBody := api.User{
Email: user.Email,
}
return c.JSON(http.StatusOK, responseBody)
}
func (svc *Service) AppsListHandler(c echo.Context) error {
user, err := svc.GetUser(c)
if err != nil {
return c.JSON(http.StatusBadRequest, ErrorResponse{
Error: true,
Message: fmt.Sprintf("Bad arguments %s", err.Error()),
})
}
if user == nil {
return c.NoContent(http.StatusUnauthorized)
}
user, _ := c.Get("user").(*User)
apps := []api.App{}
for _, app := range user.Apps {
@ -89,6 +127,11 @@ func (svc *Service) AppsListHandler(c echo.Context) error {
var lastEvent NostrEvent
result := svc.db.Where("app_id = ?", app.ID).Order("id desc").Limit(1).Find(&lastEvent)
if result.Error != nil {
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: "Failed to fetch last event",
})
}
if result.RowsAffected > 0 {
apiApp.LastEventAt = &lastEvent.CreatedAt
}
@ -99,32 +142,18 @@ func (svc *Service) AppsListHandler(c echo.Context) error {
}
func (svc *Service) AppsShowHandler(c echo.Context) error {
// csrf, _ := c.Get(middleware.DefaultCSRFConfig.ContextKey).(string)
user, err := svc.GetUser(c)
if err != nil {
return c.JSON(http.StatusBadRequest, ErrorResponse{
Error: true,
Message: fmt.Sprintf("Bad arguments %s", err.Error()),
})
}
if user == nil {
return c.NoContent(http.StatusUnauthorized)
}
user, _ := c.Get("user").(*User)
app := App{}
svc.db.Where("user_id = ? AND nostr_pubkey = ?", user.ID, c.Param("pubkey")).First(&app)
findResult := svc.db.Where("user_id = ? AND nostr_pubkey = ?", user.ID, c.Param("pubkey")).First(&app)
if app.NostrPubkey == "" {
if findResult.RowsAffected == 0 {
return c.JSON(http.StatusNotFound, ErrorResponse{
Error: true,
Message: "App does not exist",
})
}
var lastEvent NostrEvent
lastEventResult := svc.db.Where("app_id = ?", app.ID).Order("id desc").Limit(1).Find(&lastEvent)
//var eventsCount int64
//svc.db.Model(&NostrEvent{}).Where("app_id = ?", app.ID).Count(&eventsCount)
paySpecificPermission := AppPermission{}
appPermissions := []AppPermission{}
@ -170,52 +199,85 @@ func (svc *Service) AppsShowHandler(c echo.Context) error {
return c.JSON(http.StatusOK, response)
}
func (svc *Service) CSRFHandler(c echo.Context) error {
csrf, _ := c.Get(middleware.DefaultCSRFConfig.ContextKey).(string)
return c.JSON(http.StatusOK, csrf)
func (svc *Service) AppsDeleteHandler(c echo.Context) error {
user, _ := c.Get("user").(*User)
pubkey := c.Param("pubkey")
if pubkey == "" {
return c.JSON(http.StatusBadRequest, ErrorResponse{
Message: "Invalid pubkey parameter",
})
}
app := App{}
result := svc.db.Where("user_id = ? AND nostr_pubkey = ?", user.ID, pubkey).First(&app)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return c.JSON(http.StatusNotFound, ErrorResponse{
Message: "App not found",
})
}
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: "Failed to fetch app",
})
}
if err := svc.db.Delete(&app).Error; err != nil {
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: "Failed to delete app",
})
}
return c.NoContent(http.StatusNoContent)
}
func (svc *Service) AppsCreateHandler(c echo.Context) error {
user, err := svc.GetUser(c)
if err != nil {
user, _ := c.Get("user").(*User)
var requestData api.CreateAppRequest
if err := c.Bind(&requestData); err != nil {
return c.JSON(http.StatusBadRequest, ErrorResponse{
Error: true,
Message: fmt.Sprintf("Bad arguments %s", err.Error()),
Message: fmt.Sprintf("Bad request: %s", err.Error()),
})
}
if user == nil {
return c.NoContent(http.StatusUnauthorized)
}
name := c.FormValue("name")
name := requestData.Name
var pairingPublicKey string
var pairingSecretKey string
if c.FormValue("pubkey") == "" {
if requestData.Pubkey == "" {
pairingSecretKey = nostr.GeneratePrivateKey()
pairingPublicKey, _ = nostr.GetPublicKey(pairingSecretKey)
} else {
pairingPublicKey = c.FormValue("pubkey")
pairingPublicKey = requestData.Pubkey
//validate public key
decoded, err := hex.DecodeString(pairingPublicKey)
if err != nil || len(decoded) != 32 {
svc.Logger.Errorf("Invalid public key format: %s", pairingPublicKey)
return c.JSON(http.StatusBadRequest, ErrorResponse{
Error: true,
Message: fmt.Sprintf("Invalid public key format: %s", pairingPublicKey),
})
}
}
// make sure there is not already a pubkey is already associated with an app
// as apps are currently indexed by pubkey
// TODO: shouldn't this be a database constraint?
existingApp := App{}
findResult := svc.db.Where("user_id = ? AND nostr_pubkey = ?", user.ID, pairingPublicKey).First(&existingApp)
if findResult.RowsAffected > 0 {
return c.JSON(http.StatusConflict, ErrorResponse{
Message: "Pubkey already in use: " + existingApp.NostrPubkey,
})
}
app := App{Name: name, NostrPubkey: pairingPublicKey}
maxAmount, _ := strconv.Atoi(c.FormValue("maxAmount"))
budgetRenewal := c.FormValue("budgetRenewal")
maxAmount := requestData.MaxAmount
budgetRenewal := requestData.BudgetRenewal
expiresAt := time.Time{}
if c.FormValue("expiresAt") != "" {
expiresAt, err = time.Parse(time.RFC3339, c.FormValue("expiresAt"))
if requestData.ExpiresAt != "" {
var err error
expiresAt, err = time.Parse(time.RFC3339, requestData.ExpiresAt)
if err != nil {
svc.Logger.Errorf("Invalid expiresAt: %s", pairingPublicKey)
return c.JSON(http.StatusBadRequest, ErrorResponse{
Error: true,
Message: fmt.Sprintf("Invalid expiresAt: %v", err),
})
}
@ -225,13 +287,13 @@ func (svc *Service) AppsCreateHandler(c echo.Context) error {
expiresAt = time.Date(expiresAt.Year(), expiresAt.Month(), expiresAt.Day(), 23, 59, 59, 0, expiresAt.Location())
}
err = svc.db.Transaction(func(tx *gorm.DB) error {
err = tx.Model(&user).Association("Apps").Append(&app)
err := svc.db.Transaction(func(tx *gorm.DB) error {
err := tx.Model(&user).Association("Apps").Append(&app)
if err != nil {
return err
}
requestMethods := c.FormValue("requestMethods")
requestMethods := requestData.RequestMethods
if requestMethods == "" {
return fmt.Errorf("Won't create an app without request methods.")
}
@ -239,7 +301,7 @@ func (svc *Service) AppsCreateHandler(c echo.Context) error {
methodsToCreate := strings.Split(requestMethods, " ")
for _, m := range methodsToCreate {
//if we don't know this method, we return an error
if _, ok := nip47MethodDescriptions[m]; !ok {
if !strings.Contains(NIP_47_CAPABILITIES, m) {
return fmt.Errorf("Did not recognize request method: %s", m)
}
appPermission := AppPermission{
@ -265,7 +327,6 @@ func (svc *Service) AppsCreateHandler(c echo.Context) error {
"name": name,
}).Errorf("Failed to save app: %v", err)
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Error: true,
Message: fmt.Sprintf("Failed to save app: %v", err),
})
}
@ -280,8 +341,8 @@ func (svc *Service) AppsCreateHandler(c echo.Context) error {
responseBody.Pubkey = pairingPublicKey
responseBody.PairingSecret = pairingSecretKey
if c.FormValue("returnTo") != "" {
returnToUrl, err := url.Parse(c.FormValue("returnTo"))
if requestData.ReturnTo != "" {
returnToUrl, err := url.Parse(requestData.ReturnTo)
if err == nil {
query := returnToUrl.Query()
query.Add("relay", publicRelayUrl)
@ -301,50 +362,3 @@ func (svc *Service) AppsCreateHandler(c echo.Context) error {
responseBody.PairingUri = fmt.Sprintf("nostr+walletconnect://%s?relay=%s&secret=%s%s", svc.cfg.IdentityPubkey, publicRelayUrl, pairingSecretKey, lud16)
return c.JSON(http.StatusOK, responseBody)
}
func (svc *Service) AppsDeleteHandler(c echo.Context) error {
user, err := svc.GetUser(c)
// TODO: error handling
if err != nil {
return err
}
if user == nil {
return c.NoContent(http.StatusUnauthorized)
}
app := App{}
svc.db.Where("user_id = ? AND nostr_pubkey = ?", user.ID, c.Param("pubkey")).First(&app)
svc.db.Delete(&app)
return c.NoContent(http.StatusNoContent)
}
func (svc *Service) LogoutHandler(c echo.Context) error {
sess, _ := session.Get(CookieName, c)
sess.Options.MaxAge = -1
if svc.cfg.CookieDomain != "" {
sess.Options.Domain = svc.cfg.CookieDomain
}
sess.Save(c.Request(), c.Response())
return c.NoContent(http.StatusNoContent)
}
func (svc *Service) InfoHandler(c echo.Context) error {
responseBody := &api.InfoResponse{}
responseBody.BackendType = svc.cfg.LNBackendType
return c.JSON(http.StatusOK, responseBody)
}
func (svc *Service) UserMeHandler(c echo.Context) error {
user, err := svc.GetUser(c)
if err != nil {
// TODO: error handling
return err
}
if user == nil {
return c.NoContent(http.StatusNotFound)
}
responseBody := api.User{
Email: user.Email,
}
return c.JSON(http.StatusOK, responseBody)
}

View file

@ -1,23 +1,26 @@
package frontend
import (
"embed"
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
//go:embed dist
var embeddedReactAssets embed.FS
func RegisterHandlers(e *echo.Echo) {
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Skipper: nil,
// Root directory from where the static content is served.
Root: "frontend/dist",
// Index file for serving a directory.
// Optional. Default value "index.html".
Index: "index.html",
Root: "dist",
// Enable HTML5 mode by forwarding all not-found requests to root so that
// SPA (single-page application) can handle the routing.
HTML5: true,
Browse: false,
IgnoreBase: false,
Filesystem: nil,
Filesystem: http.FS(embeddedReactAssets),
}))
}

View file

@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Alby - Nostr Wallet Connect</title>
<link rel="icon" href="/src/assets/images/nwc-logo.svg" />
<link rel="icon" href="/nwc-logo.svg" />
</head>
<body>
<div id="root"></div>

View file

@ -21,6 +21,7 @@
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"@types/node": "^20.10.8",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="120" fill="none"><g clip-path="url(#a)"><path fill="#472459" d="M127.2 65.67c-5.13 5.11-12.23 3.88-17.35-1.25l-41.4-41.6 19.4-19.47a11.35 11.35 0 0 1 18.4 3.37l21.58 56.22a2.5 2.5 0 0 1-.57 2.67l-13.63 13.57 13.58-13.51Z"/><path fill="url(#b)" d="m109.85 64.42-54.16-54.4a13.13 13.13 0 0 0-18.57-.05L3.87 43.07a13.13 13.13 0 0 0-.05 18.57l54.16 54.4a13.13 13.13 0 0 0 18.57.04l10.7-10.64 3.12-3.12-10-10.03a19.04 19.04 0 0 1-23.83-2.56l-6.8-6.85a2.46 2.46 0 0 1 0-3.5l3.35-3.32-8.4-8.46a3.67 3.67 0 0 1-.34-4.9 3.57 3.57 0 0 1 5.29-.23l8.51 8.56 6.68-6.63-8.41-8.46a3.67 3.67 0 0 1-.33-4.9 3.58 3.58 0 0 1 5.3-.24l8.5 8.57 3.36-3.34a2.47 2.47 0 0 1 3.5.01l6.8 6.84a19.03 19.03 0 0 1 2.41 23.86l10 10.03 5.69-5.67 8.16-8.12 17.4-17.31c-5.15 5.11-12.25 3.88-17.36-1.25Z"/></g><defs><linearGradient id="b" x1="63.6" x2="63.6" y1="6.15" y2="119.91" gradientUnits="userSpaceOnUse"><stop stop-color="#FFCA4A"/><stop offset="1" stop-color="#F7931A"/></linearGradient><clipPath id="a"><path fill="#fff" d="M0 0h128v119.91H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,16 +1,16 @@
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import About from "./screens/About";
import AppsList from "./screens/apps/AppsList";
import ShowApp from "./screens/apps/ShowApp";
import Login from "./screens/Login";
import NotFound from "./screens/NotFound";
import Navbar from "src/components/Navbar";
import Footer from "src/components/Footer";
import Toaster from "src/components/Toast/Toaster";
import Navbar from "./components/Navbar";
import Footer from "./components/Footer";
import NewApp from "./screens/apps/NewApp";
import AppCreated from "./screens/apps/AppCreated";
import Toaster from "./components/Toast/Toaster";
import About from "src/screens/About";
import Login from "src/screens/Login";
import AppsList from "src/screens/apps/AppsList";
import ShowApp from "src/screens/apps/ShowApp";
import NewApp from "src/screens/apps/NewApp";
import AppCreated from "src/screens/apps/AppCreated";
import NotFound from "src/screens/NotFound";
function App() {
return (

View file

@ -1 +0,0 @@
Convert these to React components (see components/icons)

View file

@ -1,6 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 10.6667V16" stroke="#9ca3af" stroke-linecap="round" stroke-width="2" />
<path d="M12 8.7619V8" stroke="#9ca3af" stroke-linecap="round" stroke-width="" />
<circle cx="12" cy="12" r="9" stroke="#9ca3af" stroke-width="2" />
</svg>

Before

Width:  |  Height:  |  Size: 344 B

View file

@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512">
<style>
svg {
fill: #9ca3af;
}
</style>
<path d="M201.4 342.6c12.5 12.5 32.8 12.5 45.3 0l160-160c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L224 274.7 86.6 137.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l160 160z" />
</svg>

Before

Width:  |  Height:  |  Size: 324 B

View file

@ -1,6 +1,6 @@
import albyLogoWithText from "../assets/images/alby-logo-with-text.svg";
import albyLogoWithTextDark from "../assets/images/alby-logo-with-text-dark.svg";
import { useDarkMode } from "../hooks/useDarkMode";
import { useDarkMode } from "src/hooks/useDarkMode";
import albyLogoWithText from "src/assets/images/alby-logo-with-text.svg";
import albyLogoWithTextDark from "src/assets/images/alby-logo-with-text-dark.svg";
function Footer() {
const isDarkMode = useDarkMode();

View file

@ -1,14 +1,15 @@
import { Outlet, useLocation } from "react-router-dom";
import { useInfo } from "../hooks/useInfo";
import { useLogin } from "../hooks/useLogin";
import nwcLogo from "../assets/images/nwc-logo.svg";
import caretIcon from "../assets/icons/caret.svg";
import aboutIcon from "../assets/icons/about.svg";
import React from "react";
import { LogoutIcon } from "./icons/LogoutIcon";
import { handleFetchError, validateFetchResponse } from "../utils/fetch";
import { useCSRF } from "../hooks/useCSRF";
import { useUser } from "../hooks/useUser";
import { Outlet, useLocation } from "react-router-dom";
import { useInfo } from "src/hooks/useInfo";
import { useLogin } from "src/hooks/useLogin";
import { useCSRF } from "src/hooks/useCSRF";
import { useUser } from "src/hooks/useUser";
import { LogoutIcon } from "src/components/icons/LogoutIcon";
import { AboutIcon } from "src/components/icons/AboutIcon";
import { CaretIcon } from "src/components/icons/CaretIcon";
import nwcLogo from "src/assets/images/nwc-logo.svg";
import { handleFetchError, validateFetchResponse } from "src/utils/fetch";
function Navbar() {
const { data: info } = useInfo();
@ -16,8 +17,9 @@ function Navbar() {
const location = useLocation();
const linkStyles =
"text-gray-400 font-medium hover:text-gray-600 dark:hover:text-gray-300 transition";
const selectedLinkStyles = "text-gray-900 dark:text-gray-100";
"font-medium text-gray-400 hover:text-gray-500 dark:hover:text-gray-300 transition px-2 ";
const selectedLinkStyles =
"text-gray-900 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-100";
return (
<>
@ -25,7 +27,7 @@ function Navbar() {
<div className="bg-white border-b border-gray-200 dark:bg-surface-01dp dark:border-neutral-700 mb-6">
<nav className="container max-w-screen-lg mx-auto px-4 lg:px-0 py-3">
<div className="flex justify-between items-center">
<div className="flex items-center space-x-12 justify-center">
<div className="flex items-center space-x-12">
<a
href="/"
className="font-headline text-[20px] dark:text-white flex gap-2 justify-center items-center"
@ -47,7 +49,8 @@ function Navbar() {
>
<a
className={`${linkStyles} ${
location.pathname === "/apps" && selectedLinkStyles
location.pathname.startsWith("/apps") &&
selectedLinkStyles
}`}
href="/apps"
>
@ -109,16 +112,14 @@ function ProfileDropdown() {
return (
<div className="flex items-center relative">
<p
className="text-gray-400 text-xs font-medium sm:text-base cursor-pointer select-none "
className="text-gray-400 text-xs font-medium sm:text-base cursor-pointer select-none"
onClick={() => setOpen((current) => !current)}
>
<span>{user.email}</span>
<img
id="caret"
<span className="mx-1">{user.email}</span>
<CaretIcon
className={`inline cursor-pointer w-4 ml-2 ${
isOpen ? "rotate-180" : ""
}`}
src={caretIcon}
/>
</p>
@ -131,11 +132,7 @@ function ProfileDropdown() {
className="md:hidden flex items-center justify-left py-2 text-gray-400 dark:text-gray-400"
href="/about"
>
<img
className="inline cursor-pointer w-4 mr-3"
src={aboutIcon}
alt="about-svg"
/>
<AboutIcon className="inline cursor-pointer w-4 mr-3" />
<p className="font-normal">About</p>
</a>

View file

@ -1,5 +1,6 @@
import ReactQRCode from "react-qr-code";
import { useDarkMode } from "../hooks/useDarkMode";
import { useDarkMode } from "src/hooks/useDarkMode";
export type Props = {
value: string;
@ -19,7 +20,6 @@ export type Props = {
function QRCode({ value, size, level, className }: Props) {
const isDarkMode = useDarkMode();
console.log(isDarkMode);
const fgColor = isDarkMode ? "#FFFFFF" : "#242424";
const bgColor = isDarkMode ? "#242424" : "#FFFFFF";

View file

@ -0,0 +1,19 @@
import { SVGAttributes } from "react";
export function AboutIcon(props: SVGAttributes<SVGElement>) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<circle cx="12" cy="12" r="8.5" />
<path strokeLinecap="round" d="M12 10.5v7M12 8V7"></path>
</svg>
);
}

View file

@ -0,0 +1,22 @@
import { SVGAttributes } from "react";
export function CaretIcon(props: SVGAttributes<SVGElement>) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
strokeWidth="1"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
d="M12.248 8.237a.25.25 0 00-.354 0L5.53 14.601a.75.75 0 11-1.06-1.06l6.363-6.364a1.75 1.75 0 012.475 0l6.364 6.364a.75.75 0 01-1.06 1.06l-6.364-6.364z"
clipRule="evenodd"
/>
</svg>
);
}

View file

@ -11,8 +11,8 @@ export function CheckIcon(props: SVGAttributes<SVGElement>) {
{...props}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M18.3814 5.35432C18.738 5.56495 18.8564 6.02484 18.6458 6.3815L11.5591 18.3815C11.4426 18.5787 11.2424 18.7119 11.0156 18.7431C10.7887 18.7743 10.56 18.7002 10.3946 18.5418L5.48126 13.8366C5.18211 13.5501 5.17184 13.0753 5.45833 12.7761C5.74482 12.477 6.21958 12.4667 6.51874 12.7532L10.7487 16.804L17.3542 5.61874C17.5648 5.26208 18.0247 5.14369 18.3814 5.35432Z"
fill="black"
/>

View file

@ -12,8 +12,8 @@ export function EyeIcon(props: SVGAttributes<SVGElement>) {
>
<path d="M12 14a2 2 0 100-4 2 2 0 000 4z" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M21 12c0 2.761-4.03 5-9 5s-9-2.239-9-5 4.03-5 9-5 9 2.239 9 5zm-5 0a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>

View file

@ -11,8 +11,8 @@ export function InvoiceIcon(props: SVGAttributes<SVGElement>) {
{...props}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M7.5 4A1.5 1.5 0 006 5.5v13A1.5 1.5 0 007.5 20h9a1.5 1.5 0 001.5-1.5v-13A1.5 1.5 0 0016.5 4h-9zm6.854 4.354a.5.5 0 00-.708-.708l-4 4a.5.5 0 00.708.708l4-4zM11.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm2 4a1 1 0 100-2 1 1 0 000 2zm-5 2.5a.5.5 0 01.5-.5h6a.5.5 0 010 1H9a.5.5 0 01-.5-.5zm.5 1.5a.5.5 0 000 1h6a.5.5 0 000-1H9z"
/>
</svg>

View file

@ -11,8 +11,8 @@ export function LogoIcon(props: SVGAttributes<SVGElement>) {
{...props}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M13.6281 5.03035L20.9945 12.43L20.9956 12.431C21.4028 12.84 21.8601 13.0639 22.279 13.1062C22.6711 13.1459 23.0667 13.0322 23.4219 12.6949L19.4029 2.22631C18.9491 1.2345 17.6421 0.998839 16.8705 1.7739L13.6281 5.03035ZM20.5966 12.8277L20.5967 12.8278L20.5965 12.828C21.5558 13.7914 22.8871 14.0212 23.8507 13.0622L20.5884 16.3091L19.0591 17.8312L17.9915 18.8938L16.1182 17.0137C17.026 15.6333 16.8781 13.76 15.6662 12.5406L14.3908 11.2573C14.2095 11.0748 13.9176 11.074 13.7351 11.2553L13.1055 11.8809L11.5093 10.2748C11.2323 9.99609 10.7744 10.0126 10.5176 10.3196C10.2928 10.5907 10.3315 10.9906 10.5786 11.2392L12.1549 12.8252L10.9035 14.0685L9.3073 12.4624C9.03028 12.1837 8.5705 12.1981 8.31561 12.5072C8.0928 12.7764 8.13155 13.1762 8.37864 13.4249L9.95488 15.0109L9.32725 15.6346C9.14479 15.8159 9.14392 16.1077 9.32522 16.2902L10.6006 17.5735C11.8123 18.7927 13.6847 18.9526 15.0709 18.054L16.9452 19.9351L16.3577 20.5197L14.3527 22.5153C13.3889 23.4743 11.8301 23.4707 10.8708 22.5072L0.717064 12.3077C-0.242097 11.344 -0.238627 9.78546 0.725161 8.82627L6.96087 2.61994C7.92465 1.66089 9.4834 1.66451 10.4427 2.62803L20.5966 12.8277Z"
fill="currentColor"
/>

View file

@ -11,13 +11,13 @@ export function SearchIcon(props: SVGAttributes<SVGElement>) {
{...props}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M10.5 5.5a5 5 0 100 10 5 5 0 000-10zm-6.5 5a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0z"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M14.47 14.47a.75.75 0 011.06 0l4 4a.75.75 0 11-1.06 1.06l-4-4a.75.75 0 010-1.06z"
/>
</svg>

View file

@ -12,20 +12,20 @@ export function TransactionsIcon(props: SVGAttributes<SVGElement>) {
>
<circle cx="5.5" cy="7.5" r="1.5" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M8 6.5a.5.5 0 01.5-.5h11a.5.5 0 010 1h-11a.5.5 0 01-.5-.5zM8 8.5a.5.5 0 01.5-.5h6a.5.5 0 010 1h-6a.5.5 0 01-.5-.5z"
/>
<circle cx="5.5" cy="12" r="1.5" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M8 11a.5.5 0 01.5-.5h8a.5.5 0 010 1h-8A.5.5 0 018 11zM8 13a.5.5 0 01.5-.5h7a.5.5 0 010 1h-7A.5.5 0 018 13z"
/>
<circle cx="5.5" cy="16.5" r="1.5" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M8 15.5a.5.5 0 01.5-.5H18a.5.5 0 010 1H8.5a.5.5 0 01-.5-.5zM8 17.5a.5.5 0 01.5-.5h4a.5.5 0 010 1h-4a.5.5 0 01-.5-.5z"
/>
</svg>

View file

@ -11,13 +11,13 @@ export function WalletIcon(props: SVGAttributes<SVGElement>) {
{...props}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M12 8a2 2 0 012-2h4a2 2 0 012 2v8a2 2 0 01-2 2h-4a2 2 0 01-2-2V8zm2-1a1 1 0 00-1 1v8a1 1 0 001 1h4a1 1 0 001-1V8a1 1 0 00-1-1h-4z"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M5.5 6A1.5 1.5 0 004 7.5v9A1.5 1.5 0 005.5 18h10a1.5 1.5 0 001.5-1.5v-9A1.5 1.5 0 0015.5 6h-10zm2 7.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"
/>
</svg>

View file

@ -1,6 +1,7 @@
import useSWR from "swr";
import { App } from "../types";
import { swrFetcher } from "../swr";
import { swrFetcher } from "src/utils/swr";
import { App } from "src/types";
export function useApp(pubkey: string | undefined) {
return useSWR<App>(pubkey && `/api/apps/${pubkey}`, swrFetcher);

View file

@ -1,6 +1,7 @@
import useSWR from "swr";
import { App } from "../types";
import { swrFetcher } from "../swr";
import { swrFetcher } from "src/utils/swr";
import { App } from "src/types";
export function useApps() {
return useSWR<App[]>("/api/apps", swrFetcher);

View file

@ -1,5 +1,6 @@
import useSWR from "swr";
import { swrFetcher } from "../swr";
import { swrFetcher } from "src/utils/swr";
export function useCSRF() {
return useSWR<string>("/api/csrf", swrFetcher);

View file

@ -1,6 +1,7 @@
import useSWR from "swr";
import { InfoResponse } from "../types";
import { swrFetcher } from "../swr";
import { swrFetcher } from "src/utils/swr";
import { InfoResponse } from "src/types";
export function useInfo() {
return useSWR<InfoResponse>("/api/info", swrFetcher);

View file

@ -1,6 +1,7 @@
import { useLocation, useNavigate } from "react-router-dom";
import React from "react";
import { useUser } from "./useUser";
import { useLocation, useNavigate } from "react-router-dom";
import { useUser } from "src/hooks/useUser";
const RETURN_TO_KEY = "returnTo";

View file

@ -1,6 +1,7 @@
import useSWR from "swr";
import { swrFetcher } from "../swr";
import { User } from "../types";
import { swrFetcher } from "src/utils/swr";
import { User } from "src/types";
export function useUser() {
return useSWR<User>("/api/user/me", swrFetcher);

View file

@ -1,4 +1,4 @@
import nostrBitcoin from "../assets/images/nostr-bitcoin.png";
import nostrBitcoin from "src/assets/images/nostr-bitcoin.png";
function About() {
return (
@ -93,8 +93,8 @@ function About() {
>
Amethyst
</a>
is the first Nostr Client to fully support Nostr Wallet Connect. More
are working on support for it and are following shortly.
&nbsp;is the first Nostr Client to fully support Nostr Wallet Connect.
More are working on support for it and are following shortly.
</p>
<h4 className="font-bold text-l font-headline mt-6 dark:text-white">
@ -119,14 +119,14 @@ function About() {
self-hosted.
<br />
We are looking for contributors to add support for more wallet
backends. If you're interested please contact us
backends. If you're interested please contact us&nbsp;
<a
href="https://github.com/getAlby/nostr-wallet-connect/"
className="dark:text-gray-300 underline"
>
on GitHub
</a>
or
&nbsp;or&nbsp;
<a
href="mailto:hello@getalby.com"
className="dark:text-gray-300 underline"

View file

@ -1,6 +1,6 @@
import nwcLogo from "../assets/images/nwc-logo.svg";
import albyHead from "../assets/images/alby-head.svg";
import { useInfo } from "../hooks/useInfo";
import nwcLogo from "src/assets/images/nwc-logo.svg";
import albyHead from "src/assets/images/alby-head.svg";
import { useInfo } from "src/hooks/useInfo";
function Login() {
const { data: info } = useInfo();

View file

@ -1,15 +1,19 @@
function NotFound() {
return (
<div className="bg-white rounded-md shadow px-4 lg:p-8 dark:bg-surface-02dp">
<h3 className="font-bold text-2xl font-headline mb-4 dark:text-white">
404 Page Not Found
</h3>
<p className="leading-relaxed dark:text-neutral-400 mb-4">
The page you are looking for does not exist.
</p>
<a href="/" className="text-purple-700 dark:text-purple-400">
Return Home
</a>
<div className="flex justify-center">
<div className="container max-w-screen-lg">
<div className="bg-white rounded-md shadow my-4 px-4 lg:p-8 dark:bg-surface-02dp">
<h3 className="font-bold text-2xl font-headline mb-4 dark:text-white">
404 Page Not Found
</h3>
<p className="leading-relaxed dark:text-neutral-400 mb-4">
The page you are looking for does not exist.
</p>
<a href="/" className="text-purple-700 dark:text-purple-400">
Return Home
</a>
</div>
</div>
</div>
);
}

View file

@ -1,11 +1,11 @@
import { useEffect, useRef, useState } from "react";
import { useLocation, Navigate } from "react-router-dom";
import { CreateAppResponse } from "../../types";
import { EyeIcon } from "../../components/icons/EyeIcon";
import { CopyIcon } from "../../components/icons/CopyIcon";
import { LogoIcon } from "../../components/icons/LogoIcon";
import QRCode from "../../components/QRCode";
import { CreateAppResponse } from "src/types";
import { EyeIcon } from "src/components/icons/EyeIcon";
import { CopyIcon } from "src/components/icons/CopyIcon";
import { LogoIcon } from "src/components/icons/LogoIcon";
import QRCode from "src/components/QRCode";
export default function AppCreated() {
const { state } = useLocation();
@ -15,6 +15,25 @@ export default function AppCreated() {
const [isPopupVisible, setPopupVisible] = useState(false);
const popupRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// dispatch a success event which can be listened to by the opener or by the app that embedded the webview
// this gives those apps the chance to know the user has enabled the connection
const nwcEvent = new CustomEvent("nwc:success", { detail: {} });
window.dispatchEvent(nwcEvent);
// notify the opener of the successful connection
if (window.opener) {
window.opener.postMessage(
{
type: "nwc:success",
payload: { success: true },
},
"*"
);
}
}, []);
// TODO: use a modal library instead of doing this manually
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
@ -31,7 +50,7 @@ export default function AppCreated() {
}, []);
if (!createAppResponse) {
return <Navigate to="/apps/new" state={{ from: "/apps/success" }} />;
return <Navigate to="/apps/new" />;
}
const pairingUri = createAppResponse.pairingUri;
@ -63,7 +82,7 @@ export default function AppCreated() {
};
return (
<div className="w-full max-w-screen-sm mx-auto">
<div className="w-full max-w-screen-sm mx-auto px-4 md:px-8">
<h2 className="font-bold text-2xl font-headline mb-2 dark:text-white text-center">
🚀 Almost there!
</h2>

View file

@ -1,8 +1,9 @@
import Loading from "../../components/Loading";
import { useApps } from "../../hooks/useApps";
import { PlusIcon } from "../../components/icons/PlusIcon";
import { useNavigate } from "react-router-dom";
import { useApps } from "src/hooks/useApps";
import { PlusIcon } from "src/components/icons/PlusIcon";
import Loading from "src/components/Loading";
function AppsList() {
const { data: apps } = useApps();
@ -14,7 +15,7 @@ function AppsList() {
return (
<>
<div className="mb-4 flex justify-between items-center">
<div className="mb-4 flex justify-between items-center px-4 md:px-8">
<h2 className="font-bold text-2xl font-headline dark:text-white">
Connected apps
</h2>

View file

@ -1,6 +1,7 @@
import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import toast from "react-hot-toast";
import {
BudgetRenewalType,
CreateAppResponse,
@ -14,26 +15,24 @@ import {
RequestMethodType,
nip47MethodDescriptions,
validBudgetRenewals,
} from "../../types";
import { handleFetchError, validateFetchResponse } from "../../utils/fetch";
import { useCSRF } from "../../hooks/useCSRF";
import { WalletIcon } from "../../components/icons/WalletIcon";
import { LightningIcon } from "../../components/icons/LightningIcon";
import { InvoiceIcon } from "../../components/icons/InvoiceIcon";
import { SearchIcon } from "../../components/icons/SearchIcon";
import { TransactionsIcon } from "../../components/icons/TransactionsIcon";
import { EditIcon } from "../../components/icons/EditIcon";
import { useUser } from "../../hooks/useUser";
} from "src/types";
import { useCSRF } from "src/hooks/useCSRF";
import { EditIcon } from "src/components/icons/EditIcon";
import { WalletIcon } from "src/components/icons/WalletIcon";
import { LightningIcon } from "src/components/icons/LightningIcon";
import { InvoiceIcon } from "src/components/icons/InvoiceIcon";
import { SearchIcon } from "src/components/icons/SearchIcon";
import { TransactionsIcon } from "src/components/icons/TransactionsIcon";
import { handleFetchError, validateFetchResponse } from "src/utils/fetch";
const NewApp = () => {
const { data: csrf } = useCSRF();
const { data: user } = useUser();
const navigate = useNavigate();
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const [edit, setEdit] = useState(false);
const [isEditing, setEditing] = useState(false);
const nameParam = (queryParams.get("name") || queryParams.get("c")) ?? "";
const [appName, setAppName] = useState(nameParam);
@ -49,9 +48,20 @@ const NewApp = () => {
? budgetRenewalParam
: "monthly";
const reqMethodsParam = queryParams.get("request_methods");
const parseRequestMethods = (reqParam: string): Set<RequestMethodType> => {
const methods = reqParam
? reqParam.split(" ")
: Object.keys(nip47MethodDescriptions);
// Create a Set of RequestMethodType from the array
const requestMethodsSet = new Set<RequestMethodType>(
methods as RequestMethodType[]
);
return requestMethodsSet;
};
const reqMethodsParam = queryParams.get("request_methods") ?? "";
const [requestMethods, setRequestMethods] = useState(
reqMethodsParam ?? Object.keys(nip47MethodDescriptions).join(" ")
parseRequestMethods(reqMethodsParam)
);
const maxAmountParam = queryParams.get("max_amount") ?? "";
@ -59,26 +69,17 @@ const NewApp = () => {
parseInt(maxAmountParam || "100000")
);
const parseExpiresParam = (expiresParam: string): string => {
if (/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(expiresParam)) {
const d = new Date(expiresParam);
const isIso =
d instanceof Date &&
!isNaN(d.getTime()) &&
d.toISOString() === expiresAtParam;
if (isIso) {
return expiresParam;
}
const parseExpiresParam = (expiresParam: string): Date | undefined => {
const expiresParamTimestamp = parseInt(expiresParam);
if (!isNaN(expiresParamTimestamp)) {
return new Date(expiresParamTimestamp * 1000);
}
if (!isNaN(parseInt(expiresParam))) {
return new Date(parseInt(expiresAtParam as string) * 1000).toISOString();
}
return "";
return undefined;
};
// Only timestamp in seconds or ISO string is expected
const expiresAtParam = parseExpiresParam(queryParams.get("expires_at") ?? "");
const [expiresAt, setExpiresAt] = useState(expiresAtParam ?? "");
const [expiresAt, setExpiresAt] = useState<Date | undefined>(
parseExpiresParam(queryParams.get("expires_at") ?? "")
);
const [days, setDays] = useState(0);
const [expireOptions, setExpireOptions] = useState(false);
@ -86,27 +87,28 @@ const NewApp = () => {
const handleDays = (days: number) => {
setDays(days);
if (!days) {
setExpiresAt("");
setExpiresAt(undefined);
return;
}
const expiryDate = new Date(today.getTime() + days * 24 * 60 * 60 * 1000);
setExpiresAt(expiryDate.toISOString());
setExpiresAt(expiryDate);
};
const handleRequestMethodChange = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rm = event.target.value;
if (requestMethods.includes(rm)) {
const requestMethod = event.target.value as RequestMethodType;
const newRequestMethods = new Set(requestMethods);
if (newRequestMethods.has(requestMethod)) {
// If checked and item is already in the list, remove it
const newMethods = requestMethods
.split(" ")
.filter((reqMethod) => reqMethod !== rm)
.join(" ");
setRequestMethods(newMethods);
newRequestMethods.delete(requestMethod);
} else {
setRequestMethods(`${requestMethods} ${rm}`);
newRequestMethods.add(requestMethod);
}
setRequestMethods(newRequestMethods);
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
@ -115,37 +117,31 @@ const NewApp = () => {
throw new Error("No CSRF token");
}
const formData = new FormData();
formData.append("name", appName);
formData.append("maxAmount", maxAmount.toString());
formData.append("budgetRenewal", budgetRenewal);
formData.append("expiresAt", expiresAt);
formData.append("requestMethods", requestMethods);
formData.append("pubkey", pubkey);
formData.append("returnTo", returnTo);
try {
const response = await fetch("/api/apps", {
method: "POST",
headers: {
"X-CSRF-Token": csrf,
"Content-Type": "application/json",
},
body: formData,
// TODO: send consider sending JSON data
/*body: JSON.stringify({
body: JSON.stringify({
name: appName,
pubkey: pubkey,
maxAmount: maxAmount.toString(),
budgetRenewal: budgetRenewal,
expiresAt: expiresAt,
requestMethods: requestMethods,
pubkey,
maxAmount,
budgetRenewal,
expiresAt: expiresAt?.toISOString(),
requestMethods: [...requestMethods].join(" "),
returnTo: returnTo,
}),*/
}),
});
await validateFetchResponse(response);
const createAppResponse: CreateAppResponse = await response.json();
if (createAppResponse.returnTo) {
// open connection URI directly in an app
window.location.href = createAppResponse.returnTo;
return;
}
navigate("/apps/created", {
state: createAppResponse,
});
@ -182,12 +178,11 @@ const NewApp = () => {
return (
<div>
<h2 className="font-bold text-2xl font-headline mb-4 dark:text-white">
{nameParam ? `Connect to ${appName}` : "Connect a new app"}
</h2>
<form onSubmit={handleSubmit} acceptCharset="UTF-8">
<div className="bg-white dark:bg-surface-02dp rounded-md shadow p-4 md:p-8">
<h2 className="font-bold text-2xl font-headline mb-4 dark:text-white">
{nameParam ? `Connect to ${appName}` : "Connect a new app"}
</h2>
{!nameParam && (
<>
<label
@ -214,9 +209,9 @@ const NewApp = () => {
)}
<div className="flex justify-between items-center mb-2 text-gray-800 dark:text-white">
<p className="text-lg font-medium">Authorize the app to:</p>
{!reqMethodsParam && (
{!reqMethodsParam && !isEditing && (
<EditIcon
onClick={() => setEdit(true)}
onClick={() => setEditing(true)}
className="text-gray-800 dark:text-gray-300 cursor-pointer w-6"
/>
)}
@ -224,20 +219,22 @@ const NewApp = () => {
<div className="mb-6">
<ul className="flex flex-col w-full">
{Object.keys(nip47MethodDescriptions).map((rm, index) => {
const RequestMethodIcon = iconMap[rm as RequestMethodType];
{(
Object.keys(nip47MethodDescriptions) as RequestMethodType[]
).map((rm, index) => {
const RequestMethodIcon = iconMap[rm];
return (
<li
key={index}
className={`w-full ${
rm == "pay_invoice" ? "order-last" : ""
} ${!edit && !requestMethods.includes(rm) ? "hidden" : ""}`}
} ${!isEditing && !requestMethods.has(rm) ? "hidden" : ""}`}
>
<div className="flex items-center mb-2">
{RequestMethodIcon && (
<RequestMethodIcon
className={`text-gray-800 dark:text-gray-300 w-5 mr-3 ${
edit ? "hidden" : ""
isEditing ? "hidden" : ""
}`}
/>
)}
@ -245,10 +242,10 @@ const NewApp = () => {
type="checkbox"
id={rm}
value={rm}
checked={requestMethods.includes(rm)}
checked={requestMethods.has(rm as RequestMethodType)}
onChange={handleRequestMethodChange}
className={` ${
!edit ? "hidden" : ""
!isEditing ? "hidden" : ""
} w-4 h-4 mr-4 text-purple-700 bg-gray-50 border border-gray-300 rounded focus:ring-purple-700 dark:focus:ring-purple-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-surface-00dp dark:border-gray-700 cursor-pointer`}
/>
<label
@ -261,8 +258,8 @@ const NewApp = () => {
{rm == "pay_invoice" && (
<div
className={`pt-2 pb-2 pl-5 ml-2.5 border-l-2 border-l-gray-200 dark:border-l-gray-400 ${
!requestMethods.includes(rm)
? edit
!requestMethods.has(rm)
? isEditing
? "pointer-events-none opacity-30"
: "hidden"
: ""
@ -279,24 +276,23 @@ const NewApp = () => {
>
{Object.keys(budgetOptions).map((budget) => {
return (
<>
<div
onClick={() =>
setMaxAmount(budgetOptions[budget])
}
className={`col-span-2 md:col-span-1 cursor-pointer rounded border-2 ${
maxAmount == budgetOptions[budget]
? "border-purple-700 dark:border-purple-300 text-purple-700 bg-purple-100 dark:bg-purple-900"
: "border-gray-200 dark:border-gray-400"
} text-center py-4 dark:text-white`}
>
{budget}
<br />
{budgetOptions[budget]
? "sats"
: "#reckless"}
</div>
</>
<div
key={budget}
onClick={() =>
setMaxAmount(budgetOptions[budget])
}
className={`col-span-2 md:col-span-1 cursor-pointer rounded border-2 ${
maxAmount == budgetOptions[budget]
? "border-purple-700 dark:border-purple-300 text-purple-700 bg-purple-100 dark:bg-purple-900"
: "border-gray-200 dark:border-gray-400"
} text-center py-4 dark:text-white`}
>
{budget}
<br />
{budgetOptions[budget]
? "sats"
: "#reckless"}
</div>
);
})}
</div>
@ -319,7 +315,7 @@ const NewApp = () => {
</ul>
</div>
{!expiresAtParam ? (
{!expiresAt || days ? (
<>
<div
onClick={() => setExpireOptions(true)}
@ -342,6 +338,7 @@ const NewApp = () => {
{Object.keys(expiryOptions).map((expiry) => {
return (
<div
key={expiry}
onClick={() => handleDays(expiryOptions[expiry])}
className={`cursor-pointer rounded border-2 ${
days == expiryOptions[expiry]
@ -363,20 +360,13 @@ const NewApp = () => {
Connection expiry time
</p>
<p className="text-gray-600 dark:text-gray-300 text-sm">
{new Date(parseInt(expiresAtParam) * 1000).toISOString()}
{expiresAt.toLocaleString()}
</p>
</>
)}
{user && user.email && (
<p className="mt-8 pt-4 border-t border-gray-300 dark:border-gray-700 text-sm text-gray-500 dark:text-neutral-300 text-center">
You're logged in as{" "}
<span className="font-mono">{user.email}</span>
</p>
)}
</div>
<div className="mt-6 flex flex-col sm:flex-row sm:justify-center">
<div className="mt-6 flex flex-col sm:flex-row sm:justify-center px-4 md:px-8">
{!pubkey && (
<a
href="/apps"

View file

@ -1,22 +1,28 @@
import Loading from "../../components/Loading";
import { useNavigate, useParams } from "react-router-dom";
import { useInfo } from "../../hooks/useInfo";
import { useApp } from "../../hooks/useApp";
import { handleFetchError, validateFetchResponse } from "../../utils/fetch";
import toast from "../../components/Toast";
import { useCSRF } from "../../hooks/useCSRF";
import { RequestMethodType, nip47MethodDescriptions } from "../../types";
import { RequestMethodType, nip47MethodDescriptions } from "src/types";
import { useInfo } from "src/hooks/useInfo";
import { useApp } from "src/hooks/useApp";
import { useCSRF } from "src/hooks/useCSRF";
import toast from "src/components/Toast";
import Loading from "src/components/Loading";
import { handleFetchError, validateFetchResponse } from "src/utils/fetch";
function ShowApp() {
const { data: info } = useInfo();
const { data: csrf } = useCSRF();
const { pubkey } = useParams() as { pubkey: string };
const { data: app } = useApp(pubkey);
const { data: app, error } = useApp(pubkey);
const navigate = useNavigate();
if (error) {
return <p className="text-red-500">{error.message}</p>;
}
if (!app || !info) {
return <Loading />;
}
const handleDelete = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
try {
@ -54,34 +60,36 @@ function ShowApp() {
<div className="divide-y divide-gray-200 dark:divide-white/10 dark:bg-surface-02dp">
<div className="pb-4">
<table>
<tr>
<td className="align-top w-32 font-medium dark:text-white">
Public Key
</td>
<td className="text-gray-600 dark:text-neutral-400 break-all">
{app.nostrPubkey}
</td>
</tr>
<tr>
<td className="align-top font-medium dark:text-white">
Last used
</td>
<td className="text-gray-600 dark:text-neutral-400">
{app.lastEventAt
? new Date(app.lastEventAt).toLocaleDateString()
: "never"}
</td>
</tr>
<tr>
<td className="align-top font-medium dark:text-white">
Expires at
</td>
<td className="text-gray-600 dark:text-neutral-400">
{app.expiresAt
? new Date(app.expiresAt).toLocaleDateString()
: "never"}
</td>
</tr>
<tbody>
<tr>
<td className="align-top w-32 font-medium dark:text-white">
Public Key
</td>
<td className="text-gray-600 dark:text-neutral-400 break-all">
{app.nostrPubkey}
</td>
</tr>
<tr>
<td className="align-top font-medium dark:text-white">
Last used
</td>
<td className="text-gray-600 dark:text-neutral-400">
{app.lastEventAt
? new Date(app.lastEventAt).toLocaleDateString()
: "never"}
</td>
</tr>
<tr>
<td className="align-top font-medium dark:text-white">
Expires at
</td>
<td className="text-gray-600 dark:text-neutral-400">
{app.expiresAt
? new Date(app.expiresAt).toLocaleDateString()
: "never"}
</td>
</tr>
</tbody>
</table>
</div>
@ -90,7 +98,7 @@ function ShowApp() {
Permissions
</h3>
<ul className="mt-2 text-gray-600 dark:text-neutral-400">
{app.requestMethods.map((method, index) => (
{app.requestMethods.map((method: string, index: number) => (
<li key={index} className="mb-2 relative pl-6">
<span className="absolute left-0 text-green-500"></span>
{nip47MethodDescriptions[method as RequestMethodType]}
@ -100,16 +108,18 @@ function ShowApp() {
{app.maxAmount > 0 && (
<div className="pl-6">
<table className="text-gray-600 dark:text-neutral-400">
<tr>
<td className="font-medium">Budget</td>
<td>
{app.maxAmount} sats ({app.budgetUsage} sats used)
</td>
</tr>
<tr>
<td className="font-medium pr-3">Renews</td>
<td>{app.budgetRenewal}</td>
</tr>
<tbody>
<tr>
<td className="font-medium">Budget</td>
<td>
{app.maxAmount} sats ({app.budgetUsage} sats used)
</td>
</tr>
<tr>
<td className="font-medium pr-3">Renews</td>
<td>{app.budgetRenewal}</td>
</tr>
</tbody>
</table>
</div>
)}

View file

@ -1,2 +0,0 @@
export const swrFetcher = (...args: Parameters<typeof fetch>) =>
fetch(...args).then((res) => res.json());

View file

@ -54,6 +54,10 @@ export interface User {
expiresAt: string;
}
export interface ErrorResponse {
message: string;
}
export interface App {
id: number;
userId: number;

View file

@ -1,4 +1,4 @@
import toast from "../components/Toast";
import toast from "src/components/Toast";
export async function validateFetchResponse(response: Response) {
if (!response.ok) {

18
frontend/src/utils/swr.ts Normal file
View file

@ -0,0 +1,18 @@
import { ErrorResponse } from "src/types";
export const swrFetcher = async (...args: Parameters<typeof fetch>) => {
const response = await fetch(...args);
const json = await response.json();
if (!response.ok) {
const error = new Error(
response.status +
" " +
((json as ErrorResponse).message || "Unknown error")
);
throw error;
}
return json;
};

View file

@ -6,6 +6,8 @@
"module": "ESNext",
"skipLibCheck": true,
"baseUrl": ".",
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,

View file

@ -1,5 +1,6 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import path from "path";
// https://vitejs.dev/config/
export default defineConfig({
@ -10,4 +11,9 @@ export default defineConfig({
"/alby": "http://localhost:8080",
},
},
resolve: {
alias: {
src: path.resolve(__dirname, "./src"),
},
},
});

View file

@ -9,7 +9,7 @@
"@alloc/quick-lru@^5.2.0":
version "5.2.0"
resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30"
resolved "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz"
integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
"@esbuild/android-arm64@0.18.20":
@ -175,7 +175,7 @@
"@jridgewell/gen-mapping@^0.3.2":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz"
integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==
dependencies:
"@jridgewell/set-array" "^1.0.1"
@ -184,22 +184,22 @@
"@jridgewell/resolve-uri@^3.1.0":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz"
integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==
"@jridgewell/set-array@^1.0.1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz"
integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
version "1.4.15"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
"@jridgewell/trace-mapping@^0.3.9":
version "0.3.20"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f"
resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz"
integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==
dependencies:
"@jridgewell/resolve-uri" "^3.1.0"
@ -306,19 +306,19 @@
"@tailwindcss/aspect-ratio@^0.4.2":
version "0.4.2"
resolved "https://registry.yarnpkg.com/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz#9ffd52fee8e3c8b20623ff0dcb29e5c21fb0a9ba"
resolved "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz"
integrity sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==
"@tailwindcss/forms@^0.5.7":
version "0.5.7"
resolved "https://registry.yarnpkg.com/@tailwindcss/forms/-/forms-0.5.7.tgz#db5421f062a757b5f828bc9286ba626c6685e821"
resolved "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz"
integrity sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==
dependencies:
mini-svg-data-uri "^1.2.3"
"@tailwindcss/typography@^0.5.10":
version "0.5.10"
resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.5.10.tgz#2abde4c6d5c797ab49cf47610830a301de4c1e0a"
resolved "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.10.tgz"
integrity sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==
dependencies:
lodash.castarray "^4.4.0"
@ -331,6 +331,13 @@
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
"@types/node@^20.10.8":
version "20.10.8"
resolved "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz"
integrity sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==
dependencies:
undici-types "~5.26.4"
"@types/prop-types@*":
version "15.7.11"
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz"
@ -493,12 +500,12 @@ ansi-styles@^4.1.0:
any-promise@^1.0.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz"
integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==
anymatch@~3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz"
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
dependencies:
normalize-path "^3.0.0"
@ -506,7 +513,7 @@ anymatch@~3.1.2:
arg@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
resolved "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz"
integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
argparse@^2.0.1:
@ -538,7 +545,7 @@ balanced-match@^1.0.0:
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
brace-expansion@^1.1.7:
@ -573,7 +580,7 @@ callsites@^3.0.0:
camelcase-css@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz"
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001565:
@ -591,7 +598,7 @@ chalk@^4.0.0:
chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
dependencies:
anymatch "~3.1.2"
@ -606,7 +613,7 @@ chokidar@^3.5.3:
client-only@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
color-convert@^2.0.1:
@ -623,7 +630,7 @@ color-name@~1.1.4:
commander@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz"
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
concat-map@0.0.1:
@ -642,7 +649,7 @@ cross-spawn@^7.0.2:
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
csstype@^3.0.2:
@ -664,7 +671,7 @@ deep-is@^0.1.3:
didyoumean@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz"
integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==
dir-glob@^3.0.1:
@ -676,7 +683,7 @@ dir-glob@^3.0.1:
dlv@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79"
resolved "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz"
integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==
doctrine@^3.0.0:
@ -915,7 +922,7 @@ fsevents@~2.3.2:
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
glob-parent@^5.1.2, glob-parent@~5.1.2:
@ -934,7 +941,7 @@ glob-parent@^6.0.2:
glob@7.1.6:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
dependencies:
fs.realpath "^1.0.0"
@ -977,7 +984,7 @@ globby@^11.1.0:
goober@^2.1.10:
version "2.1.13"
resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.13.tgz#e3c06d5578486212a76c9eba860cbc3232ff6d7c"
resolved "https://registry.npmjs.org/goober/-/goober-2.1.13.tgz"
integrity sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==
graphemer@^1.4.0:
@ -992,7 +999,7 @@ has-flag@^4.0.0:
hasown@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c"
resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz"
integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==
dependencies:
function-bind "^1.1.2"
@ -1030,14 +1037,14 @@ inherits@2:
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
is-core-module@^2.13.0:
version "2.13.1"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384"
resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz"
integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==
dependencies:
hasown "^2.0.0"
@ -1071,7 +1078,7 @@ isexe@^2.0.0:
jiti@^1.19.1:
version "1.21.0"
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d"
resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz"
integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==
"js-tokens@^3.0.0 || ^4.0.0":
@ -1118,17 +1125,17 @@ levn@^0.4.1:
lilconfig@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"
resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz"
integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==
lilconfig@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.0.0.tgz#f8067feb033b5b74dab4602a5f5029420be749bc"
resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz"
integrity sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==
lines-and-columns@^1.1.6:
version "1.2.4"
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
locate-path@^6.0.0:
@ -1140,12 +1147,12 @@ locate-path@^6.0.0:
lodash.castarray@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.castarray/-/lodash.castarray-4.4.0.tgz#c02513515e309daddd4c24c60cfddcf5976d9115"
resolved "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz"
integrity sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz"
integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==
lodash.merge@^4.6.2:
@ -1182,7 +1189,7 @@ micromatch@^4.0.4, micromatch@^4.0.5:
mini-svg-data-uri@^1.2.3:
version "1.4.4"
resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz#8ab0aabcdf8c29ad5693ca595af19dd2ead09939"
resolved "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz"
integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==
minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
@ -1199,7 +1206,7 @@ ms@2.1.2:
mz@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz"
integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
dependencies:
any-promise "^1.0.0"
@ -1223,7 +1230,7 @@ node-releases@^2.0.14:
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
normalize-range@^0.1.2:
@ -1233,12 +1240,12 @@ normalize-range@^0.1.2:
object-assign@^4.0.1, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
object-hash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"
resolved "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz"
integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
once@^1.3.0:
@ -1298,7 +1305,7 @@ path-key@^3.1.0:
path-parse@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-type@^4.0.0:
@ -1318,17 +1325,17 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
pify@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz"
integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
pirates@^4.0.1:
version "4.0.6"
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz"
integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==
postcss-import@^15.1.0:
version "15.1.0"
resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70"
resolved "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz"
integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==
dependencies:
postcss-value-parser "^4.0.0"
@ -1337,14 +1344,14 @@ postcss-import@^15.1.0:
postcss-js@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2"
resolved "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz"
integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==
dependencies:
camelcase-css "^2.0.1"
postcss-load-config@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3"
resolved "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz"
integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==
dependencies:
lilconfig "^3.0.0"
@ -1352,14 +1359,14 @@ postcss-load-config@^4.0.1:
postcss-nested@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c"
resolved "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz"
integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==
dependencies:
postcss-selector-parser "^6.0.11"
postcss-selector-parser@6.0.10:
version "6.0.10"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz"
integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
dependencies:
cssesc "^3.0.0"
@ -1367,7 +1374,7 @@ postcss-selector-parser@6.0.10:
postcss-selector-parser@^6.0.11:
version "6.0.13"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b"
resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz"
integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==
dependencies:
cssesc "^3.0.0"
@ -1375,7 +1382,7 @@ postcss-selector-parser@^6.0.11:
postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@^8.4.23, postcss@^8.4.27, postcss@^8.4.32:
@ -1394,7 +1401,7 @@ prelude-ls@^1.2.1:
prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
dependencies:
loose-envify "^1.4.0"
@ -1408,7 +1415,7 @@ punycode@^2.1.0:
qr.js@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f"
resolved "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz"
integrity sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==
queue-microtask@^1.2.2:
@ -1426,19 +1433,19 @@ react-dom@^18.2.0:
react-hot-toast@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.4.1.tgz#df04295eda8a7b12c4f968e54a61c8d36f4c0994"
resolved "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz"
integrity sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==
dependencies:
goober "^2.1.10"
react-is@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-qr-code@^2.0.12:
version "2.0.12"
resolved "https://registry.yarnpkg.com/react-qr-code/-/react-qr-code-2.0.12.tgz#98f99e9ad5ede46d73ab819e2dd9925c5f5d7a2d"
resolved "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.12.tgz"
integrity sha512-k+pzP5CKLEGBRwZsDPp98/CAJeXlsYRHM2iZn1Sd5Th/HnKhIZCSg27PXO58zk8z02RaEryg+60xa4vyywMJwg==
dependencies:
prop-types "^15.8.1"
@ -1468,14 +1475,14 @@ react@^18.2.0:
read-cache@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz"
integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==
dependencies:
pify "^2.3.0"
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
@ -1487,7 +1494,7 @@ resolve-from@^4.0.0:
resolve@^1.1.7, resolve@^1.22.2:
version "1.22.8"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz"
integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
dependencies:
is-core-module "^2.13.0"
@ -1570,7 +1577,7 @@ strip-json-comments@^3.1.1:
sucrase@^3.32.0:
version "3.34.0"
resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.34.0.tgz#1e0e2d8fcf07f8b9c3569067d92fbd8690fb576f"
resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz"
integrity sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==
dependencies:
"@jridgewell/gen-mapping" "^0.3.2"
@ -1590,12 +1597,12 @@ supports-color@^7.1.0:
supports-preserve-symlinks-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
swr@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.4.tgz#03ec4c56019902fbdc904d78544bd7a9a6fa3f07"
resolved "https://registry.npmjs.org/swr/-/swr-2.2.4.tgz"
integrity sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ==
dependencies:
client-only "^0.0.1"
@ -1636,14 +1643,14 @@ text-table@^0.2.0:
thenify-all@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
resolved "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz"
integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==
dependencies:
thenify ">= 3.1.0 < 4"
"thenify@>= 3.1.0 < 4":
version "3.3.1"
resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f"
resolved "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz"
integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==
dependencies:
any-promise "^1.0.0"
@ -1662,7 +1669,7 @@ ts-api-utils@^1.0.1:
ts-interface-checker@^0.1.9:
version "0.1.13"
resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz"
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
type-check@^0.4.0, type-check@~0.4.0:
@ -1682,6 +1689,11 @@ typescript@^5.0.2:
resolved "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz"
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
undici-types@~5.26.4:
version "5.26.5"
resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
update-browserslist-db@^1.0.13:
version "1.0.13"
resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz"
@ -1699,12 +1711,12 @@ uri-js@^4.2.2:
use-sync-external-store@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
util-deprecate@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
vite@^4.4.5:
@ -1737,7 +1749,7 @@ yallist@^4.0.0:
yaml@^2.3.4:
version "2.3.4"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2"
resolved "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz"
integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==
yocto-queue@^0.1.0:

View file

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/nbd-wtf/go-nostr"
decodepay "github.com/nbd-wtf/ln-decodepay"
@ -64,7 +65,7 @@ func (svc *Service) HandleLookupInvoiceEvent(ctx context.Context, request *Nip47
paymentHash := lookupInvoiceParams.PaymentHash
if paymentHash == "" {
paymentRequest, err := decodepay.Decodepay(lookupInvoiceParams.Invoice)
paymentRequest, err := decodepay.Decodepay(strings.ToLower(lookupInvoiceParams.Invoice))
if err != nil {
svc.Logger.WithFields(logrus.Fields{
"eventId": event.ID,

View file

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/nbd-wtf/go-nostr"
decodepay "github.com/nbd-wtf/ln-decodepay"
@ -36,6 +37,8 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, request *Nip47Req
}
bolt11 = payParams.Invoice
// Convert invoice to lowercase string
bolt11 = strings.ToLower(bolt11)
paymentRequest, err := decodepay.Decodepay(bolt11)
if err != nil {
svc.Logger.WithFields(logrus.Fields{

38
lnd.go
View file

@ -7,6 +7,7 @@ import (
"encoding/hex"
"errors"
"sort"
"strings"
"time"
"github.com/getAlby/nostr-wallet-connect/lnd"
@ -63,7 +64,7 @@ func (svc *LNDService) ListTransactions(ctx context.Context, senderPubkey string
// Fetch invoices
var invoices []*lnrpc.Invoice
if invoiceType == "" || invoiceType == "incoming" {
incomingResp, err := svc.client.ListInvoices(ctx, &lnrpc.ListInvoiceRequest{NumMaxInvoices: limit, IndexOffset: offset})
incomingResp, err := svc.client.ListInvoices(ctx, &lnrpc.ListInvoiceRequest{Reversed: true, NumMaxInvoices: limit, IndexOffset: offset})
if err != nil {
return nil, err
}
@ -82,7 +83,7 @@ func (svc *LNDService) ListTransactions(ctx context.Context, senderPubkey string
var payments []*lnrpc.Payment
if invoiceType == "" || invoiceType == "outgoing" {
// Not just pending but failed payments will also be included because of IncludeIncomplete
outgoingResp, err := svc.client.ListPayments(ctx, &lnrpc.ListPaymentsRequest{MaxPayments: limit, IndexOffset: offset, IncludeIncomplete: unpaid})
outgoingResp, err := svc.client.ListPayments(ctx, &lnrpc.ListPaymentsRequest{Reversed: true, MaxPayments: limit, IndexOffset: offset, IncludeIncomplete: unpaid})
if err != nil {
return nil, err
}
@ -95,11 +96,11 @@ func (svc *LNDService) ListTransactions(ctx context.Context, senderPubkey string
continue
}
var paymentRequest decodepay.Bolt11
var expiresAt *time.Time
var expiresAt *int64
var description string
var descriptionHash string
if payment.PaymentRequest != "" {
paymentRequest, err = decodepay.Decodepay(payment.PaymentRequest)
paymentRequest, err = decodepay.Decodepay(strings.ToLower(payment.PaymentRequest))
if err != nil {
svc.Logger.WithFields(logrus.Fields{
"bolt11": payment.PaymentRequest,
@ -107,17 +108,17 @@ func (svc *LNDService) ListTransactions(ctx context.Context, senderPubkey string
return nil, err
}
expiresAt = &time.Time{}
*expiresAt = time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second)
expiresAtUnix := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix()
expiresAt = &expiresAtUnix
description = paymentRequest.Description
descriptionHash = paymentRequest.DescriptionHash
}
var settledAt *time.Time
var settledAt *int64
if payment.Status == lnrpc.Payment_SUCCEEDED {
// FIXME: how to get the actual settled at time?
settledAt = &time.Time{}
*settledAt = time.Unix(0, payment.CreationTimeNs)
settledAtUnix := time.Unix(0, payment.CreationTimeNs).Unix()
settledAt = &settledAtUnix
}
transaction := Nip47Transaction{
@ -127,7 +128,7 @@ func (svc *LNDService) ListTransactions(ctx context.Context, senderPubkey string
PaymentHash: payment.PaymentHash,
Amount: payment.ValueMsat,
FeesPaid: payment.FeeMsat,
CreatedAt: time.Unix(0, payment.CreationTimeNs),
CreatedAt: time.Unix(0, payment.CreationTimeNs).Unix(),
Description: description,
DescriptionHash: descriptionHash,
ExpiresAt: expiresAt,
@ -139,7 +140,7 @@ func (svc *LNDService) ListTransactions(ctx context.Context, senderPubkey string
// sort by created date descending
sort.SliceStable(transactions, func(i, j int) bool {
return transactions[i].CreatedAt.After(transactions[j].CreatedAt)
return transactions[i].CreatedAt > transactions[j].CreatedAt
})
return transactions, nil
@ -323,7 +324,7 @@ func makePreimageHex() ([]byte, error) {
return bytes, nil
}
func NewLNDService(ctx context.Context, svc *Service, e *echo.Echo) (result *LNDService, err error) {
func NewLNDService(ctx context.Context, svc *Service, e *echo.Echo) (result LNClient, err error) {
lndClient, err := lnd.NewLNDclient(lnd.LNDoptions{
Address: svc.cfg.LNDAddress,
CertFile: svc.cfg.LNDCertFile,
@ -356,18 +357,17 @@ func NewLNDService(ctx context.Context, svc *Service, e *echo.Echo) (result *LND
}
func lndInvoiceToTransaction(invoice *lnrpc.Invoice) *Nip47Transaction {
var settledAt *time.Time
var settledAt *int64
var preimage string
if invoice.State == lnrpc.Invoice_SETTLED {
settledAt = &time.Time{}
*settledAt = time.Unix(invoice.SettleDate, 0)
settledAt = &invoice.SettleDate
// only set preimage if invoice is settled
preimage = hex.EncodeToString(invoice.RPreimage)
}
var expiresAt *time.Time
var expiresAt *int64
if invoice.Expiry > 0 {
expiresAt = &time.Time{}
*expiresAt = time.Unix(invoice.SettleDate, 0)
expiresAtUnix := invoice.CreationDate + invoice.Expiry
expiresAt = &expiresAtUnix
}
return &Nip47Transaction{
@ -379,7 +379,7 @@ func lndInvoiceToTransaction(invoice *lnrpc.Invoice) *Nip47Transaction {
PaymentHash: hex.EncodeToString(invoice.RHash),
Amount: invoice.ValueMsat,
FeesPaid: invoice.AmtPaidMsat,
CreatedAt: time.Unix(invoice.CreationDate, 0),
CreatedAt: invoice.CreationDate,
SettledAt: settledAt,
ExpiresAt: expiresAt,
// TODO: Metadata (e.g. keysend)

View file

@ -0,0 +1,17 @@
package migrations
import (
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
// Create a composite index to improve performance of finding the latest nostr event for an app
var _202401092201_add_events_id_index = &gormigrate.Migration{
ID: "202401092201_add_events_id_index",
Migrate: func(tx *gorm.DB) error {
return tx.Exec("CREATE INDEX IF NOT EXISTS idx_nostr_events_app_id_and_id ON nostr_events(app_id, id)").Error
},
Rollback: func(tx *gorm.DB) error {
return nil
},
}

View file

@ -11,7 +11,8 @@ func Migrate(db *gorm.DB) error {
_202309271616_initial_migration,
_202309271617_fix_preimage_null,
_202309271618_add_payment_sum_index,
_202401092201_add_events_id_index,
})
return m.Migrate()
}
}

View file

@ -37,24 +37,6 @@ const (
NOSTR_EVENT_STATE_PUBLISH_UNCONFIRMED = "sent"
)
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",
NIP_47_LIST_TRANSACTIONS_METHOD: "Read incoming transaction history",
}
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",
NIP_47_LIST_TRANSACTIONS_METHOD: "transactions",
}
// TODO: move to models/Alby
type AlbyMe struct {
Identifier string `json:"identifier"`
@ -135,9 +117,9 @@ type Nip47Transaction struct {
PaymentHash string `json:"payment_hash"`
Amount int64 `json:"amount"`
FeesPaid int64 `json:"fees_paid"`
CreatedAt time.Time `json:"created_at"`
ExpiresAt *time.Time `json:"expires_at"`
SettledAt *time.Time `json:"settled_at"`
CreatedAt int64 `json:"created_at"`
ExpiresAt *int64 `json:"expires_at"`
SettledAt *int64 `json:"settled_at"`
Metadata interface{} `json:"metadata,omitempty"`
}
@ -207,7 +189,6 @@ type LookupInvoiceResponse struct {
}
type ErrorResponse struct {
Error bool `json:"error"`
Message string `json:"message"`
}

View file

@ -22,6 +22,16 @@ type ListAppsResponse struct {
Apps []App `json:"apps"`
}
type CreateAppRequest struct {
Name string `json:"name"`
Pubkey string `json:"pubkey"`
MaxAmount int `json:"maxAmount"`
BudgetRenewal string `json:"budgetRenewal"`
ExpiresAt string `json:"expiresAt"`
RequestMethods string `json:"requestMethods"`
ReturnTo string `json:"returnTo"`
}
type CreateAppResponse struct {
PairingUri string `json:"pairingUri"`
PairingSecret string `json:"pairingSecretKey"`

View file

@ -23,15 +23,6 @@ type Service struct {
Logger *logrus.Logger
}
/*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_LIST_TRANSACTIONS_METHOD: true,
}*/
func (svc *Service) GetUser(c echo.Context) (user *User, err error) {
sess, _ := session.Get(CookieName, c)
userID := sess.Values["user_id"]
@ -54,21 +45,15 @@ 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 <-ctx.Done():
svc.Logger.Info("Exiting subscription.")
return nil
case <-sub.EndOfStoredEvents:
if !svc.ReceivedEOS {
svc.Logger.Info("Received EOS")
}
svc.ReceivedEOS = true
case event := <-sub.Events:
go func() {
go func() {
<-sub.EndOfStoredEvents
svc.ReceivedEOS = true
svc.Logger.Info("Received EOS")
}()
go func() {
for event := range sub.Events {
go func(event *nostr.Event) {
resp, err := svc.HandleEvent(ctx, event)
if err != nil {
svc.Logger.WithFields(logrus.Fields{
@ -132,8 +117,22 @@ func (svc *Service) StartSubscription(ctx context.Context, sub *nostr.Subscripti
}).Info("Reply sent but no response from relay (timeout)")
}
}
}()
}(event)
}
svc.Logger.Info("Subscription ended")
}()
select {
case <-sub.Relay.Context().Done():
svc.Logger.Errorf("Relay error %v", sub.Relay.ConnectionError)
return sub.Relay.ConnectionError
case <-ctx.Done():
if ctx.Err() != context.Canceled {
svc.Logger.Errorf("Subscription error %v", ctx.Err())
return ctx.Err()
}
svc.Logger.Info("Exiting subscription.")
return nil
}
}
@ -162,6 +161,10 @@ func (svc *Service) HandleEvent(ctx context.Context, event *nostr.Event) (result
NostrPubkey: event.PubKey,
}).Error
if err != nil {
svc.Logger.WithFields(logrus.Fields{
"nostrPubkey": event.PubKey,
}).Errorf("Failed to find app for nostr pubkey: %v", err)
ss, err := nip04.ComputeSharedSecret(event.PubKey, svc.cfg.NostrSecretKey)
if err != nil {
return nil, err

View file

@ -111,6 +111,7 @@ var mockNodeInfo = NodeInfo{
}
var mockTime = time.Unix(1693876963, 0)
var mockTimeUnix = mockTime.Unix()
var mockTransactions = []Nip47Transaction{
{
@ -122,7 +123,7 @@ var mockTransactions = []Nip47Transaction{
PaymentHash: "payment_hash_1",
Amount: 1000,
FeesPaid: 50,
SettledAt: &mockTime,
SettledAt: &mockTimeUnix,
Metadata: map[string]interface{}{
"key1": "value1",
"key2": 42,
@ -137,7 +138,7 @@ var mockTransactions = []Nip47Transaction{
PaymentHash: "payment_hash_2",
Amount: 2000,
FeesPaid: 75,
SettledAt: &mockTime,
SettledAt: &mockTimeUnix,
},
}
var mockTransaction = &mockTransactions[0]
@ -571,7 +572,7 @@ func TestHandleEvent(t *testing.T) {
assert.Equal(t, mockTransactions[0].PaymentHash, transaction.PaymentHash)
assert.Equal(t, mockTransactions[0].Amount, transaction.Amount)
assert.Equal(t, mockTransactions[0].FeesPaid, transaction.FeesPaid)
assert.Equal(t, mockTransactions[0].SettledAt.Unix(), transaction.SettledAt.Unix())
assert.Equal(t, mockTransactions[0].SettledAt, transaction.SettledAt)
// get_info: without permission
newPayload, err = nip04.Encrypt(nip47GetInfoJson, ss)
@ -627,7 +628,7 @@ func TestHandleEvent(t *testing.T) {
assert.Equal(t, []string{"get_info"}, received.Result.(*Nip47GetInfoResponse).Methods)
}
func createTestService(t *testing.T) (svc *Service, ln *MockLn) {
func createTestService(t *testing.T) (svc *Service, ln LNClient) {
db, err := gorm.Open(sqlite.Open(testDB), &gorm.Config{})
assert.NoError(t, err)
err = db.AutoMigrate(&User{}, &App{}, &AppPermission{}, &NostrEvent{}, &Payment{}, &Identity{})

View file

@ -25,19 +25,3 @@ func GetStartOfBudget(budget_type string, createdAt time.Time) time.Time {
return createdAt
}
}
func GetEndOfBudget(budget_type string, createdAt time.Time) time.Time {
start := GetStartOfBudget(budget_type, createdAt)
switch budget_type {
case "daily":
return start.AddDate(0, 0, 1)
case "weekly":
return start.AddDate(0, 0, 7)
case "monthly":
return start.AddDate(0, 1, 0)
case "yearly":
return start.AddDate(1, 0, 0)
default: //"never"
return time.Time{}
}
}