diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml
new file mode 100644
index 000000000..4bd5ecb83
--- /dev/null
+++ b/.github/workflows/linting.yml
@@ -0,0 +1,35 @@
+name: Code quality - linting and typechecking
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ types: [opened, synchronize]
+
+jobs:
+ linting:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./frontend
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ with:
+ node-version: 20.x
+ cache: "yarn"
+ cache-dependency-path: frontend/yarn.lock
+
+ - run: yarn install
+ - run: yarn prepare:http
+
+ - name: Linting
+ run: yarn lint:js
+
+ - name: Prettier
+ run: yarn format
+
+ - name: Typechecking
+ run: yarn tsc:compile
diff --git a/.github/workflows/wails2.yaml b/.github/workflows/wails2.yaml
index 5d79db736..e5ffd1fc7 100644
--- a/.github/workflows/wails2.yaml
+++ b/.github/workflows/wails2.yaml
@@ -8,11 +8,12 @@ jobs:
strategy:
fail-fast: false
matrix:
- build: [
- {name: albyhub, platform: linux/amd64, os: ubuntu-20.04},
- {name: albyhub, platform: windows/amd64, os: windows-2019},
- {name: albyhub, platform: darwin/universal, os: macos-12}
- ]
+ build:
+ [
+ { name: albyhub, platform: linux/amd64, os: ubuntu-20.04 },
+ { name: albyhub, platform: windows/amd64, os: windows-2019 },
+ { name: albyhub, platform: darwin/universal, os: macos-12 },
+ ]
runs-on: ${{ matrix.build.os }}
steps:
- uses: actions/checkout@v4
@@ -47,7 +48,7 @@ jobs:
- name: Setup NodeJS
uses: actions/setup-node@v4
with:
- node-version: '20.x'
+ node-version: "20.x"
- name: Install Wails
run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.7.1
@@ -60,7 +61,9 @@ jobs:
- name: Install macOS Wails deps
if: runner.os == 'macOS'
- run: brew install mitchellh/gon/gon
+ run: |
+ brew install create-dmg
+ brew install Bearer/tap/gon
shell: bash
- name: Wails Doctor
@@ -84,19 +87,28 @@ jobs:
- name: Build App
if: runner.os == 'macOS'
- run: wails build --platform darwin/universal -webview2 download -o ${{ matrix.build.name }} -tags "wails"
+ run: wails build --platform darwin/universal -webview2 embed -o ${{ matrix.build.name }} -tags "wails"
shell: bash
- name: Build App
if: runner.os == 'Linux'
- run: wails build --platform linux/amd64 -webview2 download -o ${{ matrix.build.name }} -tags "wails"
+ run: wails build --platform linux/amd64 -webview2 embed -o ${{ matrix.build.name }} -tags "wails"
shell: bash
- name: Build Windows App
if: runner.os == 'Windows'
- run: wails build --platform windows/amd64 -webview2 download -o ${{ matrix.build.name }}.exe -tags "wails"
+ run: wails build --platform windows/amd64 -webview2 embed -o ${{ matrix.build.name }}.exe -tags "wails"
shell: bash
+ - name: Import Code-Signing Certificates for macOS
+ if: runner.os == 'macOS'
+ uses: Apple-Actions/import-codesign-certs@v1
+ with:
+ # The certificates in a PKCS12 file encoded as a base64 string
+ p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
+ # The password used to import the PKCS12 file.
+ p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
+
- name: Copy DLLs to the output directory
if: runner.os == 'Windows'
run: |
@@ -155,14 +167,34 @@ jobs:
rm -Rf ./build/bin/*
mv ./build/out/albyhub-${{runner.os}}.tar.bz2 ./build/bin/
+ - name: Sign the macOS binary
+ if: runner.os == 'macOS'
+ run: |
+ echo "Signing Package"
+ /usr/bin/codesign -s "Developer ID Application: Alby Inc." -f -v --deep --timestamp --options runtime --entitlements ./build/darwin/entitlements.plist ./build/bin/AlbyHub.app
+ env:
+ AC_USERNAME: ${{ secrets.APPLE_USERNAME }}
+ AC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
+ AC_PROVIDER: ${{ secrets.APPLE_TEAM_ID }}
+
- name: Make DMG image for macOS
if: runner.os == 'macOS'
run: |
mkdir -p ./build/out
- hdiutil create -volname "AlbyHub" -srcfolder ./build/bin/${{ matrix.build.name }}.app -ov -format UDZO ./build/out/albyhub-${{runner.os}}.dmg
+ create-dmg --volname "AlbyHub" --background "./build/darwin/dmgcover.png" --window-pos 200 120 --window-size 800 400 --icon-size 80 --icon "AlbyHub.app" 200 160 --hide-extension "AlbyHub.app" --app-drop-link 600 160 "./build/out/albyhub-${{runner.os}}.dmg" "./build/bin/${{ matrix.build.name }}.app"
rm -Rf ./build/bin/*
mv ./build/out/albyhub-${{runner.os}}.dmg ./build/bin/
+ - name: Notarize the DMG image
+ if: runner.os == 'macOS'
+ run: |
+ echo "Notarizing Zip Files"
+ gon -log-level=info -log-json ./build/darwin/gon-notarize.json
+ env:
+ AC_USERNAME: ${{ secrets.APPLE_USERNAME }}
+ AC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
+ AC_PROVIDER: ${{ secrets.APPLE_TEAM_ID }}
+
- name: Make Windows ZIP archive
if: runner.os == 'Windows'
run: |
diff --git a/.gitignore b/.gitignore
index d2e89e340..3d56ca075 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,7 +14,7 @@ frontend/node_modules
frontend/wailsjs
package.json.md5
-build
+build/bin
*.log
diff --git a/README.md b/README.md
index 4ffbc341f..4a55c3fb6 100644
--- a/README.md
+++ b/README.md
@@ -181,7 +181,7 @@ Follow the steps to integrate Mutinynet with your NWC Next setup:
3. During onboarding, after setting your password and authorizing via Alby OAuth, you'll be directed to `/onboarding/lightning/migrate-alby`. Click "Skip For Now" to access your wallet interface
-4. Navigate to `channels/onchain/new-address`, copy your On-Chain Address, then visit the [Mutinynet Faucet](https://faucet.mutinynet.com/) to deposit sats. Ensure the transaction confirms on [mempool.space](https://mutinynet.com/)
+4. Navigate to `channels/onchain/deposit-bitcoin`, copy your On-Chain Address, then visit the [Mutinynet Faucet](https://faucet.mutinynet.com/) to deposit sats. Ensure the transaction confirms on [mempool.space](https://mutinynet.com/)
5. Your On-chain balance will update under `/channels`
diff --git a/alby/alby_oauth_service.go b/alby/alby_oauth_service.go
index 970e235c5..75fa1679c 100644
--- a/alby/alby_oauth_service.go
+++ b/alby/alby_oauth_service.go
@@ -72,24 +72,27 @@ func (svc *albyOAuthService) CallbackHandler(ctx context.Context, code string) e
}
svc.saveToken(token)
+ me, err := svc.GetMe(ctx)
+ if err != nil {
+ svc.logger.WithError(err).Error("Failed to fetch user me")
+ // remove token so user can retry
+ svc.config.SetUpdate(accessTokenKey, "", "")
+ return err
+ }
+
existingUserIdentifier, err := svc.GetUserIdentifier()
if err != nil {
svc.logger.WithError(err).Error("Failed to get alby user identifier")
return err
}
- // setup Alby account on first time login
+ // save the user's alby account ID on first time login
if existingUserIdentifier == "" {
- // fetch and save the user's alby account ID. This cannot be changed.
- me, err := svc.GetMe(ctx)
- if err != nil {
- svc.logger.WithError(err).Error("Failed to fetch user me")
- // remove token so user can retry
- svc.config.SetUpdate(accessTokenKey, me.Identifier, "")
- return err
- }
-
svc.config.SetUpdate(userIdentifierKey, me.Identifier, "")
+ } else if me.Identifier != existingUserIdentifier {
+ // remove token so user can retry with correct account
+ svc.config.SetUpdate(accessTokenKey, "", "")
+ return errors.New("Alby Hub is connected to a different alby account. Please log out of your Alby Account at getalby.com and try again.")
}
return nil
diff --git a/api/api.go b/api/api.go
index 70dc7e16d..2cc11fa61 100644
--- a/api/api.go
+++ b/api/api.go
@@ -16,6 +16,7 @@ import (
"github.com/getAlby/nostr-wallet-connect/alby"
"github.com/getAlby/nostr-wallet-connect/backup"
+ "github.com/getAlby/nostr-wallet-connect/config"
"github.com/getAlby/nostr-wallet-connect/db"
"github.com/getAlby/nostr-wallet-connect/lnclient"
"github.com/getAlby/nostr-wallet-connect/lsp"
@@ -375,17 +376,57 @@ func (api *api) CloseChannel(ctx context.Context, peerId, channelId string, forc
})
}
-func (api *api) GetNewOnchainAddress(ctx context.Context) (*NewOnchainAddressResponse, error) {
+func (api *api) GetNewOnchainAddress(ctx context.Context) (string, error) {
if api.svc.GetLNClient() == nil {
- return nil, errors.New("LNClient not started")
+ return "", errors.New("LNClient not started")
}
address, err := api.svc.GetLNClient().GetNewOnchainAddress(ctx)
if err != nil {
- return nil, err
+ return "", err
}
- return &NewOnchainAddressResponse{
- Address: address,
- }, nil
+
+ api.svc.GetConfig().SetUpdate(config.OnchainAddressKey, address, "")
+
+ return address, nil
+}
+
+func (api *api) GetUnusedOnchainAddress(ctx context.Context) (string, error) {
+ if api.svc.GetLNClient() == nil {
+ return "", errors.New("LNClient not started")
+ }
+
+ currentAddress, err := api.svc.GetConfig().Get(config.OnchainAddressKey, "")
+ if err != nil {
+ api.logger.WithError(err).Error("Failed to get current address from config")
+ return "", err
+ }
+
+ if currentAddress != "" {
+ // check if address has any transactions
+ response, err := api.RequestEsploraApi("/address/" + currentAddress + "/txs")
+ if err != nil {
+ api.logger.WithError(err).Error("Failed to get current address transactions")
+ return currentAddress, nil
+ }
+
+ transactions, ok := response.([]interface{})
+ if !ok {
+ api.logger.WithField("response", response).Error("Failed to cast esplora address txs response", response)
+ return currentAddress, nil
+ }
+
+ if len(transactions) == 0 {
+ // address has not been used yet
+ return currentAddress, nil
+ }
+ }
+
+ newAddress, err := api.GetNewOnchainAddress(ctx)
+ if err != nil {
+ api.logger.WithError(err).Error("Failed to retrieve new onchain address")
+ return "", err
+ }
+ return newAddress, nil
}
func (api *api) SignMessage(ctx context.Context, message string) (*SignMessageResponse, error) {
@@ -426,6 +467,7 @@ func (api *api) GetBalances(ctx context.Context) (*BalancesResponse, error) {
return balances, nil
}
+// TODO: remove dependency on this endpoint
func (api *api) RequestMempoolApi(endpoint string) (interface{}, error) {
url := api.svc.GetConfig().GetEnv().MempoolApi + endpoint
@@ -558,12 +600,16 @@ func (api *api) Setup(ctx context.Context, setupRequest *SetupRequest) error {
}
if setupRequest.PhoenixdAddress != "" {
- api.svc.GetConfig().SetUpdate("PhoenixAddress", setupRequest.PhoenixdAddress, setupRequest.UnlockPassword)
+ api.svc.GetConfig().SetUpdate("PhoenixdAddress", setupRequest.PhoenixdAddress, setupRequest.UnlockPassword)
}
if setupRequest.PhoenixdAuthorization != "" {
api.svc.GetConfig().SetUpdate("PhoenixdAuthorization", setupRequest.PhoenixdAuthorization, setupRequest.UnlockPassword)
}
+ if setupRequest.CashuMintUrl != "" {
+ api.svc.GetConfig().SetUpdate("CashuMintUrl", setupRequest.CashuMintUrl, setupRequest.UnlockPassword)
+ }
+
return nil
}
diff --git a/api/esplora.go b/api/esplora.go
new file mode 100644
index 000000000..f1d185501
--- /dev/null
+++ b/api/esplora.go
@@ -0,0 +1,56 @@
+package api
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "time"
+
+ "github.com/sirupsen/logrus"
+)
+
+func (api *api) RequestEsploraApi(endpoint string) (interface{}, error) {
+ url := api.svc.GetConfig().GetEnv().LDKEsploraServer + endpoint
+
+ client := http.Client{
+ Timeout: time.Second * 10,
+ }
+
+ req, err := http.NewRequest(http.MethodGet, url, nil)
+ if err != nil {
+ api.logger.WithError(err).WithFields(logrus.Fields{
+ "url": url,
+ }).Error("Failed to create http request")
+ return nil, err
+ }
+
+ res, err := client.Do(req)
+ if err != nil {
+ api.logger.WithError(err).WithFields(logrus.Fields{
+ "url": url,
+ }).Error("Failed to send request")
+ return nil, err
+ }
+
+ defer res.Body.Close()
+
+ body, readErr := io.ReadAll(res.Body)
+ if readErr != nil {
+ api.logger.WithError(err).WithFields(logrus.Fields{
+ "url": url,
+ }).Error("Failed to read response body")
+ return nil, errors.New("failed to read response body")
+ }
+
+ var jsonContent interface{}
+ jsonErr := json.Unmarshal(body, &jsonContent)
+ if jsonErr != nil {
+ api.logger.WithError(jsonErr).WithFields(logrus.Fields{
+ "url": url,
+ }).Error("Failed to deserialize json")
+ return nil, fmt.Errorf("failed to deserialize json %s %s", url, string(body))
+ }
+ return jsonContent, nil
+}
diff --git a/api/models.go b/api/models.go
index 7e9462964..1f912721f 100644
--- a/api/models.go
+++ b/api/models.go
@@ -29,7 +29,8 @@ type API interface {
DisconnectPeer(ctx context.Context, peerId string) error
OpenChannel(ctx context.Context, openChannelRequest *OpenChannelRequest) (*OpenChannelResponse, error)
CloseChannel(ctx context.Context, peerId, channelId string, force bool) (*CloseChannelResponse, error)
- GetNewOnchainAddress(ctx context.Context) (*NewOnchainAddressResponse, error)
+ GetNewOnchainAddress(ctx context.Context) (string, error)
+ GetUnusedOnchainAddress(ctx context.Context) (string, error)
SignMessage(ctx context.Context, message string) (*SignMessageResponse, error)
RedeemOnchainFunds(ctx context.Context, toAddress string) (*RedeemOnchainFundsResponse, error)
GetBalances(ctx context.Context) (*BalancesResponse, error)
@@ -98,7 +99,7 @@ type BackupReminderRequest struct {
}
type SetupRequest struct {
- LNBackendType string `json:"backendType"`
+ LNBackendType string `json:"backendType"`
UnlockPassword string `json:"unlockPassword"`
// Breez / Greenlight
@@ -174,10 +175,6 @@ type RedeemOnchainFundsResponse struct {
type OnchainBalanceResponse = lnclient.OnchainBalanceResponse
type BalancesResponse = lnclient.BalancesResponse
-type NewOnchainAddressResponse struct {
- Address string `json:"address"`
-}
-
// debug api
type SendPaymentProbesRequest struct {
Invoice string `json:"invoice"`
diff --git a/appicon.png b/appicon.png
index 37a74c5bf..27ff80fa0 100644
Binary files a/appicon.png and b/appicon.png differ
diff --git a/build/darwin/Info.dev.plist b/build/darwin/Info.dev.plist
new file mode 100644
index 000000000..836d49860
--- /dev/null
+++ b/build/darwin/Info.dev.plist
@@ -0,0 +1,68 @@
+
+
+
+ CFBundlePackageType
+ APPL
+ CFBundleName
+ {{.Info.ProductName}}
+ CFBundleExecutable
+ {{.Name}}
+ CFBundleIdentifier
+ com.getalby.{{.Name}}
+ CFBundleVersion
+ {{.Info.ProductVersion}}
+ CFBundleGetInfoString
+ {{.Info.Comments}}
+ CFBundleShortVersionString
+ {{.Info.ProductVersion}}
+ CFBundleIconFile
+ iconfile
+ LSMinimumSystemVersion
+ 10.13.0
+ NSHighResolutionCapable
+ true
+ NSHumanReadableCopyright
+ {{.Info.Copyright}}
+ {{if .Info.FileAssociations}}
+ CFBundleDocumentTypes
+
+ {{range .Info.FileAssociations}}
+
+ CFBundleTypeExtensions
+
+ {{.Ext}}
+
+ CFBundleTypeName
+ {{.Name}}
+ CFBundleTypeRole
+ {{.Role}}
+ CFBundleTypeIconFile
+ {{.IconName}}
+
+ {{end}}
+
+ {{end}}
+ {{if .Info.Protocols}}
+ CFBundleURLTypes
+
+ {{range .Info.Protocols}}
+
+ CFBundleURLName
+ com.getalby.{{.Scheme}}
+ CFBundleURLSchemes
+
+ {{.Scheme}}
+
+ CFBundleTypeRole
+ {{.Role}}
+
+ {{end}}
+
+ {{end}}
+ NSAppTransportSecurity
+
+ NSAllowsLocalNetworking
+
+
+
+
diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist
new file mode 100644
index 000000000..ceaced927
--- /dev/null
+++ b/build/darwin/Info.plist
@@ -0,0 +1,63 @@
+
+
+
+ CFBundlePackageType
+ APPL
+ CFBundleName
+ {{.Info.ProductName}}
+ CFBundleExecutable
+ {{.Name}}
+ CFBundleIdentifier
+ com.getalby.{{.Name}}
+ CFBundleVersion
+ {{.Info.ProductVersion}}
+ CFBundleGetInfoString
+ {{.Info.Comments}}
+ CFBundleShortVersionString
+ {{.Info.ProductVersion}}
+ CFBundleIconFile
+ iconfile
+ LSMinimumSystemVersion
+ 10.13.0
+ NSHighResolutionCapable
+ true
+ NSHumanReadableCopyright
+ {{.Info.Copyright}}
+ {{if .Info.FileAssociations}}
+ CFBundleDocumentTypes
+
+ {{range .Info.FileAssociations}}
+
+ CFBundleTypeExtensions
+
+ {{.Ext}}
+
+ CFBundleTypeName
+ {{.Name}}
+ CFBundleTypeRole
+ {{.Role}}
+ CFBundleTypeIconFile
+ {{.IconName}}
+
+ {{end}}
+
+ {{end}}
+ {{if .Info.Protocols}}
+ CFBundleURLTypes
+
+ {{range .Info.Protocols}}
+
+ CFBundleURLName
+ com.getalby.{{.Scheme}}
+ CFBundleURLSchemes
+
+ {{.Scheme}}
+
+ CFBundleTypeRole
+ {{.Role}}
+
+ {{end}}
+
+ {{end}}
+
+
diff --git a/build/darwin/dmgcover.png b/build/darwin/dmgcover.png
new file mode 100644
index 000000000..1c83e2dcf
Binary files /dev/null and b/build/darwin/dmgcover.png differ
diff --git a/build/darwin/entitlements.plist b/build/darwin/entitlements.plist
new file mode 100644
index 000000000..d415eaaed
--- /dev/null
+++ b/build/darwin/entitlements.plist
@@ -0,0 +1,16 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.network.client
+
+ com.apple.security.network.server
+
+ com.apple.security.files.user-selected.read-write
+
+ com.apple.security.files.downloads.read-write
+
+
+
diff --git a/build/darwin/gon-notarize.json b/build/darwin/gon-notarize.json
new file mode 100644
index 000000000..a44abc20f
--- /dev/null
+++ b/build/darwin/gon-notarize.json
@@ -0,0 +1,8 @@
+{
+ "notarize": [
+ {
+ "path": "./build/bin/albyhub-macOS.dmg",
+ "bundle_id": "com.getalby.AlbyHub"
+ }
+ ]
+}
diff --git a/build/darwin/gon-sign.json b/build/darwin/gon-sign.json
new file mode 100644
index 000000000..7f1f7a7ef
--- /dev/null
+++ b/build/darwin/gon-sign.json
@@ -0,0 +1,13 @@
+{
+ "source": [
+ "./build/bin/AlbyHub.app",
+ "./build/bin/AlbyHub.app/Contents/MacOS/AlbyHub",
+ "./build/bin/AlbyHub.app/Contents/Frameworks/libbreez_sdk_bindings.dylib",
+ "./build/bin/AlbyHub.app/Contents/Frameworks/libglalby_bindings.dylib"
+ ],
+ "bundle_id": "com.getalby.AlbyHub",
+ "sign": {
+ "application_identity": "Developer ID Application: Alby Inc.",
+ "entitlements_file": "./build/darwin/entitlements.plist"
+ }
+}
diff --git a/config/models.go b/config/models.go
index 1d3ed0ca5..dd397b658 100644
--- a/config/models.go
+++ b/config/models.go
@@ -9,6 +9,10 @@ const (
CashuBackendType = "CASHU"
)
+const (
+ OnchainAddressKey = "OnchainAddress"
+)
+
type AppConfig struct {
Relay string `envconfig:"RELAY" default:"wss://relay.getalby.com/v1"`
LNBackendType string `envconfig:"LN_BACKEND_TYPE"`
@@ -21,7 +25,7 @@ type AppConfig struct {
CookieSecret string `envconfig:"COOKIE_SECRET"`
LogLevel string `envconfig:"LOG_LEVEL"`
LDKNetwork string `envconfig:"LDK_NETWORK" default:"bitcoin"`
- LDKEsploraServer string `envconfig:"LDK_ESPLORA_SERVER" default:"https://electrs.albylabs.com"`
+ LDKEsploraServer string `envconfig:"LDK_ESPLORA_SERVER" default:"https://electrs.albylabs.com"` // TODO: remove LDK prefix
LDKGossipSource string `envconfig:"LDK_GOSSIP_SOURCE" default:"https://rapidsync.lightningdevkit.org/snapshot"`
LDKLogLevel string `envconfig:"LDK_LOG_LEVEL"`
MempoolApi string `envconfig:"MEMPOOL_API" default:"https://mempool.space/api"`
@@ -32,7 +36,7 @@ type AppConfig struct {
BaseUrl string `envconfig:"BASE_URL" default:"http://localhost:8080"`
FrontendUrl string `envconfig:"FRONTEND_URL"`
LogEvents bool `envconfig:"LOG_EVENTS" default:"false"`
- PhoenixdAddress string `envconfig:"PHOENIXD_ADDRESS" default:"http://127.0.0.1:9740"`
+ PhoenixdAddress string `envconfig:"PHOENIXD_ADDRESS"`
PhoenixdAuthorization string `envconfig:"PHOENIXD_AUTHORIZATION"`
GoProfilerAddr string `envconfig:"GO_PROFILER_ADDR"`
DdProfilerEnabled bool `envconfig:"DD_PROFILER_ENABLED" default:"false"`
diff --git a/events/models.go b/events/models.go
index 315b1917f..bd4987968 100644
--- a/events/models.go
+++ b/events/models.go
@@ -29,9 +29,10 @@ type ChannelBackupEvent struct {
}
type ChannelBackupInfo struct {
- ChannelID string `json:"channel_id"`
- NodeID string `json:"node_id"`
- PeerID string `json:"peer_id"`
- ChannelSize uint64 `json:"channel_size"`
- FundingTxID string `json:"funding_tx_id"`
+ ChannelID string `json:"channel_id"`
+ NodeID string `json:"node_id"`
+ PeerID string `json:"peer_id"`
+ ChannelSize uint64 `json:"channel_size"`
+ FundingTxID string `json:"funding_tx_id"`
+ FundingTxVout uint32 `json:"funding_tx_vout"`
}
diff --git a/frontend/.eslintignore b/frontend/.eslintignore
deleted file mode 100644
index 76add878f..000000000
--- a/frontend/.eslintignore
+++ /dev/null
@@ -1,2 +0,0 @@
-node_modules
-dist
\ No newline at end of file
diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs
deleted file mode 100644
index 05ceb4551..000000000
--- a/frontend/.eslintrc.cjs
+++ /dev/null
@@ -1,29 +0,0 @@
-module.exports = {
- root: true,
- env: { browser: true, es2020: true },
- extends: [
- "eslint:recommended",
- "plugin:@typescript-eslint/recommended",
- "plugin:react-hooks/recommended",
- "prettier",
- ],
- ignorePatterns: ["dist", ".eslintrc.cjs"],
- parser: "@typescript-eslint/parser",
- plugins: ["react-refresh", "@typescript-eslint"],
- rules: {
- "react-refresh/only-export-components": [
- "warn",
- { allowConstantExport: true },
- ],
- "@typescript-eslint/ban-ts-comment": [
- "error",
- {
- "ts-ignore": "allow-with-description",
- },
- ],
- "@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
- "no-console": ["error", { allow: ["info", "warn", "error"] }],
- "no-constant-binary-expression": "error",
- curly: "error",
- },
-};
diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs
new file mode 100644
index 000000000..11d0af740
--- /dev/null
+++ b/frontend/eslint.config.mjs
@@ -0,0 +1,83 @@
+import { fixupConfigRules, fixupPluginRules } from "@eslint/compat";
+import { FlatCompat } from "@eslint/eslintrc";
+import js from "@eslint/js";
+import typescriptEslint from "@typescript-eslint/eslint-plugin";
+import tsParser from "@typescript-eslint/parser";
+import reactRefresh from "eslint-plugin-react-refresh";
+import globals from "globals";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const compat = new FlatCompat({
+ baseDirectory: __dirname,
+ recommendedConfig: js.configs.recommended,
+ allConfig: js.configs.all,
+});
+
+export default [
+ {
+ ignores: [
+ "**/dist",
+ "**/node_modules",
+ "**/dist",
+ "src/components/ui/navigation-menu.tsx",
+ ],
+ },
+ ...fixupConfigRules(
+ compat.extends(
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:react-hooks/recommended",
+ "prettier"
+ )
+ ),
+ {
+ plugins: {
+ "react-refresh": reactRefresh,
+ "@typescript-eslint": fixupPluginRules(typescriptEslint),
+ },
+
+ languageOptions: {
+ globals: {
+ ...globals.browser,
+ },
+
+ parser: tsParser,
+ },
+ files: ["**/*.ts", "**/*.tsx"],
+ rules: {
+ "react-refresh/only-export-components": [
+ "warn",
+ {
+ allowConstantExport: true,
+ },
+ ],
+
+ "@typescript-eslint/ban-ts-comment": [
+ "error",
+ {
+ "ts-ignore": "allow-with-description",
+ },
+ ],
+
+ "@typescript-eslint/no-unused-vars": [
+ "warn",
+ {
+ args: "none",
+ },
+ ],
+
+ "no-console": [
+ "error",
+ {
+ allow: ["info", "warn", "error"],
+ },
+ ],
+
+ "no-constant-binary-expression": "error",
+ curly: "error",
+ },
+ },
+];
diff --git a/frontend/lint-staged.config.js b/frontend/lint-staged.config.js
index 653218efc..36a197e72 100644
--- a/frontend/lint-staged.config.js
+++ b/frontend/lint-staged.config.js
@@ -1,5 +1,8 @@
export default {
- "src/**/*.{ts,tsx,json}": ["eslint --fix --max-warnings 0","prettier --write"],
+ "src/**/*.{ts,tsx,json}": [
+ "eslint --fix --no-warn-ignored --max-warnings 0",
+ "prettier --write",
+ ],
"platform_specific/**/*.ts": ["prettier --write"],
"package.json": ["prettier --write"],
"src/**/*.ts": () => "tsc --noEmit",
diff --git a/frontend/package.json b/frontend/package.json
index 0d4544c0a..27f39f839 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -11,8 +11,8 @@
"prepare:wails": "shx cp ./platform_specific/wails/src/utils/*.ts src/utils/",
"prepare:http": "shx cp ./platform_specific/http/src/utils/*.ts src/utils/",
"lint": "yarn lint:js && yarn tsc:compile && yarn format:fix --report-unused-disable-directives --max-warnings 0",
- "lint:js": "eslint src --ext .js,.ts,.tsx --max-warnings 0",
- "lint:js:fix": "eslint src --ext .js,.ts,.tsx --fix",
+ "lint:js": "eslint src --max-warnings 0",
+ "lint:js:fix": "eslint src --fix",
"tsc:compile": "tsc --noEmit",
"format": "prettier --check '**/*.(md|json)' 'src/**/*.(js|ts|tsx)' '!src/assets/**/*.json' 'platform_specific/**/*.ts'",
"format:fix": "prettier --loglevel silent --write '**/*.(md|json)' 'src/**/*.(js|ts|tsx)' '!src/assets/**/*.json' 'platform_specific/**/*.ts'",
@@ -59,6 +59,9 @@
"devDependencies": {
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
+ "@eslint/compat": "^1.0.3",
+ "@eslint/eslintrc": "^3.1.0",
+ "@eslint/js": "^9.4.0",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
@@ -71,10 +74,11 @@
"@typescript-eslint/parser": "^7.11.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.16",
- "eslint": "^8.45.0",
+ "eslint": "^9.4.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
+ "globals": "^15.4.0",
"husky": "^9.0.11",
"lint-staged": "^15.2.5",
"postcss": "^8.4.32",
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 8acb296d3..63434df7b 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,138 +1,21 @@
-import { HashRouter, Navigate, Route, Routes } from "react-router-dom";
+import { RouterProvider, createHashRouter } from "react-router-dom";
-import AppLayout from "src/components/layouts/AppLayout";
-import { DefaultRedirect } from "src/components/redirects/DefaultRedirect";
-import { HomeRedirect } from "src/components/redirects/HomeRedirect";
-import { SetupRedirect } from "src/components/redirects/SetupRedirect";
-import { StartRedirect } from "src/components/redirects/StartRedirect";
import { ThemeProvider } from "src/components/ui/theme-provider";
-import { BackupMnemonic } from "src/screens/BackupMnemonic";
-import NotFound from "src/screens/NotFound";
-import Start from "src/screens/Start";
-import Unlock from "src/screens/Unlock";
-import { Welcome } from "src/screens/Welcome";
-import AppCreated from "src/screens/apps/AppCreated";
-import AppList from "src/screens/apps/AppList";
-import NewApp from "src/screens/apps/NewApp";
-import ShowApp from "src/screens/apps/ShowApp";
-import AppStore from "src/screens/appstore/AppStore";
-import Channels from "src/screens/channels/Channels";
-import NewChannel from "src/screens/channels/NewChannel";
-import MigrateAlbyFunds from "src/screens/onboarding/MigrateAlbyFunds";
-import NewOnchainAddress from "src/screens/onchain/NewAddress";
-import ConnectPeer from "src/screens/peers/ConnectPeer";
-import Settings from "src/screens/settings/Settings";
-import { ImportMnemonic } from "src/screens/setup/ImportMnemonic";
-import { SetupFinish } from "src/screens/setup/SetupFinish";
-import { SetupNode } from "src/screens/setup/SetupNode";
-import { SetupPassword } from "src/screens/setup/SetupPassword";
-import { SetupWallet } from "src/screens/setup/SetupWallet";
-import Wallet from "src/screens/wallet";
-import SignMessage from "src/screens/wallet/SignMessage";
import { usePosthog } from "./hooks/usePosthog";
-import SettingsLayout from "src/components/layouts/SettingsLayout";
-import TwoColumnFullScreenLayout from "src/components/layouts/TwoColumnFullScreenLayout";
-import { OnboardingRedirect } from "src/components/redirects/OnboardingRedirect";
import { Toaster } from "src/components/ui/toaster";
-import { BackupNode } from "src/screens/BackupNode";
-import { BackupNodeSuccess } from "src/screens/BackupNodeSuccess";
-import { Intro } from "src/screens/Intro";
-import AlbyAuthRedirect from "src/screens/alby/AlbyAuthRedirect";
-import { CurrentChannelOrder } from "src/screens/channels/CurrentChannelOrder";
-import { Success } from "src/screens/onboarding/Success";
-import Peers from "src/screens/peers/Peers";
-import { ChangeUnlockPassword } from "src/screens/settings/ChangeUnlockPassword";
-import DebugTools from "src/screens/settings/DebugTools";
-import { RestoreNode } from "src/screens/setup/RestoreNode";
+import routes from "src/routes.tsx";
function App() {
usePosthog();
+
+ const router = createHashRouter(routes);
+
return (
<>
-
-
- }
- />
- }>
- } />
- }>
- }>
- } />
- }
- />
- } />
- } />
-
-
- }>
- } />
- } />
-
- }>
- } />
-
- }>
- } />
- } />
- } />
- } />
-
- }>
- } />
-
- }>
- } />
- } />
- } />
- }
- />
-
- }>
- } />
- } />
-
-
- } />
- }>
-
-
-
- }
- />
- } />
- } />
- } />
- }>
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
- }>
-
- } />
-
- } />
-
-
- } />
-
-
+
>
);
diff --git a/frontend/src/assets/images/node/cashu.png b/frontend/src/assets/images/node/cashu.png
new file mode 100644
index 000000000..b86012b32
Binary files /dev/null and b/frontend/src/assets/images/node/cashu.png differ
diff --git a/frontend/src/assets/images/node/lnd.png b/frontend/src/assets/images/node/lnd.png
new file mode 100644
index 000000000..fbfeedae5
Binary files /dev/null and b/frontend/src/assets/images/node/lnd.png differ
diff --git a/frontend/src/components/AlbyConnectionCard.tsx b/frontend/src/components/AlbyConnectionCard.tsx
new file mode 100644
index 000000000..5116441bb
--- /dev/null
+++ b/frontend/src/components/AlbyConnectionCard.tsx
@@ -0,0 +1,156 @@
+import {
+ CheckCircle2,
+ CircleX,
+ Edit,
+ ExternalLinkIcon,
+ Link2Icon,
+ ZapIcon,
+} from "lucide-react";
+import { Link } from "react-router-dom";
+import ExternalLink from "src/components/ExternalLink";
+import Loading from "src/components/Loading";
+import UserAvatar from "src/components/UserAvatar";
+import { Button } from "src/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "src/components/ui/card";
+import { LoadingButton } from "src/components/ui/loading-button";
+import { Progress } from "src/components/ui/progress";
+import { Separator } from "src/components/ui/separator";
+import { useAlbyMe } from "src/hooks/useAlbyMe";
+import { LinkStatus, useLinkAccount } from "src/hooks/useLinkAccount";
+import { App } from "src/types";
+
+function AlbyConnectionCard({ connection }: { connection?: App }) {
+ const { data: albyMe } = useAlbyMe();
+ const { loading, linkStatus, loadingLinkStatus, linkAccount } =
+ useLinkAccount();
+
+ return (
+
+
+ Alby Account
+
+ Link Your Alby Account to use your lightning address with Alby Hub and
+ use apps that you connected to your Alby Account.
+
+
+
+
+
+
+
+
+
+
{albyMe?.name}
+
+
+ {albyMe?.lightning_address}
+
+
+
+
+ {loadingLinkStatus && }
+ {!connection || linkStatus === LinkStatus.SharedNode ? (
+
+ {!loading && }
+ Link your Alby Account
+
+ ) : linkStatus === LinkStatus.ThisNode ? (
+
+ ) : (
+ linkStatus === LinkStatus.OtherNode && (
+
+ )
+ )}
+
+
+
+
+
+
+ {connection && (
+ <>
+ {connection.maxAmount > 0 && (
+ <>
+
+
+
+ You've spent
+
+
+ {new Intl.NumberFormat().format(
+ connection.budgetUsage
+ )}{" "}
+ sats
+
+
+
+ {" "}
+
+ Left in budget
+
+
+ {new Intl.NumberFormat().format(
+ connection.maxAmount - connection.budgetUsage
+ )}{" "}
+ sats
+
+
+
+
+
+ {connection.maxAmount > 0 ? (
+ <>
+ {new Intl.NumberFormat().format(connection.maxAmount)}{" "}
+ sats / {connection.budgetRenewal}
+ >
+ ) : (
+ "Not set"
+ )}
+
+
+
+
+
+
+ >
+ )}
+ >
+ )}
+
+
+
+
+ );
+}
+
+export default AlbyConnectionCard;
diff --git a/frontend/src/components/AppHeader.tsx b/frontend/src/components/AppHeader.tsx
index 9d5cec875..a4e59af43 100644
--- a/frontend/src/components/AppHeader.tsx
+++ b/frontend/src/components/AppHeader.tsx
@@ -1,4 +1,5 @@
import { ReactElement } from "react";
+import Breadcrumbs from "src/components/Breadcrumbs";
type Props = {
title: string | ReactElement;
@@ -8,13 +9,18 @@ type Props = {
function AppHeader({ title, description, contentRight }: Props) {
return (
-
-
-
{title}
-
{description}
+ <>
+
+
+
+
{title}
+
+ {description}
+
+
+
{contentRight}
-
{contentRight}
-
+ >
);
}
diff --git a/frontend/src/components/AuthCodeForm.tsx b/frontend/src/components/AuthCodeForm.tsx
index ff2cdcf17..138125a6f 100644
--- a/frontend/src/components/AuthCodeForm.tsx
+++ b/frontend/src/components/AuthCodeForm.tsx
@@ -61,12 +61,14 @@ function AuthCodeForm() {