mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
Merge branch 'task-react' into server-self-host
This commit is contained in:
commit
c0cce751e2
58 changed files with 788 additions and 631 deletions
2
.dockerignore
Normal file
2
.dockerignore
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
frontend/node_modules
|
||||
frontend/dist
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
40
.github/workflows/multiplatform.yaml
vendored
40
.github/workflows/multiplatform.yaml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
104
.github/workflows/workflow.yaml
vendored
104
.github/workflows/workflow.yaml
vendored
|
|
@ -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
|
||||
|
|
|
|||
13
Dockerfile
13
Dockerfile
|
|
@ -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" ]
|
||||
|
|
|
|||
28
README.md
28
README.md
|
|
@ -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
20
alby.go
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
260
echo_handlers.go
260
echo_handlers.go
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
1
frontend/public/nwc-logo.svg
Normal file
1
frontend/public/nwc-logo.svg
Normal 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 |
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
Convert these to React components (see components/icons)
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
19
frontend/src/components/icons/AboutIcon.tsx
Normal file
19
frontend/src/components/icons/AboutIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
22
frontend/src/components/icons/CaretIcon.tsx
Normal file
22
frontend/src/components/icons/CaretIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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
|
||||
<a
|
||||
href="https://github.com/getAlby/nostr-wallet-connect/"
|
||||
className="dark:text-gray-300 underline"
|
||||
>
|
||||
on GitHub
|
||||
</a>
|
||||
or
|
||||
or
|
||||
<a
|
||||
href="mailto:hello@getalby.com"
|
||||
className="dark:text-gray-300 underline"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
export const swrFetcher = (...args: Parameters<typeof fetch>) =>
|
||||
fetch(...args).then((res) => res.json());
|
||||
|
|
@ -54,6 +54,10 @@ export interface User {
|
|||
expiresAt: string;
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface App {
|
||||
id: number;
|
||||
userId: number;
|
||||
|
|
|
|||
|
|
@ -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
18
frontend/src/utils/swr.ts
Normal 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;
|
||||
};
|
||||
|
|
@ -6,6 +6,8 @@
|
|||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
"baseUrl": ".",
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
38
lnd.go
|
|
@ -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)
|
||||
|
|
|
|||
17
migrations/202401092201_add_events_id_index.go
Normal file
17
migrations/202401092201_add_events_id_index.go
Normal 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
|
||||
},
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
models.go
25
models.go
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
53
service.go
53
service.go
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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{})
|
||||
|
|
|
|||
16
utils.go
16
utils.go
|
|
@ -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{}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue