├── .dockerignore ├── .env.example ├── .github ├── dependabot.yaml └── workflows │ ├── build-docker.yaml │ ├── create-release.yaml │ ├── http.yml │ ├── linting.yml │ ├── release.yaml │ ├── test-postgres.yml │ └── wails.yml ├── .gitignore ├── .mockery.yaml ├── .prettierignore ├── .vscode ├── extensions.json └── settings.json ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── alby ├── alby_oauth_service.go ├── alby_oauth_service_test.go ├── alby_service.go └── models.go ├── api ├── api.go ├── api_test.go ├── apps_test.go ├── backup.go ├── esplora.go ├── lsp.go ├── models.go ├── rebalance.go └── transactions.go ├── appicon.png ├── apps ├── apps_service.go └── tests │ └── apps_service_test.go ├── build ├── darwin │ ├── Info.dev.plist │ ├── Info.plist │ ├── dmgcover.png │ ├── entitlements.plist │ ├── gon-sign.json │ └── http │ │ └── gon-notarize.json └── docker │ └── copy_dylibs.sh ├── cmd ├── db_migrate │ ├── main.go │ └── migrate_test.go └── http │ └── main.go ├── config ├── aesgcm.go ├── aesgcm_test.go ├── config.go └── models.go ├── constants └── constants.go ├── db ├── db.go ├── migrations │ ├── 202401191539_initial_migration.go │ ├── 202401191539_initial_migration.sql.tmpl │ ├── 202403171120_delete_ldk_payments.go │ ├── 202404021909_nullable_expires_at.go │ ├── 202405302121_store_decrypted_request.go │ ├── 202406061259_delete_content.go │ ├── 202406071726_vacuum.go │ ├── 202406301207_rename_request_methods.go │ ├── 202407012100_transactions.go │ ├── 202407151352_autoincrement.go │ ├── 202407201604_transactions_indexes.go │ ├── 202407262257_remove_invalid_scopes.go │ ├── 202408061737_add_boostagrams_and_use_json.go │ ├── 202408191242_transaction_failure_reason.go │ ├── 202408291715_app_metadata.go │ ├── 202410141503_add_wallet_pubkey.go │ ├── 202412212345_fix_types.go │ ├── 202504231037_add_indexes.go │ ├── 202504231037_hold_invoices.go │ ├── 202506170342_swaps.go │ ├── 202508041712_delete_non_cascade_deleted_records.go │ ├── 202508041737_postgres_amount_bigint.go │ ├── 202508041738_app_last_used.go │ ├── 202508041739_response_events_index.go │ ├── 202508151405_swap_xpub.go │ ├── 202508192137_forwards.go │ ├── 202509031250_transactions_updated_at_index.go │ ├── README.md │ └── migrate.go ├── models.go ├── queries │ ├── get_budget_usage.go │ ├── get_isolated_balance.go │ └── get_isolated_balance_test.go ├── sqlite-wrapper │ └── driver.go └── test │ └── db_test.go ├── doc └── logo.svg ├── docker-compose.yml ├── events ├── events.go └── models.go ├── fly.toml ├── frontend ├── .env.local.example ├── .husky │ ├── commit-msg │ └── pre-commit ├── .prettierignore ├── .prettierrc ├── commitlint.config.js ├── components.json ├── eslint.config.mjs ├── frontend.go ├── index.html ├── lint-staged.config.js ├── package.json ├── platform_specific │ ├── http │ │ └── src │ │ │ └── utils │ │ │ ├── openLink.ts │ │ │ └── request.ts │ └── wails │ │ └── src │ │ └── utils │ │ ├── openLink.ts │ │ └── request.ts ├── public │ ├── favicon.ico │ ├── favicon.svg │ ├── fonts │ │ ├── Inter-italic.var.woff2 │ │ └── Inter-roman.var.woff2 │ ├── icon-1024.png │ ├── icon-192.png │ ├── icon-512.png │ ├── images │ │ ├── illustrations │ │ │ ├── alby-account-dark.svg │ │ │ ├── alby-account-light.svg │ │ │ ├── lightning-network-dark.svg │ │ │ ├── lightning-network-light.svg │ │ │ ├── sub-wallet-dark.svg │ │ │ ├── sub-wallet-light.svg │ │ │ └── tick.svg │ │ └── quotes │ │ │ ├── antonopoulos.svg │ │ │ ├── back.svg │ │ │ ├── finney.svg │ │ │ ├── hayek.svg │ │ │ ├── nakamoto.svg │ │ │ ├── obama.svg │ │ │ ├── roland.svg │ │ │ ├── saylor.svg │ │ │ └── wilson.svg │ └── robots.txt ├── src │ ├── App.tsx │ ├── assets │ │ ├── images │ │ │ ├── alby-head.svg │ │ │ ├── cloud.png │ │ │ ├── cloud2.png │ │ │ └── node │ │ │ │ ├── cashu.png │ │ │ │ └── lnd.png │ │ ├── lotties │ │ │ ├── loading-dark.json │ │ │ └── loading-light.json │ │ ├── suggested-apps │ │ │ ├── alby-go.png │ │ │ ├── alby-hub.png │ │ │ ├── alby.png │ │ │ ├── amethyst.png │ │ │ ├── bitrefill.png │ │ │ ├── bringin.png │ │ │ ├── btcpay.png │ │ │ ├── buzzpay.png │ │ │ ├── clams.png │ │ │ ├── claude.png │ │ │ ├── coracle.png │ │ │ ├── damus.png │ │ │ ├── goose.png │ │ │ ├── habla-news.png │ │ │ ├── iris.png │ │ │ ├── kiwi.png │ │ │ ├── lightning-messageboard.png │ │ │ ├── lnbits.png │ │ │ ├── lume.png │ │ │ ├── nakapay.png │ │ │ ├── nostrcheck-server.png │ │ │ ├── nostrudel.png │ │ │ ├── nostter.png │ │ │ ├── nostur.png │ │ │ ├── paper-scissors-hodl.png │ │ │ ├── primal.png │ │ │ ├── pullthatupjamie.png │ │ │ ├── simple-boost.png │ │ │ ├── snort.png │ │ │ ├── stacker-news.png │ │ │ ├── tictactoe.png │ │ │ ├── uncle-jim.png │ │ │ ├── wave-space.png │ │ │ ├── wavlake.png │ │ │ ├── wherostr.png │ │ │ ├── yakihonne.png │ │ │ ├── zap-stream.png │ │ │ ├── zapplanner.png │ │ │ ├── zapple-pay.png │ │ │ ├── zappy-bird.png │ │ │ ├── zapstore.png │ │ │ └── zeus.png │ │ └── zapplanner │ │ │ ├── bitcoinbrink.png │ │ │ ├── hrf.png │ │ │ └── opensats.png │ ├── components │ │ ├── AnchorReserveAlert.tsx │ │ ├── AppAvatar.tsx │ │ ├── AppHeader.tsx │ │ ├── AppSidebar.tsx │ │ ├── AuthCodeForm.tsx │ │ ├── Banner.tsx │ │ ├── Breadcrumbs.tsx │ │ ├── BudgetAmountSelect.tsx │ │ ├── BudgetRenewalSelect.tsx │ │ ├── CardButton.tsx │ │ ├── CloseChannelDialogContent.tsx │ │ ├── CommandPalette.tsx │ │ ├── Container.tsx │ │ ├── CustomPagination.tsx │ │ ├── DisconnectPeerDialogContent.tsx │ │ ├── EmptyState.tsx │ │ ├── ExecuteCustomNodeCommandDialogContent.tsx │ │ ├── ExpirySelect.tsx │ │ ├── ExternalLink.tsx │ │ ├── FormattedFiatAmount.tsx │ │ ├── IsolatedAppDrawDownDialog.tsx │ │ ├── IsolatedAppTopupDialog.tsx │ │ ├── Loading.tsx │ │ ├── LottieLoading.tsx │ │ ├── LowReceivingCapacityAlert.tsx │ │ ├── MempoolAlert.tsx │ │ ├── OnchainAddressDisplay.tsx │ │ ├── PayLightningInvoice.tsx │ │ ├── PaymentFailedAlert.tsx │ │ ├── PendingPaymentAlert.tsx │ │ ├── Permissions.tsx │ │ ├── PodcastingInfo.tsx │ │ ├── QRCode.tsx │ │ ├── RebalanceChannelDialogContent.tsx │ │ ├── ResetRoutingDataDialogContent.tsx │ │ ├── ResponsiveButton.tsx │ │ ├── RoutingFeeDialogContent.tsx │ │ ├── Scopes.tsx │ │ ├── SettingsHeader.tsx │ │ ├── SidebarHint.tsx │ │ ├── SpendingAlert.tsx │ │ ├── TransactionItem.tsx │ │ ├── TransactionsList.tsx │ │ ├── TransactionsListMenu.tsx │ │ ├── TwoColumnLayoutHeader.tsx │ │ ├── UnlinkAlbyAccount.tsx │ │ ├── UpgradeCard.tsx │ │ ├── UpgradeDialog.tsx │ │ ├── UserAvatar.tsx │ │ ├── channels │ │ │ ├── ChannelDropdownMenu.tsx │ │ │ ├── ChannelPeerNote.tsx │ │ │ ├── ChannelPublicPrivateAlert.tsx │ │ │ ├── ChannelWaitingForConfirmations.tsx │ │ │ ├── ChannelWarning.tsx │ │ │ ├── ChannelsCards.tsx │ │ │ ├── ChannelsTable.tsx │ │ │ ├── DuplicateChannelAlert.tsx │ │ │ ├── HealthcheckAlert.tsx │ │ │ ├── LDKChannelWithoutPeerAlert.tsx │ │ │ ├── LSPTermsDialog.tsx │ │ │ ├── OnchainTransactionsTable.tsx │ │ │ └── SwapAlert.tsx │ │ ├── connections │ │ │ ├── AboutAppCard.tsx │ │ │ ├── AlbyConnectionCard.tsx │ │ │ ├── AppCard.tsx │ │ │ ├── AppCardConnectionInfo.tsx │ │ │ ├── AppCardNotice.tsx │ │ │ ├── AppDetailConnectedApps.tsx │ │ │ ├── AppLinksCard.tsx │ │ │ ├── AppStore.tsx │ │ │ ├── AppStoreDetailHeader.tsx │ │ │ ├── AppTransactionList.tsx │ │ │ ├── AppUsage.tsx │ │ │ ├── ConnectedApps.tsx │ │ │ ├── ConnectionDetailsModal.tsx │ │ │ ├── DisconnectApp.tsx │ │ │ ├── InstallApp.tsx │ │ │ ├── SuggestedAppData.tsx │ │ │ └── SuggestedApps.tsx │ │ ├── home │ │ │ └── widgets │ │ │ │ ├── AppOfTheDayWidget.tsx │ │ │ │ ├── BlockHeightWidget.tsx │ │ │ │ ├── ForwardsWidget.tsx │ │ │ │ ├── LatestUsedAppsWidget.tsx │ │ │ │ ├── LightningMessageboardWidget.tsx │ │ │ │ ├── NodeStatusWidget.tsx │ │ │ │ ├── OnchainFeesWidget.tsx │ │ │ │ ├── SupportAlbyWidget.tsx │ │ │ │ └── WhatsNewWidget.tsx │ │ ├── icons │ │ │ ├── Alby.tsx │ │ │ ├── AlbyHubIcon.tsx │ │ │ ├── AlbyHubLogo.tsx │ │ │ ├── Apple.tsx │ │ │ ├── Chrome.tsx │ │ │ ├── Firefox.tsx │ │ │ ├── LDK.tsx │ │ │ ├── Lightning.tsx │ │ │ ├── NostrWalletConnectIcon.tsx │ │ │ ├── Phoenixd.tsx │ │ │ ├── PlayStore.tsx │ │ │ └── ZapStore.tsx │ │ ├── images │ │ │ └── AlbyHead.tsx │ │ ├── layouts │ │ │ ├── AppLayout.tsx │ │ │ ├── ReceiveLayout.tsx │ │ │ ├── SettingsLayout.tsx │ │ │ └── TwoColumnFullScreenLayout.tsx │ │ ├── mnemonic │ │ │ ├── MnemonicDialog.tsx │ │ │ └── MnemonicInputs.tsx │ │ ├── password │ │ │ ├── PasswordInput.tsx │ │ │ └── RevealPasswordToggle.tsx │ │ ├── redirects │ │ │ ├── DefaultRedirect.tsx │ │ │ ├── HomeRedirect.tsx │ │ │ ├── SetupRedirect.tsx │ │ │ └── StartRedirect.tsx │ │ ├── stepper.tsx │ │ └── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── buttonVariants.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── command.tsx │ │ │ ├── custom │ │ │ ├── carousel-dots.tsx │ │ │ ├── circle-progress.tsx │ │ │ ├── external-link-button.tsx │ │ │ ├── input-with-adornment.tsx │ │ │ ├── link-button.tsx │ │ │ ├── loading-button.tsx │ │ │ └── useDotButton.tsx │ │ │ ├── dialog.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── search-input.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── sidebar.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── textarea.tsx │ │ │ ├── theme-provider.tsx │ │ │ └── tooltip.tsx │ ├── constants.ts │ ├── contexts │ │ └── CommandPaletteContext.tsx │ ├── fonts.css │ ├── hooks │ │ ├── use-mobile.ts │ │ ├── use-mobile.tsx │ │ ├── useAlbyInfo.ts │ │ ├── useAlbyMe.ts │ │ ├── useApp.ts │ │ ├── useApps.ts │ │ ├── useBalances.ts │ │ ├── useBanner.tsx │ │ ├── useBitcoinRate.ts │ │ ├── useCapabilities.ts │ │ ├── useChannelPeerSuggestions.ts │ │ ├── useChannels.ts │ │ ├── useCommandPalette.ts │ │ ├── useCreateLightningAddress.ts │ │ ├── useDeleteApp.ts │ │ ├── useDeleteLightningAddress.ts │ │ ├── useForwards.ts │ │ ├── useHealthCheck.ts │ │ ├── useInfo.ts │ │ ├── useLSPChannelOffer.tsx │ │ ├── useLinkAccount.ts │ │ ├── useMempoolApi.ts │ │ ├── useMigrateLDKStorage.ts │ │ ├── useNodeConnectionInfo.ts │ │ ├── useNodeDetails.ts │ │ ├── useNotifyReceivedPayments.ts │ │ ├── useOnboardingData.ts │ │ ├── useOnchainAddress.ts │ │ ├── useOnchainTransactions.ts │ │ ├── usePeers.ts │ │ ├── useRemoveSuccessfulChannelOrder.ts │ │ ├── useSwaps.tsx │ │ ├── useSyncWallet.ts │ │ ├── useTransaction.ts │ │ ├── useTransactions.ts │ │ └── useUnusedApps.ts │ ├── index.css │ ├── lib │ │ ├── auth.ts │ │ ├── backendType.ts │ │ ├── clipboard.ts │ │ └── utils.ts │ ├── main.tsx │ ├── requests │ │ └── createApp.ts │ ├── routes.tsx │ ├── screens │ │ ├── ConnectAlbyAccount.tsx │ │ ├── CreateNodeMigrationFileSuccess.tsx │ │ ├── Home.tsx │ │ ├── Intro.tsx │ │ ├── MigrateNode.tsx │ │ ├── NotFound.tsx │ │ ├── Start.tsx │ │ ├── Unlock.tsx │ │ ├── Welcome.tsx │ │ ├── alby │ │ │ ├── AlbyAuthRedirect.tsx │ │ │ ├── AlbyReviews.tsx │ │ │ └── SupportAlby.tsx │ │ ├── apps │ │ │ ├── AppDetails.tsx │ │ │ ├── AppsCleanup.tsx │ │ │ ├── ConnectAppCard.tsx │ │ │ ├── Connections.tsx │ │ │ └── NewApp.tsx │ │ ├── appstore │ │ │ └── AppStoreDetail.tsx │ │ ├── channels │ │ │ ├── Channels.tsx │ │ │ ├── CurrentChannelOrder.tsx │ │ │ ├── IncreaseIncomingCapacity.tsx │ │ │ ├── IncreaseOutgoingCapacity.tsx │ │ │ ├── auto │ │ │ │ ├── AutoChannel.tsx │ │ │ │ ├── OpenedAutoChannel.tsx │ │ │ │ └── OpeningAutoChannel.tsx │ │ │ └── first │ │ │ │ ├── FirstChannel.tsx │ │ │ │ ├── OpenedFirstChannel.tsx │ │ │ │ └── OpeningFirstChannel.tsx │ │ ├── internal-apps │ │ │ ├── Bitrefill.tsx │ │ │ ├── BuzzPay.tsx │ │ │ ├── Claude.tsx │ │ │ ├── Goose.tsx │ │ │ ├── LightningMessageboard.tsx │ │ │ ├── SimpleBoost.tsx │ │ │ ├── Tictactoe.tsx │ │ │ └── ZapPlanner.tsx │ │ ├── onchain │ │ │ ├── BuyBitcoin.tsx │ │ │ └── DepositBitcoin.tsx │ │ ├── peers │ │ │ ├── ConnectPeer.tsx │ │ │ └── Peers.tsx │ │ ├── settings │ │ │ ├── About.tsx │ │ │ ├── AlbyAccount.tsx │ │ │ ├── AutoUnlock.tsx │ │ │ ├── Backup.tsx │ │ │ ├── ChangeUnlockPassword.tsx │ │ │ ├── DebugTools.tsx │ │ │ ├── DeveloperSettings.tsx │ │ │ └── Settings.tsx │ │ ├── setup │ │ │ ├── ImportMnemonic.tsx │ │ │ ├── RestoreNode.tsx │ │ │ ├── SetupAdvanced.tsx │ │ │ ├── SetupFinish.tsx │ │ │ ├── SetupNode.tsx │ │ │ ├── SetupPassword.tsx │ │ │ ├── SetupSecurity.tsx │ │ │ └── node │ │ │ │ ├── CashuForm.tsx │ │ │ │ ├── LDKForm.tsx │ │ │ │ ├── LNDForm.tsx │ │ │ │ ├── PhoenixdForm.tsx │ │ │ │ └── PresetNodeForm.tsx │ │ ├── subwallets │ │ │ ├── NewSubwallet.tsx │ │ │ ├── SubwalletCreated.tsx │ │ │ ├── SubwalletIntro.tsx │ │ │ └── SubwalletList.tsx │ │ └── wallet │ │ │ ├── NodeAlias.tsx │ │ │ ├── OnboardingChecklist.tsx │ │ │ ├── Receive.tsx │ │ │ ├── Send.tsx │ │ │ ├── SignMessage.tsx │ │ │ ├── WithdrawOnchainFunds.tsx │ │ │ ├── index.tsx │ │ │ ├── receive │ │ │ ├── ReceiveInvoice.tsx │ │ │ ├── ReceiveOffer.tsx │ │ │ └── ReceiveOnchain.tsx │ │ │ ├── send │ │ │ ├── ConfirmPayment.tsx │ │ │ ├── LnurlPay.tsx │ │ │ ├── Onchain.tsx │ │ │ ├── OnchainSuccess.tsx │ │ │ ├── PaymentSuccess.tsx │ │ │ └── ZeroAmount.tsx │ │ │ └── swap │ │ │ ├── AutoSwap.tsx │ │ │ ├── SwapInStatus.tsx │ │ │ ├── SwapOutStatus.tsx │ │ │ └── index.tsx │ ├── state │ │ ├── ChannelOrderStore.ts │ │ └── SetupStore.ts │ ├── themes │ │ ├── alby.css │ │ ├── bitcoin.css │ │ ├── claymorphism.css │ │ ├── default.css │ │ ├── ghibli.css │ │ ├── index.css │ │ ├── matrix.css │ │ └── nostr.css │ ├── types.ts │ ├── utils │ │ ├── handleRequestError.ts │ │ ├── isHttpMode.ts │ │ └── swr.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── types │ └── argon2-wasm-esm.d.ts ├── vite.config.ts └── yarn.lock ├── go.mod ├── go.sum ├── http ├── alby_http_service.go ├── http_service.go ├── http_service_test.go └── models.go ├── lnclient ├── cashu │ └── cashu.go ├── ldk │ ├── ldk.go │ ├── ldk_event_broadcaster.go │ ├── ldk_logger.go │ └── ldk_test.go ├── lnd │ ├── lnd.go │ └── wrapper │ │ ├── interface.go │ │ └── lnd.go ├── models.go └── phoenixd │ └── phoenixd.go ├── logger └── logger.go ├── lsp └── models.go ├── main_wails.go ├── nip47 ├── cipher │ ├── cipher.go │ └── cipher_test.go ├── controllers │ ├── cancel_hold_invoice_controller.go │ ├── cancel_hold_invoice_controller_test.go │ ├── controllers_test.go │ ├── create_connection_controller.go │ ├── create_connection_controller_test.go │ ├── decode_request.go │ ├── get_balance_controller.go │ ├── get_balance_controller_test.go │ ├── get_budget_controller.go │ ├── get_budget_controller_test.go │ ├── get_info_controller.go │ ├── get_info_controller_test.go │ ├── list_transactions_controller.go │ ├── list_transactions_controller_test.go │ ├── lookup_invoice_controller.go │ ├── lookup_invoice_controller_test.go │ ├── make_hold_invoice_controller.go │ ├── make_hold_invoice_controller_test.go │ ├── make_invoice_controller.go │ ├── make_invoice_controller_test.go │ ├── map_nip47_error.go │ ├── models.go │ ├── multi_pay_invoice_controller.go │ ├── multi_pay_invoice_controller_test.go │ ├── multi_pay_keysend_controller.go │ ├── multi_pay_keysend_controller_test.go │ ├── nip47_controller.go │ ├── pay_invoice_controller.go │ ├── pay_invoice_controller_test.go │ ├── pay_keysend_controller.go │ ├── pay_keysend_controller_test.go │ ├── settle_hold_invoice_controller.go │ ├── settle_hold_invoice_controller_test.go │ └── sign_message_controller.go ├── event_handler.go ├── event_handler_shared_wallet_pubkey_test.go ├── event_handler_test.go ├── models │ ├── models.go │ └── transactions.go ├── nip47_service.go ├── notifications │ ├── models.go │ ├── nip47_notification_queue.go │ ├── nip47_notifier.go │ └── nip47_notifier_test.go ├── permissions │ ├── permissions.go │ └── permissions_test.go └── publish_nip47_info.go ├── nostr └── models │ └── models.go ├── render.yaml ├── scripts ├── caddy-subpath │ ├── Caddyfile │ └── README.md ├── keys │ └── rolznz.asc ├── linux-aarch64 │ ├── Caddyfile.example │ ├── README.md │ ├── install.sh │ └── update.sh ├── linux-x86_64 │ ├── Caddyfile.example │ ├── README.md │ ├── install.sh │ ├── phoenixd │ │ ├── README.md │ │ ├── docker-compose.yml │ │ ├── install.sh │ │ └── render.yaml │ └── update.sh ├── pi-aarch64 │ ├── README.md │ ├── install.sh │ └── update.sh ├── pi-arm │ ├── README.md │ ├── install.sh │ └── update.sh └── verify.sh ├── service ├── create_app_consumer.go ├── delete_app_consumer.go ├── keys │ ├── keys.go │ └── keys_test.go ├── models.go ├── payment_forwarded_consumer.go ├── profiler.go ├── service.go ├── start.go ├── stop.go └── update_app_consumer.go ├── swaps └── swaps_service.go ├── tests ├── create_app.go ├── create_mock_relay.go ├── db │ ├── postgres │ │ └── docker-compose.yml │ └── test_db.go ├── mock_event_consumer.go ├── mock_ln_client.go ├── mocks │ ├── AlbyOAuthService.go │ ├── AlbyService.go │ ├── Config.go │ ├── EventPublisher.go │ ├── Keys.go │ ├── LNClient.go │ └── Service.go └── test_service.go ├── transactions ├── app_payments_test.go ├── check_unsettled_transaction_test.go ├── hold_invoice_self_payment_consumer.go ├── isolated_app_payments_test.go ├── keysend_test.go ├── list_transactions_test.go ├── lookup_transaction_test.go ├── make_invoice_test.go ├── notifications_test.go ├── payments_test.go ├── receive_keysend_test.go ├── self_hold_payments_test.go ├── self_payments_test.go └── transactions_service.go ├── utils ├── utils.go └── utils_test.go ├── version └── version.go ├── wails.json └── wails ├── wails_app.go └── wails_handlers.go /.dockerignore: -------------------------------------------------------------------------------- 1 | frontend/node_modules 2 | frontend/dist -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # only enable event logging in production 2 | SEND_EVENTS_TO_ALBY=false 3 | 4 | # do not link your current account when you run a dev instance (so it stays pointing at your mainnet one) 5 | AUTO_LINK_ALBY_ACCOUNT=false 6 | 7 | # Optionally set LDK debug log level to get more info 8 | #LDK_LOG_LEVEL=2 9 | # Optionally set Main application debug log level to get more info 10 | #LOG_LEVEL=5 11 | 12 | # Base URL required for custom OAuth client 13 | #BASE_URL=http://localhost:8080 14 | # Development settings (yarn dev:http) 15 | FRONTEND_URL=http://localhost:5173 16 | 17 | #REBALANCE_SERVICE_URL=https://lsp1.mutiny.megalith-node.com 18 | 19 | #AUTO_UNLOCK_PASSWORD=123 20 | #WORK_DIR=.data 21 | #DATABASE_URI=nwc.db 22 | #JWT_SECRET=secretsecret 23 | #RELAY=wss://relay.getalby.com/v1 24 | #RELAY=ws://localhost:7447/v1 25 | #PORT=8080 26 | 27 | #LOG_DB_QUERIES=true 28 | 29 | # Alby OAuth configuration 30 | #ALBY_OAUTH_CLIENT_SECRET= 31 | #ALBY_OAUTH_CLIENT_ID= 32 | 33 | 34 | # Polar LND Client 35 | #LN_BACKEND_TYPE=LND 36 | #LND_CERT_FILE=/home/YOUR_USERNAME/.polar/networks/1/volumes/lnd/alice/tls.cert 37 | #LND_ADDRESS=127.0.0.1:10001 38 | #LND_MACAROON_FILE=/home/YOUR_USERNAME/.polar/networks/1/volumes/lnd/alice/data/chain/bitcoin/regtest/admin.macaroon 39 | 40 | #LDK_VSS_URL="http://localhost:8090/vss" 41 | 42 | # Boltz API 43 | #BOLTZ_API=https://api.testnet.boltz.exchange 44 | #NETWORK=testnet -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: npm 8 | directory: /frontend 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /.github/workflows/build-docker.yaml: -------------------------------------------------------------------------------- 1 | name: Multiplatform Docker build & push 2 | on: 3 | push: 4 | jobs: 5 | build: 6 | env: 7 | REGISTRY: ghcr.io 8 | IMAGENAME: ${{ github.event.repository.name }} 9 | TAG: ${{ github.ref_name }} 10 | runs-on: ubuntu-22.04 11 | steps: 12 | - uses: actions/checkout@v4 13 | name: Check out code 14 | - name: Install Go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version-file: "./go.mod" 18 | - name: Run tests 19 | run: mkdir frontend/dist && touch frontend/dist/tmp && go test ./... 20 | - name: Docker build 21 | if: github.actor != 'dependabot[bot]' 22 | uses: mr-smithers-excellent/docker-build-push@v6 23 | id: build 24 | with: 25 | image: ${{ env.IMAGENAME }} 26 | registry: ${{ env.REGISTRY }} 27 | multiPlatform: true 28 | platform: linux/amd64,linux/arm64 29 | username: ${{ github.repository_owner }} 30 | password: ${{ secrets.GITHUB_TOKEN }} 31 | addLatest: ${{ startsWith(github.ref, 'refs/tags/v') }} 32 | buildArgs: TAG=${{ env.TAG }} 33 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yaml: -------------------------------------------------------------------------------- 1 | name: Create release 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | repo-token: 7 | required: true 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - name: Download server archives 14 | uses: actions/download-artifact@v4 15 | with: 16 | pattern: albyhub-Server-* 17 | path: artifacts 18 | merge-multiple: true 19 | 20 | - name: Download desktop archives 21 | uses: actions/download-artifact@v4 22 | with: 23 | pattern: albyhub-Desktop-* 24 | path: artifacts 25 | merge-multiple: true 26 | 27 | - name: Create release without tag 28 | if: github.ref_type != 'tag' 29 | env: 30 | GH_TOKEN: ${{ secrets.repo-token }} 31 | tag: ${{ github.sha }} 32 | run: | 33 | echo "Release without tag not supported" 34 | exit 1 35 | 36 | - name: Create release with tag 37 | if: github.ref_type == 'tag' 38 | env: 39 | GH_TOKEN: ${{ secrets.repo-token }} 40 | tag: ${{ github.ref_name }} 41 | run: | 42 | gh release create ${{ env.tag }} \ 43 | --repo="$GITHUB_REPOSITORY" \ 44 | --title="${GITHUB_REPOSITORY#*/} ${tag#v}" \ 45 | --generate-notes \ 46 | --draft \ 47 | --verify-tag \ 48 | ./artifacts/* 49 | -------------------------------------------------------------------------------- /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | name: Code quality - linting and typechecking 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [opened, synchronize] 9 | 10 | jobs: 11 | linting: 12 | runs-on: ubuntu-22.04 13 | defaults: 14 | run: 15 | working-directory: ./frontend 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 20.x 22 | cache: "yarn" 23 | cache-dependency-path: frontend/yarn.lock 24 | 25 | - run: yarn install 26 | - run: yarn prepare:http 27 | 28 | - name: Linting 29 | run: yarn lint:js 30 | 31 | - name: Prettier 32 | run: yarn format 33 | 34 | - name: Typechecking 35 | run: yarn tsc:compile 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build-server: 8 | uses: ./.github/workflows/http.yml 9 | with: 10 | build-release: true 11 | secrets: 12 | APPLE_DEVELOPER_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }} 13 | APPLE_DEVELOPER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }} 14 | APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} 15 | APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} 16 | APPLE_USERNAME: ${{ secrets.APPLE_USERNAME }} 17 | 18 | build-wails: 19 | uses: ./.github/workflows/wails.yml 20 | with: 21 | build-release: true 22 | secrets: 23 | APPLE_DEVELOPER_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }} 24 | APPLE_DEVELOPER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }} 25 | APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} 26 | APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} 27 | APPLE_USERNAME: ${{ secrets.APPLE_USERNAME }} 28 | 29 | release-draft: 30 | needs: 31 | - build-server 32 | - build-wails 33 | uses: ./.github/workflows/create-release.yaml 34 | secrets: 35 | repo-token: ${{ secrets.GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/test-postgres.yml: -------------------------------------------------------------------------------- 1 | name: Backend testing with Postgres 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [opened, synchronize] 9 | 10 | jobs: 11 | test-postgres: 12 | runs-on: ubuntu-22.04 13 | 14 | services: 15 | postgres: 16 | image: postgres:17.2 17 | ports: 18 | - 5432:5432 19 | env: 20 | POSTGRES_DB: albyhub 21 | POSTGRES_USER: alby 22 | POSTGRES_PASSWORD: albytest123 23 | options: >- 24 | --health-cmd pg_isready 25 | --health-interval 10s 26 | --health-timeout 5s 27 | --health-retries 5 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | name: Check out code 32 | 33 | - name: Setup GoLang 34 | uses: actions/setup-go@v5 35 | with: 36 | go-version-file: "./go.mod" 37 | 38 | - name: Get dependencies 39 | run: go get -v -t -d ./... 40 | 41 | - name: Run tests 42 | env: 43 | TEST_DATABASE_URI: "postgresql://alby:albytest123@localhost:5432/albyhub" 44 | TEST_DB_MIGRATE_POSTGRES_URI: "postgresql://alby:albytest123@localhost:5432/albyhub" 45 | run: mkdir frontend/dist && touch frontend/dist/tmp && go test ./... 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | .env.local 4 | *.db 5 | *.macaroon 6 | node_modules 7 | nostr-wallet-connect 8 | nwc.db 9 | .breez 10 | .data 11 | .idea 12 | 13 | frontend/dist 14 | frontend/node_modules 15 | frontend/wailsjs 16 | package.json.md5 17 | 18 | build/bin 19 | 20 | *.log 21 | 22 | # generated by platform-specific files 23 | frontend/src/utils/request.ts 24 | frontend/src/utils/openLink.ts 25 | frontend/.yarn 26 | 27 | # generated by rust go bindings for local development 28 | glalby 29 | 30 | *.db-shm 31 | *.db-wal 32 | *.db-journal 33 | albyhub-data -------------------------------------------------------------------------------- /.mockery.yaml: -------------------------------------------------------------------------------- 1 | filename: "{{.InterfaceName}}.go" 2 | dir: tests/mocks 3 | pkgname: mocks 4 | template: testify 5 | packages: 6 | github.com/getAlby/hub/config: 7 | interfaces: 8 | Config: {} 9 | github.com/getAlby/hub/lnclient: 10 | interfaces: 11 | LNClient: {} 12 | github.com/getAlby/hub/service: 13 | interfaces: 14 | Service: {} 15 | github.com/getAlby/hub/service/keys: 16 | interfaces: 17 | Keys: {} 18 | github.com/getAlby/hub/alby: 19 | interfaces: 20 | AlbyService: {} 21 | AlbyOAuthService: {} 22 | github.com/getAlby/hub/events: 23 | interfaces: 24 | EventPublisher: {} 25 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | frontend/src/assets/lotties -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "wayou.vscode-todo-highlight", 6 | "golang.go", 7 | "bradlc.vscode-tailwindcss" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "[go]": { 4 | "editor.defaultFormatter": "golang.go" 5 | }, 6 | "editor.formatOnSave": true, 7 | "typescript.preferences.importModuleSpecifier": "non-relative", 8 | "typescript.preferences.autoImportFileExcludePatterns": ["@radix-ui"], 9 | "editor.codeActionsOnSave": { 10 | "source.organizeImports": "always" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bin/main -------------------------------------------------------------------------------- /api/apps_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/getAlby/hub/constants" 7 | "github.com/getAlby/hub/tests/mocks" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestCreateApp_SuperuserScopeIncorrectPassword(t *testing.T) { 13 | cfg := mocks.NewMockConfig(t) 14 | cfg.On("CheckUnlockPassword", "").Return(false) 15 | theAPI := &api{svc: mocks.NewMockService(t), cfg: cfg} 16 | response, err := theAPI.CreateApp(&CreateAppRequest{ 17 | Scopes: []string{constants.SUPERUSER_SCOPE}, 18 | }) 19 | 20 | assert.Nil(t, response) 21 | require.Error(t, err) 22 | assert.Equal(t, "incorrect unlock password to create app with superuser permission", err.Error()) 23 | } 24 | -------------------------------------------------------------------------------- /appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/appicon.png -------------------------------------------------------------------------------- /build/darwin/dmgcover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/build/darwin/dmgcover.png -------------------------------------------------------------------------------- /build/darwin/entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | com.apple.security.network.server 10 | 11 | com.apple.security.files.user-selected.read-write 12 | 13 | com.apple.security.files.downloads.read-write 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /build/darwin/gon-sign.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": [ 3 | "./build/bin/AlbyHub.app", 4 | "./build/bin/AlbyHub.app/Contents/MacOS/AlbyHub" 5 | ], 6 | "bundle_id": "com.getalby.AlbyHub", 7 | "sign": { 8 | "application_identity": "Developer ID Application: Alby Inc.", 9 | "entitlements_file": "./build/darwin/entitlements.plist" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /build/darwin/http/gon-notarize.json: -------------------------------------------------------------------------------- 1 | { 2 | "notarize": [ 3 | { 4 | "path": "./build/out/albyhub-Server-MacOS.zip", 5 | "bundle_id": "com.getalby.AlbyHub" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /build/docker/copy_dylibs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ARCH="$1" 4 | 5 | if [[ "$ARCH" == "amd64" ]]; then 6 | cp "$(go list -m -f "{{.Dir}}" github.com/getAlby/ldk-node-go)"/ldk_node/x86_64-unknown-linux-gnu/libldk_node.so ./ 7 | elif [[ "$ARCH" == "arm64" ]]; then 8 | cp "$(go list -m -f "{{.Dir}}" github.com/getAlby/ldk-node-go)"/ldk_node/aarch64-unknown-linux-gnu/libldk_node.so ./ 9 | else 10 | echo "Invalid ARCH value" 11 | exit 1 12 | fi 13 | -------------------------------------------------------------------------------- /db/migrations/202401191539_initial_migration.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | "text/template" 6 | 7 | "github.com/go-gormigrate/gormigrate/v2" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | //go:embed 202401191539_initial_migration.sql.tmpl 12 | var initialMigration string 13 | 14 | var initialMigrationTmpl = template.Must(template.New("initial_migration").Parse(initialMigration)) 15 | 16 | // Initial migration 17 | var _202401191539_initial_migration = &gormigrate.Migration{ 18 | ID: "202401191539_initial_migration", 19 | Migrate: func(tx *gorm.DB) error { 20 | return exec(tx, initialMigrationTmpl) 21 | }, 22 | Rollback: func(tx *gorm.DB) error { 23 | return nil 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /db/migrations/202403171120_delete_ldk_payments.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/go-gormigrate/gormigrate/v2" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | // TODO: delete the whole migration 11 | // Delete LDK payments that were not migrated to the new LDK format (PaymentKind) 12 | // this has now been removed as it only affects old test builds that should have been updated by now 13 | // in case someone encounters it, they can run the commands on their DB to fix the issue. 14 | var _202403171120_delete_ldk_payments = &gormigrate.Migration{ 15 | ID: "202403171120_delete_ldk_payments", 16 | Migrate: func(tx *gorm.DB) error { 17 | 18 | /*ldkDbPath := filepath.Join(appConfig.Workdir, "ldk", "storage", "ldk_node_data.sqlite") 19 | if _, err := os.Stat(ldkDbPath); errors.Is(err, os.ErrNotExist) { 20 | logger.Logger.Info("No LDK database, skipping migration") 21 | return nil 22 | } 23 | ldkDb, err := sql.Open("sqlite", ldkDbPath) 24 | if err != nil { 25 | return err 26 | } 27 | result, err := ldkDb.Exec(`update ldk_node_data set primary_namespace="payments_bkp" where primary_namespace == "payments"`) 28 | if err != nil { 29 | return err 30 | } 31 | rowsAffected, err := result.RowsAffected() 32 | if err != nil { 33 | return err 34 | } 35 | logger.Logger.WithFields(logrus.Fields{ 36 | "rowsAffected": rowsAffected, 37 | }).Info("Removed incompatible payments from LDK database") 38 | 39 | return err*/ 40 | return nil 41 | }, 42 | Rollback: func(tx *gorm.DB) error { 43 | return nil 44 | }, 45 | } 46 | -------------------------------------------------------------------------------- /db/migrations/202404021909_nullable_expires_at.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/go-gormigrate/gormigrate/v2" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | // Delete LDK payments that were not migrated to the new LDK format (PaymentKind) 11 | // TODO: delete this sometime in the future (only affects current testers) 12 | var _202404021909_nullable_expires_at = &gormigrate.Migration{ 13 | ID: "202404021909_nullable_expires_at", 14 | Migrate: func(tx *gorm.DB) error { 15 | 16 | err := tx.Exec(`update app_permissions set expires_at = NULL where expires_at = '0001-01-01 00:00:00+00:00'`).Error 17 | 18 | return err 19 | }, 20 | Rollback: func(tx *gorm.DB) error { 21 | return nil 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /db/migrations/202405302121_store_decrypted_request.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/go-gormigrate/gormigrate/v2" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | var _202405302121_store_decrypted_request = &gormigrate.Migration{ 11 | ID: "_202405302121_store_decrypted_request", 12 | Migrate: func(tx *gorm.DB) error { 13 | 14 | err := tx.Exec(`ALTER TABLE request_events ADD COLUMN method TEXT;`).Error 15 | if err != nil { 16 | return err 17 | } 18 | 19 | err = tx.Exec(`CREATE INDEX "idx_request_events_method" ON "request_events"("method");`).Error 20 | if err != nil { 21 | return err 22 | } 23 | 24 | err = tx.Exec(`ALTER TABLE request_events ADD COLUMN content_data TEXT`).Error 25 | return err 26 | }, 27 | Rollback: func(tx *gorm.DB) error { 28 | return nil 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /db/migrations/202406061259_delete_content.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/go-gormigrate/gormigrate/v2" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | // Delete the content column from both request and response events table 11 | var _202406061259_delete_content = &gormigrate.Migration{ 12 | ID: "202406061259_remove_content_columns", 13 | Migrate: func(tx *gorm.DB) error { 14 | if err := tx.Exec("ALTER TABLE response_events DROP COLUMN content").Error; err != nil { 15 | return err 16 | } 17 | 18 | if err := tx.Exec("ALTER TABLE request_events DROP COLUMN content").Error; err != nil { 19 | return err 20 | } 21 | 22 | // Disabled for now: not used. 23 | // Cannot run when testing with txdb: VACUUM must be run outside of transaction. 24 | // if !testing.Testing() { 25 | // if err := tx.Exec("VACUUM").Error; err != nil { 26 | // return err 27 | // } 28 | // } 29 | 30 | return nil 31 | }, 32 | Rollback: func(tx *gorm.DB) error { 33 | return nil 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /db/migrations/202406071726_vacuum.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/go-gormigrate/gormigrate/v2" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | // VACUUM to finish the update the vacuum mode to auto_vacuum 11 | // See https://sqlite.org/pragma.html 12 | // "The database connection can be changed between full and incremental autovacuum mode at any time. 13 | // However, changing from "none" to "full" or "incremental" can only occur when the database is new 14 | // (no tables have yet been created) or by running the VACUUM command." 15 | var _202406071726_vacuum = &gormigrate.Migration{ 16 | ID: "202406071726_vacuum", 17 | Migrate: func(tx *gorm.DB) error { 18 | // Disabled for now: not used. 19 | // Cannot run when testing with txdb: VACUUM must be run outside of transaction. 20 | // if !testing.Testing() { 21 | // if err := tx.Exec("VACUUM").Error; err != nil { 22 | // return err 23 | // } 24 | // } 25 | 26 | return nil 27 | }, 28 | Rollback: func(tx *gorm.DB) error { 29 | return nil 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /db/migrations/202406301207_rename_request_methods.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/go-gormigrate/gormigrate/v2" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | var _202406301207_rename_request_methods = &gormigrate.Migration{ 11 | ID: "202406301207_rename_request_methods", 12 | Migrate: func(tx *gorm.DB) error { 13 | if err := tx.Exec("ALTER TABLE app_permissions RENAME request_method TO scope").Error; err != nil { 14 | return err 15 | } 16 | 17 | return nil 18 | }, 19 | Rollback: func(tx *gorm.DB) error { 20 | return nil 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /db/migrations/202407262257_remove_invalid_scopes.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/go-gormigrate/gormigrate/v2" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | // This migration removes old app permissions for request methods (now we use scopes) 11 | var _202407262257_remove_invalid_scopes = &gormigrate.Migration{ 12 | ID: "202407262257_remove_invalid_scopes", 13 | Migrate: func(tx *gorm.DB) error { 14 | 15 | if err := tx.Exec(` 16 | delete from app_permissions where scope = 'pay_keysend'; 17 | delete from app_permissions where scope = 'multi_pay_keysend'; 18 | delete from app_permissions where scope = 'multi_pay_invoice'; 19 | `).Error; err != nil { 20 | return err 21 | } 22 | 23 | return nil 24 | }, 25 | Rollback: func(tx *gorm.DB) error { 26 | return nil 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /db/migrations/202408061737_add_boostagrams_and_use_json.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/go-gormigrate/gormigrate/v2" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | // This migration adds boostagram column to transactions 11 | var _202408061737_add_boostagrams_and_use_json = &gormigrate.Migration{ 12 | ID: "202408061737_add_boostagrams_and_use_json", 13 | Migrate: func(db *gorm.DB) error { 14 | err := db.Transaction(func(tx *gorm.DB) error { 15 | return tx.Exec(` 16 | ALTER TABLE transactions ADD COLUMN boostagram JSON; 17 | ALTER TABLE transactions ADD COLUMN metadata_temp JSON; 18 | UPDATE transactions SET metadata_temp = json(metadata) where metadata != ''; 19 | ALTER TABLE transactions DROP COLUMN metadata; 20 | ALTER TABLE transactions RENAME COLUMN metadata_temp TO metadata; 21 | `).Error 22 | }) 23 | 24 | return err 25 | }, 26 | Rollback: func(tx *gorm.DB) error { 27 | return nil 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /db/migrations/202408191242_transaction_failure_reason.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/go-gormigrate/gormigrate/v2" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | // This migration removes old app permissions for request methods (now we use scopes) 11 | var _202408191242_transaction_failure_reason = &gormigrate.Migration{ 12 | ID: "202408191242_transaction_failure_reason", 13 | Migrate: func(tx *gorm.DB) error { 14 | 15 | if err := tx.Exec(` 16 | ALTER TABLE transactions ADD failure_reason text; 17 | `).Error; err != nil { 18 | return err 19 | } 20 | 21 | return nil 22 | }, 23 | Rollback: func(tx *gorm.DB) error { 24 | return nil 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /db/migrations/202408291715_app_metadata.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/go-gormigrate/gormigrate/v2" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | var _202408291715_app_metadata = &gormigrate.Migration{ 11 | ID: "202408291715_app_metadata", 12 | Migrate: func(tx *gorm.DB) error { 13 | 14 | if err := tx.Exec(` 15 | ALTER TABLE apps ADD COLUMN metadata JSON; 16 | `).Error; err != nil { 17 | return err 18 | } 19 | 20 | return nil 21 | }, 22 | Rollback: func(tx *gorm.DB) error { 23 | return nil 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /db/migrations/202410141503_add_wallet_pubkey.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/go-gormigrate/gormigrate/v2" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | var _202410141503_add_wallet_pubkey = &gormigrate.Migration{ 11 | ID: "202410141503_add_wallet_pubkey", 12 | Migrate: func(tx *gorm.DB) error { 13 | 14 | if err := tx.Exec(` 15 | ALTER TABLE apps ADD COLUMN wallet_pubkey TEXT; 16 | ALTER TABLE apps RENAME COLUMN nostr_pubkey TO app_pubkey; 17 | `).Error; err != nil { 18 | return err 19 | } 20 | 21 | return nil 22 | }, 23 | Rollback: func(tx *gorm.DB) error { 24 | return nil 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /db/migrations/202412212345_fix_types.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "github.com/go-gormigrate/gormigrate/v2" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | // This migration fixes column types for Postgres compatibility. 9 | // 10 | // First, an autoincrement sequence is created for user_configs.id (this works 11 | // in sqlite, because an integer primary key becomes "autoincrement" 12 | // automatically). 13 | // 14 | // Second, user_configs.encrypted is converted into a boolean; otherwise, 15 | // it fails to de/serialize from/to the Go model's `Encrypted bool` field. 16 | // Again, this happens to work in sqlite due to the way booleans are handled 17 | // (they're just an alias for numeric). 18 | var _202412212345_fix_types = &gormigrate.Migration{ 19 | ID: "20241221234500_fix_types", 20 | Migrate: func(tx *gorm.DB) error { 21 | if tx.Dialector.Name() == "postgres" { 22 | err := tx.Exec(` 23 | CREATE SEQUENCE user_configs_id_seq; 24 | ALTER TABLE user_configs ALTER COLUMN id SET DEFAULT nextval('user_configs_id_seq'); 25 | ALTER SEQUENCE user_configs_id_seq OWNED BY user_configs.id; 26 | ALTER TABLE user_configs ALTER COLUMN encrypted SET DATA TYPE BOOLEAN USING encrypted::integer::boolean; 27 | `).Error 28 | if err != nil { 29 | return err 30 | } 31 | } 32 | return nil 33 | }, 34 | Rollback: func(tx *gorm.DB) error { 35 | return nil 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /db/migrations/202504231037_add_indexes.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/go-gormigrate/gormigrate/v2" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | var _202504231037_add_indexes = &gormigrate.Migration{ 11 | ID: "202504231037_add_indexes", 12 | Migrate: func(db *gorm.DB) error { 13 | 14 | if err := db.Transaction(func(tx *gorm.DB) error { 15 | 16 | if err := tx.Exec(` 17 | DROP INDEX IF EXISTS idx_transactions_request_event_id; 18 | DROP INDEX IF EXISTS idx_transactions_created_at; 19 | DROP INDEX IF EXISTS idx_transactions_settled_at; 20 | DROP INDEX IF EXISTS idx_transactions_app_id_type_state_created_at_settled_at_payment_hash; 21 | `).Error; err != nil { 22 | return err 23 | } 24 | 25 | if err := tx.Exec(` 26 | CREATE INDEX idx_transactions_state_type ON transactions(state, type); 27 | CREATE INDEX idx_transactions_state_type_updated_at ON transactions(state, type, updated_at); 28 | CREATE INDEX idx_transactions_app_id_state_type_updated_at ON transactions(app_id, state, type, updated_at); 29 | CREATE INDEX idx_transactions_payment_hash_settled_at_created_at ON transactions(payment_hash, settled_at, created_at); 30 | `).Error; err != nil { 31 | return err 32 | } 33 | 34 | return nil 35 | }); err != nil { 36 | return err 37 | } 38 | 39 | return nil 40 | }, 41 | Rollback: func(tx *gorm.DB) error { 42 | return nil 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /db/migrations/202504231037_hold_invoices.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/go-gormigrate/gormigrate/v2" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | var _202505091314_hold_invoices = &gormigrate.Migration{ 11 | ID: "202505091314_hold_invoices", 12 | Migrate: func(db *gorm.DB) error { 13 | 14 | if err := db.Exec(` 15 | ALTER TABLE transactions ADD COLUMN hold BOOLEAN; 16 | ALTER TABLE transactions ADD COLUMN settle_deadline integer; 17 | `).Error; err != nil { 18 | return err 19 | } 20 | 21 | return nil 22 | }, 23 | Rollback: func(tx *gorm.DB) error { 24 | return nil 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /db/migrations/202506170342_swaps.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | "text/template" 6 | 7 | "github.com/go-gormigrate/gormigrate/v2" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | const swapsMigration = ` 12 | CREATE TABLE swaps( 13 | id {{ .AutoincrementPrimaryKey }}, 14 | swap_id text UNIQUE, 15 | type text, 16 | state text, 17 | invoice text, 18 | send_amount integer, 19 | receive_amount integer, 20 | destination_address text, 21 | refund_address text, 22 | lockup_address text, 23 | boltz_pubkey text, 24 | preimage text, 25 | payment_hash text, 26 | lockup_tx_id text, 27 | claim_tx_id text, 28 | auto_swap boolean, 29 | timeout_block_height integer, 30 | swap_tree json, 31 | created_at {{ .Timestamp }}, 32 | updated_at {{ .Timestamp }} 33 | ); 34 | ` 35 | 36 | var swapsMigrationTmpl = template.Must(template.New("swapsMigration").Parse(swapsMigration)) 37 | 38 | var _202506170342_swaps = &gormigrate.Migration{ 39 | ID: "202506170342_swaps", 40 | Migrate: func(tx *gorm.DB) error { 41 | 42 | if err := exec(tx, swapsMigrationTmpl); err != nil { 43 | return err 44 | } 45 | 46 | return nil 47 | }, 48 | Rollback: func(tx *gorm.DB) error { 49 | return nil 50 | }, 51 | } 52 | -------------------------------------------------------------------------------- /db/migrations/202508041712_delete_non_cascade_deleted_records.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/go-gormigrate/gormigrate/v2" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | // NOTE: This was actually 2025-07-08 11 | var _202508041712_delete_non_cascade_deleted_records = &gormigrate.Migration{ 12 | ID: "202508041712_delete_non_cascade_deleted_records", 13 | Migrate: func(db *gorm.DB) error { 14 | 15 | // the following tables have ON DELETE CASCADE 16 | // which was not being applied due to PRAGMA foreign_keys = ON; 17 | // not applying to all DB connections: 18 | // - app_permissions -> apps 19 | // - request_events -> apps 20 | // - response_events -> request_events 21 | // however we will only delete app permissions. 22 | // excess request and response events will be removed in batches by a background task. 23 | 24 | if err := db.Exec(`DELETE FROM app_permissions 25 | WHERE app_id NOT IN (SELECT id FROM apps)`).Error; err != nil { 26 | return err 27 | } 28 | 29 | return nil 30 | }, 31 | Rollback: func(tx *gorm.DB) error { 32 | return nil 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /db/migrations/202508041737_postgres_amount_bigint.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/go-gormigrate/gormigrate/v2" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | // NOTE: This was actually 2025-07-08 11 | var _202508041737_postgres_amount_bigint = &gormigrate.Migration{ 12 | ID: "202508041737_postgres_amount_bigint", 13 | Migrate: func(db *gorm.DB) error { 14 | 15 | // sqlite works fine but postgres integers are only 4 bytes 16 | // amounts are in msats (= max ~2.1M sats) 17 | if db.Dialector.Name() != "postgres" { 18 | return nil 19 | } 20 | 21 | if err := db.Exec(`ALTER TABLE transactions 22 | ALTER COLUMN amount_msat TYPE bigint;`).Error; err != nil { 23 | return err 24 | } 25 | 26 | return nil 27 | }, 28 | Rollback: func(tx *gorm.DB) error { 29 | return nil 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /db/migrations/202508041738_app_last_used.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | "text/template" 6 | 7 | "github.com/go-gormigrate/gormigrate/v2" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | const appLastUsedMigration = `ALTER TABLE apps ADD COLUMN last_used_at {{ .Timestamp }};` 12 | 13 | var appLastUsedMigrationTmpl = template.Must(template.New("appLastUsedMigration").Parse(appLastUsedMigration)) 14 | 15 | // NOTE: This was actually 2025-07-14 16 | var _202508041738_app_last_used = &gormigrate.Migration{ 17 | ID: "202508041738_app_last_used", 18 | Migrate: func(tx *gorm.DB) error { 19 | 20 | err := exec(tx, appLastUsedMigrationTmpl) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | return nil 26 | }, 27 | Rollback: func(tx *gorm.DB) error { 28 | return nil 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /db/migrations/202508041739_response_events_index.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/go-gormigrate/gormigrate/v2" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | // NOTE: This was actually 2025-07-21 11 | var _202508041739_response_events_index = &gormigrate.Migration{ 12 | ID: "202508041739_response_events_index", 13 | Migrate: func(tx *gorm.DB) error { 14 | 15 | err := tx.Exec("CREATE INDEX idx_response_events_request_id ON response_events(request_id);").Error 16 | if err != nil { 17 | return err 18 | } 19 | 20 | return nil 21 | }, 22 | Rollback: func(tx *gorm.DB) error { 23 | return nil 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /db/migrations/202508151405_swap_xpub.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/go-gormigrate/gormigrate/v2" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | // NOTE: This was actually 2025-07-21 11 | var _202508151405_swap_xpub = &gormigrate.Migration{ 12 | ID: "202508151405_swap_xpub", 13 | Migrate: func(tx *gorm.DB) error { 14 | 15 | err := tx.Exec("ALTER TABLE swaps ADD COLUMN used_xpub BOOLEAN;").Error 16 | if err != nil { 17 | return err 18 | } 19 | 20 | return nil 21 | }, 22 | Rollback: func(tx *gorm.DB) error { 23 | return nil 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /db/migrations/202508192137_forwards.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | "text/template" 6 | 7 | "github.com/go-gormigrate/gormigrate/v2" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | const forwardsMigration = ` 12 | CREATE TABLE forwards( 13 | id {{ .AutoincrementPrimaryKey }}, 14 | outbound_amount_forwarded_msat bigint, 15 | total_fee_earned_msat bigint, 16 | created_at {{ .Timestamp }}, 17 | updated_at {{ .Timestamp }} 18 | ); 19 | ` 20 | 21 | var forwardsMigrationTmpl = template.Must(template.New("forwardsMigration").Parse(forwardsMigration)) 22 | 23 | var _202508192137_forwards = &gormigrate.Migration{ 24 | ID: "202508192137_forwards", 25 | Migrate: func(tx *gorm.DB) error { 26 | 27 | if err := exec(tx, forwardsMigrationTmpl); err != nil { 28 | return err 29 | } 30 | 31 | return nil 32 | }, 33 | Rollback: func(tx *gorm.DB) error { 34 | return nil 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /db/migrations/202509031250_transactions_updated_at_index.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/go-gormigrate/gormigrate/v2" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | // speeds up this query: 11 | // SELECT * FROM transactions WHERE state = 'SETTLED' OR type = 'outgoing' ORDER BY updated_at DESC LIMIT 20; 12 | var _202509031250_transactions_updated_at_index = &gormigrate.Migration{ 13 | ID: "202509031250_transactions_updated_at_index", 14 | Migrate: func(tx *gorm.DB) error { 15 | 16 | err := tx.Exec("CREATE INDEX IF NOT EXISTS idx_transactions_updated_at ON transactions(updated_at);").Error 17 | if err != nil { 18 | return err 19 | } 20 | 21 | return nil 22 | }, 23 | Rollback: func(tx *gorm.DB) error { 24 | return nil 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /db/migrations/README.md: -------------------------------------------------------------------------------- 1 | # Creating a new migration 2 | 3 | 1. Create a new file based on the current date and time (see existing migration format) 4 | 2. Copy the following code and update MY_ID_HERE and MY_COMMENT_HERE and DO_SOMETHING_HERE 5 | 3. Add the ID to the list of migrations in migrate.go 6 | 7 | ```go 8 | package migrations 9 | 10 | import ( 11 | "github.com/go-gormigrate/gormigrate/v2" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | // MY_COMMENT_HERE 16 | var _MY_ID_HERE = &gormigrate.Migration { 17 | ID: "MY_ID_HERE", 18 | Migrate: func(tx *gorm.DB) error { 19 | return DO_SOMETHING_HERE.Error; 20 | }, 21 | Rollback: func(tx *gorm.DB) error { 22 | return nil; 23 | }, 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /db/queries/get_isolated_balance.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "github.com/getAlby/hub/constants" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | func GetIsolatedBalance(tx *gorm.DB, appId uint) int64 { 9 | var received struct { 10 | Sum int64 11 | } 12 | tx. 13 | Table("transactions"). 14 | Select("SUM(amount_msat) as sum"). 15 | Where("app_id = ? AND type = ? AND state = ?", appId, constants.TRANSACTION_TYPE_INCOMING, constants.TRANSACTION_STATE_SETTLED).Scan(&received) 16 | 17 | var spent struct { 18 | Sum int64 19 | } 20 | 21 | tx. 22 | Table("transactions"). 23 | Select("SUM(amount_msat + fee_msat + fee_reserve_msat) as sum"). 24 | Where("app_id = ? AND type = ? AND (state = ? OR state = ?)", appId, constants.TRANSACTION_TYPE_OUTGOING, constants.TRANSACTION_STATE_SETTLED, constants.TRANSACTION_STATE_PENDING).Scan(&spent) 25 | 26 | return received.Sum - spent.Sum 27 | } 28 | -------------------------------------------------------------------------------- /db/sqlite-wrapper/driver.go: -------------------------------------------------------------------------------- 1 | package sqlite_wrapper 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "github.com/mattn/go-sqlite3" 7 | ) 8 | 9 | const Sqlite3WrapperDriverName = "sqlite3_wrapper" 10 | 11 | func init() { 12 | // We need to set the temp_store setting on every connection, including 13 | // those that are implicitly opened by Go's database/sql package. 14 | // Unfortunately, this setting cannot be provided in the DSN; therefore 15 | // we execute the PRAGMA statement in the sqlite3's connection hook. 16 | sql.Register(Sqlite3WrapperDriverName, &sqlite3.SQLiteDriver{ 17 | ConnectHook: func(conn *sqlite3.SQLiteConn) error { 18 | _, err := conn.Exec("PRAGMA temp_store = MEMORY", nil) 19 | return err 20 | }, 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /db/test/db_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/sirupsen/logrus" 8 | 9 | "github.com/getAlby/hub/logger" 10 | "github.com/getAlby/hub/tests/db" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestTempStorePragmaIsApplied(t *testing.T) { 17 | logger.Init(strconv.Itoa(int(logrus.DebugLevel))) 18 | gormDb, err := db.NewDB(t) 19 | require.NoError(t, err) 20 | defer db.CloseDB(gormDb) 21 | 22 | if gormDb.Dialector.Name() != "sqlite" { 23 | t.Skip("Skipping non-sqlite dialector") 24 | } 25 | 26 | var result string 27 | err = gormDb.Raw("PRAGMA temp_store").Scan(&result).Error 28 | require.NoError(t, err) 29 | 30 | // PRAGMA temp_store = MEMORY 31 | // MEMORY = 2 32 | assert.Equal(t, "2", result) 33 | } 34 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | albyhub: 3 | # you can manually specify the platform if you use something other than linux/amd64,linux/arm64 e.g. 4 | #platform: linux/arm64/v8 5 | container_name: albyhub 6 | image: ghcr.io/getalby/hub:latest 7 | volumes: 8 | - ./albyhub-data:/data 9 | ports: 10 | - "8080:8080" 11 | environment: 12 | - WORK_DIR=/data/albyhub 13 | stop_grace_period: 300s 14 | -------------------------------------------------------------------------------- /events/models.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type EventSubscriber interface { 8 | ConsumeEvent(ctx context.Context, event *Event, globalProperties map[string]interface{}) 9 | } 10 | 11 | type EventPublisher interface { 12 | RegisterSubscriber(eventListener EventSubscriber) 13 | RemoveSubscriber(eventListener EventSubscriber) 14 | Publish(event *Event) 15 | PublishSync(event *Event) 16 | SetGlobalProperty(key string, value interface{}) 17 | } 18 | 19 | type Event struct { 20 | Event string `json:"event"` 21 | Properties interface{} `json:"properties,omitempty"` 22 | } 23 | 24 | type StaticChannelsBackupEvent struct { 25 | NodeID string `json:"node_id"` 26 | Channels []ChannelBackup `json:"channels"` 27 | Monitors []EncodedChannelMonitorBackup `json:"monitors"` 28 | } 29 | 30 | type EncodedChannelMonitorBackup struct { 31 | Key string `json:"key"` 32 | Value string `json:"value"` 33 | } 34 | 35 | type ChannelBackup struct { 36 | ChannelID string `json:"channel_id"` 37 | PeerID string `json:"peer_id"` 38 | PeerSocketAddress string `json:"peer_socket_address"` 39 | ChannelSize uint64 `json:"channel_size"` 40 | FundingTxID string `json:"funding_tx_id"` 41 | FundingTxVout uint32 `json:"funding_tx_vout"` 42 | } 43 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml app configuration file generated for nwc on 2024-01-30T12:30:13+01:00 2 | # 3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 | # 5 | 6 | app = 'nwc' 7 | primary_region = 'lax' 8 | swap_size_mb = 2048 9 | # Add a kill timeout longer than LDK node shutdown timeout to ensure Alby Hub gracefully shuts down 10 | kill_timeout = 300 11 | 12 | [build] 13 | image = 'ghcr.io/getalby/hub:latest' 14 | 15 | [env] 16 | DATABASE_URI = '/data/nwc.db' 17 | LDK_LOG_LEVEL = '3' 18 | LOG_LEVEL = '4' 19 | WORK_DIR = '/data' 20 | 21 | [[mounts]] 22 | source = 'nwc_data' 23 | destination = '/data' 24 | initial_size = '1' 25 | auto_extend_size_threshold = 80 26 | auto_extend_size_increment = "1GB" 27 | auto_extend_size_limit = "5GB" 28 | 29 | [http_service] 30 | internal_port = 8080 31 | force_https = true 32 | auto_stop_machines = false 33 | auto_start_machines = true 34 | min_machines_running = 0 35 | processes = ['app'] 36 | 37 | [[vm]] 38 | cpu_kind = 'shared' 39 | cpus = 1 40 | memory = '512mb' 41 | -------------------------------------------------------------------------------- /frontend/.env.local.example: -------------------------------------------------------------------------------- 1 | # set a custom messageboard wallet (should be a sub-wallet with only make invoice and list transactions permissions) 2 | #VITE_LIGHTNING_MESSAGEBOARD_NWC_URL="nostr+walletconnect://5f8e7c098137ccca853327be44a9b2e956cf79a8e2336e27a4f27b3fb55325b6?relay=wss://relay.getalby.com/v1&secret=ace5c4b9e08138a2ef91b4ccf1379952c77c651866b29f5872b5165134417894" -------------------------------------------------------------------------------- /frontend/.husky/commit-msg: -------------------------------------------------------------------------------- 1 | cd frontend && yarn commitlint --edit -------------------------------------------------------------------------------- /frontend/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | cd frontend && yarn lint-staged -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5" 3 | } 4 | -------------------------------------------------------------------------------- /frontend/commitlint.config.js: -------------------------------------------------------------------------------- 1 | export default { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "src/components", 15 | "utils": "src/lib/utils" 16 | }, 17 | "iconLibrary": "lucide" 18 | } 19 | -------------------------------------------------------------------------------- /frontend/frontend.go: -------------------------------------------------------------------------------- 1 | package frontend 2 | 3 | import ( 4 | "embed" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | "github.com/labstack/echo/v4/middleware" 9 | ) 10 | 11 | //go:embed dist 12 | var embeddedReactAssets embed.FS 13 | 14 | func RegisterHandlers(e *echo.Echo) { 15 | e.Use(middleware.StaticWithConfig(middleware.StaticConfig{ 16 | Skipper: nil, 17 | // Root directory from where the static content is served. 18 | Root: "dist", 19 | // Enable HTML5 mode by forwarding all not-found requests to root so that 20 | // SPA (single-page application) can handle the routing. 21 | HTML5: true, 22 | Browse: false, 23 | IgnoreBase: false, 24 | Filesystem: http.FS(embeddedReactAssets), 25 | })) 26 | } 27 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Alby Hub 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /frontend/lint-staged.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "src/**/*.{ts,tsx,json}": [ 3 | "eslint --fix --no-warn-ignored --max-warnings 0", 4 | "prettier --write", 5 | ], 6 | "platform_specific/**/*.ts": ["prettier --write"], 7 | "package.json": ["prettier --write"], 8 | "src/**/*.ts": () => "tsc --noEmit", 9 | }; 10 | -------------------------------------------------------------------------------- /frontend/platform_specific/http/src/utils/openLink.ts: -------------------------------------------------------------------------------- 1 | export const openLink = (url: string) => { 2 | // opens the link in a new tab 3 | window.setTimeout(() => window.open(url, "_blank")); 4 | }; 5 | -------------------------------------------------------------------------------- /frontend/platform_specific/http/src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import { getAuthToken } from "src/lib/auth"; 2 | import { ErrorResponse } from "src/types"; 3 | 4 | export const request = async ( 5 | ...args: Parameters 6 | ): Promise => { 7 | if (import.meta.env.BASE_URL !== "/") { 8 | // if running on a subpath, include the subpath in the request URL 9 | // BASE_URL is set via process.env.BASE_PATH, see https://vite.dev/guide/build#public-base-path 10 | args[0] = import.meta.env.BASE_URL + args[0]; 11 | } 12 | 13 | const token = getAuthToken(); 14 | if (token) { 15 | if (!args[1]) { 16 | args[1] = {}; 17 | } 18 | args[1].headers = { 19 | ...args[1].headers, 20 | Authorization: `Bearer ${token}`, 21 | }; 22 | } 23 | 24 | try { 25 | const fetchResponse = await fetch(...args); 26 | 27 | let body: T | undefined; 28 | if (fetchResponse.status !== 204) { 29 | try { 30 | body = await fetchResponse.json(); 31 | } catch (error) { 32 | console.error(error); 33 | } 34 | } 35 | 36 | if (!fetchResponse.ok) { 37 | throw new Error( 38 | fetchResponse.status + 39 | " " + 40 | ((body as ErrorResponse)?.message || "Unknown error") 41 | ); 42 | } 43 | return body; 44 | } catch (error) { 45 | console.error("Failed to fetch", error); 46 | throw error; 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /frontend/platform_specific/wails/src/utils/openLink.ts: -------------------------------------------------------------------------------- 1 | import { BrowserOpenURL } from "wailsjs/runtime/runtime"; 2 | 3 | export const openLink = (url: string) => { 4 | // opens the link in the browser 5 | try { 6 | BrowserOpenURL(url); 7 | } catch (error) { 8 | console.error("Failed to open link", error); 9 | throw error; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /frontend/platform_specific/wails/src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import { WailsRequestRouter } from "wailsjs/go/wails/WailsApp"; 2 | 3 | export const request = async ( 4 | ...args: Parameters 5 | ): Promise => { 6 | try { 7 | const res = await WailsRequestRouter( 8 | args[0].toString(), 9 | args[1]?.method || "GET", 10 | args[1]?.body?.toString() || "" 11 | ); 12 | 13 | console.info("Wails request", ...args, res); 14 | if (res.error) { 15 | throw new Error(res.error); 16 | } 17 | 18 | return res.body; 19 | } catch (error) { 20 | console.error("Failed to fetch", error); 21 | throw error; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/fonts/Inter-italic.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/public/fonts/Inter-italic.var.woff2 -------------------------------------------------------------------------------- /frontend/public/fonts/Inter-roman.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/public/fonts/Inter-roman.var.woff2 -------------------------------------------------------------------------------- /frontend/public/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/public/icon-1024.png -------------------------------------------------------------------------------- /frontend/public/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/public/icon-192.png -------------------------------------------------------------------------------- /frontend/public/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/public/icon-512.png -------------------------------------------------------------------------------- /frontend/public/images/illustrations/tick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | RouterProvider, 3 | createBrowserRouter, 4 | createHashRouter, 5 | } from "react-router-dom"; 6 | import { Toaster } from "src/components/ui/sonner"; 7 | import { ThemeProvider } from "src/components/ui/theme-provider"; 8 | import { TouchProvider } from "src/components/ui/tooltip"; 9 | import { useInfo } from "src/hooks/useInfo"; 10 | import routes from "src/routes.tsx"; 11 | import { isHttpMode } from "src/utils/isHttpMode"; 12 | 13 | const createRouterFunc = isHttpMode() ? createBrowserRouter : createHashRouter; 14 | const router = createRouterFunc(routes, { 15 | // if running on a subpath, use the subpath as the router basename 16 | // BASE_URL is set via process.env.BASE_PATH, see https://vite.dev/guide/build#public-base-path 17 | basename: 18 | import.meta.env.BASE_URL !== "/" ? import.meta.env.BASE_URL : undefined, 19 | }); 20 | 21 | function App() { 22 | const { data: info } = useInfo(); 23 | 24 | return ( 25 | <> 26 | 27 | 32 | {info && } 33 | 34 | 35 | 36 | 37 | ); 38 | } 39 | 40 | export default App; 41 | -------------------------------------------------------------------------------- /frontend/src/assets/images/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/images/cloud.png -------------------------------------------------------------------------------- /frontend/src/assets/images/cloud2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/images/cloud2.png -------------------------------------------------------------------------------- /frontend/src/assets/images/node/cashu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/images/node/cashu.png -------------------------------------------------------------------------------- /frontend/src/assets/images/node/lnd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/images/node/lnd.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/alby-go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/alby-go.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/alby-hub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/alby-hub.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/alby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/alby.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/amethyst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/amethyst.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/bitrefill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/bitrefill.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/bringin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/bringin.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/btcpay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/btcpay.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/buzzpay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/buzzpay.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/clams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/clams.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/claude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/claude.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/coracle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/coracle.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/damus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/damus.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/goose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/goose.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/habla-news.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/habla-news.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/iris.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/iris.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/kiwi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/kiwi.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/lightning-messageboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/lightning-messageboard.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/lnbits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/lnbits.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/lume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/lume.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/nakapay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/nakapay.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/nostrcheck-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/nostrcheck-server.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/nostrudel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/nostrudel.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/nostter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/nostter.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/nostur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/nostur.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/paper-scissors-hodl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/paper-scissors-hodl.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/primal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/primal.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/pullthatupjamie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/pullthatupjamie.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/simple-boost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/simple-boost.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/snort.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/snort.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/stacker-news.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/stacker-news.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/tictactoe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/tictactoe.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/uncle-jim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/uncle-jim.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/wave-space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/wave-space.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/wavlake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/wavlake.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/wherostr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/wherostr.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/yakihonne.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/yakihonne.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/zap-stream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/zap-stream.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/zapplanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/zapplanner.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/zapple-pay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/zapple-pay.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/zappy-bird.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/zappy-bird.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/zapstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/zapstore.png -------------------------------------------------------------------------------- /frontend/src/assets/suggested-apps/zeus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/suggested-apps/zeus.png -------------------------------------------------------------------------------- /frontend/src/assets/zapplanner/bitcoinbrink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/zapplanner/bitcoinbrink.png -------------------------------------------------------------------------------- /frontend/src/assets/zapplanner/hrf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/zapplanner/hrf.png -------------------------------------------------------------------------------- /frontend/src/assets/zapplanner/opensats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getAlby/hub/fd60afc349d06938e57cac5006618d5dcdc5432c/frontend/src/assets/zapplanner/opensats.png -------------------------------------------------------------------------------- /frontend/src/components/AnchorReserveAlert.tsx: -------------------------------------------------------------------------------- 1 | import { AlertTriangleIcon } from "lucide-react"; 2 | import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert"; 3 | import { useBalances } from "src/hooks/useBalances"; 4 | import { useChannels } from "src/hooks/useChannels"; 5 | 6 | export function AnchorReserveAlert({ 7 | amount, 8 | className, 9 | isSwap, 10 | }: { 11 | amount: number; 12 | isSwap?: boolean; 13 | className?: string; 14 | }) { 15 | const { data: balances } = useBalances(); 16 | const { data: channels } = useChannels(); 17 | 18 | if (!balances || !channels) { 19 | return null; 20 | } 21 | 22 | const showAlert = 23 | amount && 24 | !!channels.length && 25 | +amount > balances.onchain.spendable - channels.length * 25000; 26 | 27 | if (!showAlert) { 28 | return null; 29 | } 30 | 31 | return ( 32 | 33 | 34 | Channel Anchor Reserves will be depleted 35 | 36 | You have channels open and by spending your entire on-chain balance 37 | including your anchor reserves may put your node at risk of unable to 38 | reclaim funds in your channel after a force-closure. To prevent this, 39 | set aside at least{" "} 40 | {new Intl.NumberFormat().format(channels.length * 25000)} sats on-chain 41 | {isSwap ? ", or pay with an external on-chain wallet." : "."} 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/components/AppHeader.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | import { Separator } from "src/components/ui/separator"; 3 | import { SidebarTrigger } from "src/components/ui/sidebar"; 4 | 5 | type Props = { 6 | icon?: React.ReactElement; 7 | title: string | ReactElement; 8 | description?: string | ReactElement; 9 | contentRight?: React.ReactNode; 10 | breadcrumb?: boolean; 11 | addSidebarTrigger?: boolean; 12 | }; 13 | 14 | function AppHeader({ 15 | icon, 16 | title, 17 | description = "", 18 | contentRight, 19 | addSidebarTrigger = true, 20 | }: Props) { 21 | return ( 22 | <> 23 |
24 | {addSidebarTrigger && } 25 | 26 | {icon} 27 |
28 |
29 |
30 |

{title}

31 | {description && ( 32 |

33 | {description} 34 |

35 | )} 36 |
37 |
{contentRight}
38 |
39 |
40 |
41 | 42 | ); 43 | } 44 | 45 | export default AppHeader; 46 | -------------------------------------------------------------------------------- /frontend/src/components/CardButton.tsx: -------------------------------------------------------------------------------- 1 | import { ChevronRightIcon } from "lucide-react"; 2 | import { ReactElement } from "react"; 3 | import { Link } from "react-router-dom"; 4 | import { Card } from "src/components/ui/card"; 5 | 6 | type Props = { 7 | title: string | ReactElement; 8 | description: string | ReactElement; 9 | to: string; 10 | }; 11 | 12 | function CardButton({ title, description, to }: Props) { 13 | return ( 14 | 15 | 16 |
17 |
18 |
19 | {title} 20 |
21 |
{description}
22 |
23 |
24 | 25 |
26 |
27 |
28 | 29 | ); 30 | } 31 | 32 | export default CardButton; 33 | -------------------------------------------------------------------------------- /frontend/src/components/Container.tsx: -------------------------------------------------------------------------------- 1 | export default function Container({ children }: React.PropsWithChildren) { 2 | return ( 3 |
4 |
5 | {children} 6 |
7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/components/EmptyState.tsx: -------------------------------------------------------------------------------- 1 | import { LucideIcon } from "lucide-react"; 2 | import React from "react"; 3 | import { Link } from "react-router-dom"; 4 | import { Button } from "src/components/ui/button"; 5 | import { cn } from "src/lib/utils"; 6 | 7 | interface Props { 8 | icon: LucideIcon; 9 | title: string; 10 | description: string; 11 | buttonText: string; 12 | buttonLink: string; 13 | showButton?: boolean; 14 | showBorder?: boolean; 15 | } 16 | 17 | const EmptyState: React.FC = ({ 18 | icon: Icon, 19 | title: message, 20 | description: subMessage, 21 | buttonText, 22 | buttonLink, 23 | showButton = true, 24 | showBorder = true, 25 | }) => { 26 | return ( 27 |
33 |
34 | 35 |

{message}

36 |

{subMessage}

37 | {showButton && ( 38 | 39 | 40 | 41 | )} 42 |
43 |
44 | ); 45 | }; 46 | 47 | export default EmptyState; 48 | -------------------------------------------------------------------------------- /frontend/src/components/ExternalLink.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { isHttpMode } from "src/utils/isHttpMode"; 3 | import { openLink } from "src/utils/openLink"; 4 | 5 | type Props = { 6 | to: string; 7 | className?: string; 8 | children?: React.ReactNode; 9 | }; 10 | 11 | export default function ExternalLink({ to, className, children }: Props) { 12 | const _isHttpMode = isHttpMode(); 13 | 14 | return _isHttpMode ? ( 15 | 21 | {children} 22 | 23 | ) : ( 24 | openLink(to)}> 25 | {children} 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/components/FormattedFiatAmount.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "src/components/ui/skeleton"; 2 | import { useBitcoinRate } from "src/hooks/useBitcoinRate"; 3 | import { useInfo } from "src/hooks/useInfo"; 4 | import { cn } from "src/lib/utils"; 5 | 6 | type FormattedFiatAmountProps = { 7 | amount: number; 8 | className?: string; 9 | showApprox?: boolean; 10 | }; 11 | 12 | export default function FormattedFiatAmount({ 13 | amount, 14 | className, 15 | showApprox, 16 | }: FormattedFiatAmountProps) { 17 | const { data: info } = useInfo(); 18 | const { data: bitcoinRate, error: bitcoinRateError } = useBitcoinRate(); 19 | 20 | if (info?.currency === "SATS" || bitcoinRateError) { 21 | return null; 22 | } 23 | 24 | return ( 25 |
26 | {showApprox && bitcoinRate && "~"} 27 | {!bitcoinRate ? ( 28 |   29 | ) : ( 30 | new Intl.NumberFormat("en-US", { 31 | style: "currency", 32 | currency: info?.currency || "usd", 33 | }).format((amount / 100_000_000) * bitcoinRate.rate_float) 34 | )} 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2Icon, LoaderIcon } from "lucide-react"; 2 | import { cn } from "src/lib/utils"; 3 | 4 | function Loading({ 5 | className, 6 | variant = "loader2", 7 | }: { 8 | className?: string; 9 | variant?: "loader2" | "loader"; 10 | }) { 11 | const Component = variant === "loader2" ? Loader2Icon : LoaderIcon; 12 | 13 | return ( 14 | <> 15 | 22 | Loading... 23 | 24 | 25 | ); 26 | } 27 | 28 | export default Loading; 29 | -------------------------------------------------------------------------------- /frontend/src/components/LottieLoading.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import Lottie from "react-lottie"; 3 | import animationDataDark from "src/assets/lotties/loading-dark.json"; 4 | import animationDataLight from "src/assets/lotties/loading-light.json"; 5 | import { useTheme } from "src/components/ui/theme-provider"; 6 | 7 | export default function LottieLoading({ size }: { size?: number }) { 8 | const { isDarkMode } = useTheme(); 9 | 10 | const options = useMemo( 11 | () => ({ 12 | loop: true, 13 | autoplay: true, 14 | animationData: isDarkMode ? animationDataDark : animationDataLight, 15 | rendererSettings: { preserveAspectRatio: "xMidYMid slice" }, 16 | }), 17 | [isDarkMode] 18 | ); 19 | 20 | return ; 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/components/LowReceivingCapacityAlert.tsx: -------------------------------------------------------------------------------- 1 | import { AlertTriangleIcon } from "lucide-react"; 2 | import { Link } from "react-router-dom"; 3 | import { 4 | Alert, 5 | AlertDescription, 6 | AlertTitle, 7 | } from "src/components/ui/alert.tsx"; 8 | 9 | export default function LowReceivingCapacityAlert() { 10 | return ( 11 | 12 | 13 | Low receiving capacity 14 | 15 | You likely won't be able to receive payments until you{" "} 16 | 17 | spend 18 | 19 | ,{" "} 20 | 21 | swap out funds 22 | 23 | , or{" "} 24 | 25 | increase your receiving capacity. 26 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/components/OnchainAddressDisplay.tsx: -------------------------------------------------------------------------------- 1 | function OnchainAddressDisplay({ address }: { address: string }) { 2 | return ( 3 | <> 4 | {address.match(/.{1,4}/g)?.map((word, index) => { 5 | if (index % 2 === 0) { 6 | return ( 7 | 8 | {word} 9 | 10 | ); 11 | } else { 12 | return ( 13 | 14 | {word} 15 | 16 | ); 17 | } 18 | })} 19 | 20 | ); 21 | } 22 | 23 | export default OnchainAddressDisplay; 24 | -------------------------------------------------------------------------------- /frontend/src/components/QRCode.tsx: -------------------------------------------------------------------------------- 1 | import ReactQRCode from "react-qr-code"; 2 | import { cn } from "src/lib/utils"; 3 | 4 | export type Props = { 5 | value: string; 6 | size?: number; 7 | className?: string; 8 | 9 | // set the level to Q if there are overlays 10 | // Q will improve error correction (so we can add overlays covering up to 25% of the QR) 11 | // at the price of decreased information density (meaning the QR codes "pixels" have to be 12 | // smaller to encode the same information). 13 | // While that isn't that much of a problem for lightning addresses (because they are usually quite short), 14 | // for invoices that contain larger amount of data those QR codes can get "harder" to read. 15 | // (meaning you have to aim your phone very precisely and have to wait longer for the reader 16 | // to recognize the QR code) 17 | level?: "Q" | undefined; 18 | }; 19 | 20 | function QRCode({ value, size, level, className }: Props) { 21 | // Do not use dark mode: some apps do not handle it well (e.g. Phoenix) 22 | // const { isDarkMode } = useTheme(); 23 | const fgColor = "#242424"; // isDarkMode ? "#FFFFFF" : "#242424"; 24 | const bgColor = "#FFFFFF"; // isDarkMode ? "#242424" : "#FFFFFF"; 25 | 26 | return ( 27 |
28 | 36 |
37 | ); 38 | } 39 | 40 | export default QRCode; 41 | -------------------------------------------------------------------------------- /frontend/src/components/ResponsiveButton.tsx: -------------------------------------------------------------------------------- 1 | import { VariantProps } from "class-variance-authority"; 2 | import * as React from "react"; 3 | import { Button } from "./ui/button"; 4 | import { buttonVariants } from "./ui/buttonVariants"; 5 | 6 | type Props = { 7 | icon: React.ComponentType; 8 | text: string; 9 | } & React.ComponentProps<"button"> & 10 | VariantProps & { 11 | asChild?: boolean; 12 | }; 13 | 14 | const ResponsiveButton = ({ icon: Icon, text, variant, ...props }: Props) => { 15 | return ( 16 | <> 17 | 21 | 24 | 25 | ); 26 | }; 27 | 28 | export default ResponsiveButton; 29 | -------------------------------------------------------------------------------- /frontend/src/components/SettingsHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Separator } from "src/components/ui/separator"; 3 | 4 | type Props = { 5 | title: string; 6 | description: string | React.ReactNode; 7 | }; 8 | 9 | function SettingsHeader({ title, description }: Props) { 10 | return ( 11 | <> 12 |
13 |
14 |

{title}

15 |

{description}

16 |
17 | 18 |
19 | 20 | ); 21 | } 22 | 23 | export default SettingsHeader; 24 | -------------------------------------------------------------------------------- /frontend/src/components/TwoColumnLayoutHeader.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | title: string; 3 | description: string; 4 | }; 5 | 6 | export default function TwoColumnLayoutHeader({ title, description }: Props) { 7 | return ( 8 |
9 |

{title}

10 |

{description}

11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/components/UpgradeCard.tsx: -------------------------------------------------------------------------------- 1 | import { SparklesIcon } from "lucide-react"; 2 | import { Button } from "src/components/ui/button"; 3 | import { UpgradeDialog } from "src/components/UpgradeDialog"; 4 | 5 | interface Props { 6 | title: string; 7 | description: string; 8 | } 9 | 10 | const UpgradeCard: React.FC = ({ 11 | title: message, 12 | description: subMessage, 13 | }) => { 14 | return ( 15 |
16 |
17 | 18 |

{message}

19 |

{subMessage}

20 | 21 | 22 | 23 |
24 |
25 | ); 26 | }; 27 | 28 | export default UpgradeCard; 29 | -------------------------------------------------------------------------------- /frontend/src/components/UserAvatar.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarFallback, AvatarImage } from "src/components/ui/avatar"; 2 | import { useAlbyMe } from "src/hooks/useAlbyMe"; 3 | import { cn } from "src/lib/utils"; 4 | 5 | function UserAvatar({ className }: { className?: string }) { 6 | const { data: albyMe } = useAlbyMe(); 7 | 8 | return ( 9 | 10 | 11 | 12 | {(albyMe?.name || albyMe?.email || "SN").substring(0, 2).toUpperCase()} 13 | 14 | 15 | ); 16 | } 17 | 18 | export default UserAvatar; 19 | -------------------------------------------------------------------------------- /frontend/src/components/channels/ChannelPeerNote.tsx: -------------------------------------------------------------------------------- 1 | import { AlertCircleIcon } from "lucide-react"; 2 | import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert"; 3 | import { RecommendedChannelPeer } from "src/types"; 4 | 5 | type ChannelPeerNoteProps = { 6 | peer: RecommendedChannelPeer; 7 | }; 8 | 9 | export function ChannelPeerNote({ peer }: ChannelPeerNoteProps) { 10 | return ( 11 | 12 | 13 | 14 | Please note when opening a channel with {peer.name} 15 | 16 | {peer.note} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/components/channels/ChannelPublicPrivateAlert.tsx: -------------------------------------------------------------------------------- 1 | import { AlertCircleIcon } from "lucide-react"; 2 | import ExternalLink from "src/components/ExternalLink"; 3 | import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert"; 4 | 5 | export function ChannelPublicPrivateAlert() { 6 | return ( 7 | 8 | 9 | Conflicting Private / Public Channels 10 | 11 |
12 | You will not be able to receive payments on any private channels. It 13 | is recommended to only open all private or all public channels. 14 |
15 | 21 | Learn more 22 | 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/components/channels/DuplicateChannelAlert.tsx: -------------------------------------------------------------------------------- 1 | import { TriangleAlertIcon } from "lucide-react"; 2 | import ExternalLink from "src/components/ExternalLink"; 3 | import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert"; 4 | import { useChannels } from "src/hooks/useChannels"; 5 | 6 | type PeerAlertProps = { 7 | pubkey?: string; 8 | name?: string; 9 | }; 10 | 11 | export function DuplicateChannelAlert({ pubkey, name }: PeerAlertProps) { 12 | const { data: channels } = useChannels(); 13 | 14 | if (!pubkey) { 15 | return null; 16 | } 17 | 18 | const matchedPeer = channels?.find((p) => p.remotePubkey === pubkey); 19 | 20 | if (!matchedPeer) { 21 | return null; 22 | } 23 | 24 | return ( 25 | 26 | 27 | 28 | You already have a channel with{" "} 29 | {name && name !== "Custom" ? ( 30 | {name} 31 | ) : ( 32 | "the selected peer" 33 | )} 34 | 35 | 36 | There are other options available rather than opening multiple channels 37 | with the same counterparty.{" "} 38 | 42 | Learn more 43 | 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /frontend/src/components/connections/AboutAppCard.tsx: -------------------------------------------------------------------------------- 1 | import { AppStoreApp } from "src/components/connections/SuggestedAppData"; 2 | import { 3 | Card, 4 | CardContent, 5 | CardHeader, 6 | CardTitle, 7 | } from "src/components/ui/card"; 8 | 9 | export function AboutAppCard({ appStoreApp }: { appStoreApp: AppStoreApp }) { 10 | return ( 11 | 12 | 13 | About the App 14 | 15 | 16 |

17 | {appStoreApp.extendedDescription} 18 |

19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/components/connections/AppDetailConnectedApps.tsx: -------------------------------------------------------------------------------- 1 | import AppCard from "src/components/connections/AppCard"; 2 | import { AppStoreApp } from "src/components/connections/SuggestedAppData"; 3 | import { useAppsForAppStoreApp } from "src/hooks/useApps"; 4 | 5 | export function AppDetailConnectedApps({ 6 | appStoreApp, 7 | showTitle, 8 | }: { 9 | appStoreApp: AppStoreApp; 10 | showTitle?: boolean; 11 | }) { 12 | const connectedApps = useAppsForAppStoreApp(appStoreApp); 13 | 14 | if (!connectedApps?.length) { 15 | return null; 16 | } 17 | 18 | return ( 19 | <> 20 | {showTitle && ( 21 |

Your Connections

22 | )} 23 |
24 | {connectedApps.map((app) => ( 25 | 26 | ))} 27 |
28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/components/connections/AppStore.tsx: -------------------------------------------------------------------------------- 1 | import { CirclePlusIcon } from "lucide-react"; 2 | import ExternalLink from "src/components/ExternalLink"; 3 | import ResponsiveButton from "src/components/ResponsiveButton"; 4 | import SuggestedApps from "src/components/connections/SuggestedApps"; 5 | 6 | function AppStore() { 7 | return ( 8 | <> 9 |
10 |
11 |
12 |

App Store

13 |
14 |
15 | 16 | 21 | 22 |
23 |
24 |
25 | 26 | 27 | ); 28 | } 29 | 30 | export default AppStore; 31 | -------------------------------------------------------------------------------- /frontend/src/components/connections/AppTransactionList.tsx: -------------------------------------------------------------------------------- 1 | import TransactionsList from "src/components/TransactionsList"; 2 | import { TransactionsListMenu } from "src/components/TransactionsListMenu"; 3 | import { 4 | Card, 5 | CardContent, 6 | CardHeader, 7 | CardTitle, 8 | } from "src/components/ui/card"; 9 | 10 | export function AppTransactionList({ appId }: { appId: number }) { 11 | return ( 12 | 13 | 14 | Transactions 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/components/home/widgets/BlockHeightWidget.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardContent, 4 | CardHeader, 5 | CardTitle, 6 | } from "src/components/ui/card"; 7 | import { useMempoolApi } from "src/hooks/useMempoolApi"; 8 | 9 | export function BlockHeightWidget() { 10 | const { data: blocks } = useMempoolApi<{ height: number }[]>("/v1/blocks"); 11 | 12 | if (!blocks?.length) { 13 | return null; 14 | } 15 | return ( 16 | 17 | 18 | Block Height 19 | 20 | 21 |

{blocks[0].height}

22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/components/home/widgets/NodeStatusWidget.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { 3 | Card, 4 | CardContent, 5 | CardHeader, 6 | CardTitle, 7 | } from "src/components/ui/card"; 8 | import { useChannels } from "src/hooks/useChannels"; 9 | import { usePeers } from "src/hooks/usePeers"; 10 | 11 | export function NodeStatusWidget() { 12 | const { data: channels } = useChannels(); 13 | const { data: peers } = usePeers(); 14 | 15 | if (!channels || !peers) { 16 | return null; 17 | } 18 | return ( 19 | 20 | 21 | 22 | Node 23 | 24 | 25 |

Channels Online

26 |

27 | {channels.filter((c) => c.active).length || 0} /{" "} 28 | {channels.length || 0} 29 |

30 |

Connected Peers

31 |

32 | {peers.filter((p) => p.isConnected).length || 0} /{" "} 33 | {peers.length || 0} 34 |

35 |
36 |
37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/components/home/widgets/OnchainFeesWidget.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardContent, 4 | CardHeader, 5 | CardTitle, 6 | } from "src/components/ui/card"; 7 | import { useMempoolApi } from "src/hooks/useMempoolApi"; 8 | 9 | export function OnchainFeesWidget() { 10 | const { data: recommendedFees } = useMempoolApi<{ 11 | fastestFee: number; 12 | halfHourFee: number; 13 | economyFee: number; 14 | minimumFee: number; 15 | }>("/v1/fees/recommended"); 16 | 17 | if (!recommendedFees) { 18 | return null; 19 | } 20 | 21 | const entries = [ 22 | { 23 | title: "No priority", 24 | value: recommendedFees.minimumFee, 25 | }, 26 | { 27 | title: "Low priority", 28 | value: recommendedFees.economyFee, 29 | }, 30 | { 31 | title: "Medium priority", 32 | value: recommendedFees.halfHourFee, 33 | }, 34 | { 35 | title: "High priority", 36 | value: recommendedFees.fastestFee, 37 | }, 38 | ]; 39 | 40 | return ( 41 | 42 | 43 | On-Chain Fees 44 | 45 | 46 | {entries.map((entry) => ( 47 |
48 |

{entry.title}

49 |

{entry.value} sat/vB

50 |
51 | ))} 52 |
53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/components/home/widgets/WhatsNewWidget.tsx: -------------------------------------------------------------------------------- 1 | import { compare } from "compare-versions"; 2 | import { 3 | Card, 4 | CardContent, 5 | CardDescription, 6 | CardHeader, 7 | CardTitle, 8 | } from "src/components/ui/card"; 9 | import { ExternalLinkButton } from "src/components/ui/custom/external-link-button"; 10 | import { useAlbyInfo } from "src/hooks/useAlbyInfo"; 11 | import { useInfo } from "src/hooks/useInfo"; 12 | 13 | export function WhatsNewWidget() { 14 | const { data: info } = useInfo(); 15 | const { data: albyInfo } = useAlbyInfo(); 16 | 17 | if (!info || !albyInfo || !albyInfo.hub.latestReleaseNotes) { 18 | return null; 19 | } 20 | 21 | const upToDate = 22 | info.version && 23 | info.version.startsWith("v") && 24 | compare(info.version.substring(1), albyInfo.hub.latestVersion, ">="); 25 | 26 | return ( 27 | 28 | 29 | What's New in {upToDate && "your "}Alby Hub? 30 | {albyInfo.hub.latestReleaseNotes} 31 | 32 | {!upToDate && ( 33 | 34 | 38 | Update Now 39 | 40 | 41 | )} 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Apple.tsx: -------------------------------------------------------------------------------- 1 | import { SVGAttributes } from "react"; 2 | 3 | export function AppleIcon(props: SVGAttributes) { 4 | return ( 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Chrome.tsx: -------------------------------------------------------------------------------- 1 | import { SVGAttributes } from "react"; 2 | 3 | export function ChromeIcon(props: SVGAttributes) { 4 | return ( 5 | 12 | 18 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Firefox.tsx: -------------------------------------------------------------------------------- 1 | import { SVGAttributes } from "react"; 2 | 3 | export function FirefoxIcon(props: SVGAttributes) { 4 | return ( 5 | 12 | 18 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/components/icons/LDK.tsx: -------------------------------------------------------------------------------- 1 | export const LDKIcon = () => { 2 | return ( 3 | 4 | 5 | 6 | 7 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Phoenixd.tsx: -------------------------------------------------------------------------------- 1 | export const PhoenixdIcon = () => { 2 | return ( 3 | 4 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /frontend/src/components/icons/PlayStore.tsx: -------------------------------------------------------------------------------- 1 | import { SVGAttributes } from "react"; 2 | 3 | export function PlayStoreIcon(props: SVGAttributes) { 4 | return ( 5 | 13 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/components/icons/ZapStore.tsx: -------------------------------------------------------------------------------- 1 | import { SVGAttributes } from "react"; 2 | 3 | export function ZapStoreIcon(props: SVGAttributes) { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/components/layouts/ReceiveLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router-dom"; 2 | import AppHeader from "src/components/AppHeader"; 3 | import Loading from "src/components/Loading"; 4 | import { useBalances } from "src/hooks/useBalances"; 5 | import { useChannels } from "src/hooks/useChannels"; 6 | 7 | import { useInfo } from "src/hooks/useInfo"; 8 | 9 | export default function ReceiveLayout() { 10 | const { hasChannelManagement } = useInfo(); 11 | const { data: balances } = useBalances(); 12 | const { data: channels } = useChannels(); 13 | 14 | if (!balances || !channels) { 15 | return ; 16 | } 17 | 18 | return ( 19 |
20 | 25 | Receive Limit: 26 |
27 | {new Intl.NumberFormat().format( 28 | Math.floor(balances.lightning.totalReceivable / 1000) 29 | )}{" "} 30 | sats 31 |
32 |
33 | ) 34 | } 35 | /> 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/components/password/PasswordInput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import RevealPasswordToggle from "src/components/password/RevealPasswordToggle"; 3 | import { InputWithAdornment } from "src/components/ui/custom/input-with-adornment"; 4 | 5 | type PasswordInputProps = Omit< 6 | React.InputHTMLAttributes, 7 | "type" | "onChange" | "value" 8 | > & { 9 | value: string; 10 | onChange?: (value: string) => void; 11 | }; 12 | 13 | export default function PasswordInput({ 14 | onChange, 15 | placeholder, 16 | value, 17 | ...restProps 18 | }: PasswordInputProps) { 19 | const [passwordVisible, setPasswordVisible] = React.useState(false); 20 | 21 | return ( 22 | onChange && onChange(e.target.value)} 27 | placeholder={placeholder} 28 | {...restProps} 29 | endAdornment={ 30 | 34 | } 35 | /> 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/components/password/RevealPasswordToggle.tsx: -------------------------------------------------------------------------------- 1 | import { EyeIcon, EyeOffIcon } from "lucide-react"; 2 | import { useEffect, useState } from "react"; 3 | import { cn } from "src/lib/utils"; 4 | 5 | type Props = { 6 | onChange: (isRevealed: boolean) => void; 7 | isRevealed?: boolean; 8 | iconClass?: string; 9 | }; 10 | 11 | export default function RevealPasswordToggle({ 12 | onChange, 13 | isRevealed, 14 | iconClass, 15 | }: Props) { 16 | const [_isRevealed, setRevealed] = useState(false); 17 | 18 | // toggle the button if password view is handled by component itself 19 | useEffect(() => { 20 | if (typeof isRevealed !== "undefined") { 21 | setRevealed(isRevealed); 22 | } 23 | }, [isRevealed]); 24 | 25 | return ( 26 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/components/redirects/DefaultRedirect.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Outlet, useLocation, useNavigate } from "react-router-dom"; 3 | import Loading from "src/components/Loading"; 4 | import { localStorageKeys } from "src/constants"; 5 | import { useInfo } from "src/hooks/useInfo"; 6 | 7 | export function DefaultRedirect() { 8 | const { data: info } = useInfo(); 9 | const location = useLocation(); 10 | const navigate = useNavigate(); 11 | 12 | React.useEffect(() => { 13 | if ( 14 | !info || 15 | (info.running && 16 | info.unlocked && 17 | (info.albyAccountConnected || !info.albyUserIdentifier)) 18 | ) { 19 | return; 20 | } 21 | const returnTo = location.pathname + location.search; 22 | window.localStorage.setItem(localStorageKeys.returnTo, returnTo); 23 | navigate("/"); 24 | }, [info, location, navigate]); 25 | 26 | if (!info) { 27 | return ; 28 | } 29 | 30 | return ; 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/components/redirects/SetupRedirect.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Outlet, useLocation, useNavigate } from "react-router-dom"; 3 | import Loading from "src/components/Loading"; 4 | import { useInfo } from "src/hooks/useInfo"; 5 | 6 | export function SetupRedirect() { 7 | const { data: info } = useInfo(); 8 | const location = useLocation(); 9 | const navigate = useNavigate(); 10 | 11 | React.useEffect(() => { 12 | if (!info) { 13 | return; 14 | } 15 | if (info.setupCompleted && info.running) { 16 | navigate("/"); 17 | return; 18 | } 19 | }, [info, location, navigate]); 20 | 21 | if (!info) { 22 | return ; 23 | } 24 | 25 | return ; 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/components/redirects/StartRedirect.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useLocation, useNavigate } from "react-router-dom"; 3 | import Loading from "src/components/Loading"; 4 | import { useInfo } from "src/hooks/useInfo"; 5 | 6 | export function StartRedirect({ children }: React.PropsWithChildren) { 7 | const { data: info } = useInfo(); 8 | const location = useLocation(); 9 | const navigate = useNavigate(); 10 | 11 | React.useEffect(() => { 12 | if (!info || (info.setupCompleted && !info.running)) { 13 | if (info && !info.albyAccountConnected && info.albyUserIdentifier) { 14 | navigate("/alby/auth"); 15 | } 16 | return; 17 | } 18 | 19 | navigate("/"); 20 | }, [info, location, navigate]); 21 | 22 | if (!info) { 23 | return ; 24 | } 25 | 26 | return children; 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 5 | 6 | import { cn } from "src/lib/utils"; 7 | 8 | function Avatar({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ); 22 | } 23 | 24 | function AvatarImage({ 25 | className, 26 | ...props 27 | }: React.ComponentProps) { 28 | return ( 29 | 34 | ); 35 | } 36 | 37 | function AvatarFallback({ 38 | className, 39 | ...props 40 | }: React.ComponentProps) { 41 | return ( 42 | 50 | ); 51 | } 52 | 53 | export { Avatar, AvatarImage, AvatarFallback }; 54 | -------------------------------------------------------------------------------- /frontend/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { type VariantProps } from "class-variance-authority"; 3 | import * as React from "react"; 4 | import { buttonVariants } from "src/components/ui/buttonVariants"; 5 | 6 | import { cn } from "src/lib/utils"; 7 | 8 | function Button({ 9 | className, 10 | variant, 11 | size, 12 | asChild = false, 13 | ...props 14 | }: React.ComponentProps<"button"> & 15 | VariantProps & { 16 | asChild?: boolean; 17 | }) { 18 | const Comp = asChild ? Slot : "button"; 19 | 20 | return ( 21 | 26 | ); 27 | } 28 | 29 | export { Button }; 30 | -------------------------------------------------------------------------------- /frontend/src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 5 | import { CheckIcon } from "lucide-react"; 6 | 7 | import { cn } from "src/lib/utils"; 8 | 9 | function Checkbox({ 10 | className, 11 | ...props 12 | }: React.ComponentProps) { 13 | return ( 14 | 22 | 26 | 27 | 28 | 29 | ); 30 | } 31 | 32 | export { Checkbox }; 33 | -------------------------------------------------------------------------------- /frontend/src/components/ui/custom/carousel-dots.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | 5 | import { cn } from "src/lib/utils"; 6 | 7 | // Dot Button Hook 8 | export type UseDotButtonType = { 9 | selectedIndex: number; 10 | scrollSnaps: number[]; 11 | onDotButtonClick: (index: number) => void; 12 | }; 13 | 14 | // Dot Button Component 15 | const CarouselDotButton = React.forwardRef< 16 | HTMLButtonElement, 17 | React.ComponentPropsWithRef<"button"> 18 | >(({ className, ...props }, ref) => { 19 | return ( 20 | 36 | ); 37 | } 38 | ); 39 | LoadingButton.displayName = "LoadingButton"; 40 | 41 | export { LoadingButton }; 42 | -------------------------------------------------------------------------------- /frontend/src/components/ui/custom/useDotButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import type { EmblaCarouselType } from "node_modules/embla-carousel/esm/components/EmblaCarousel"; 3 | import * as React from "react"; 4 | import { UseDotButtonType } from "src/components/ui/custom/carousel-dots"; 5 | 6 | export const useDotButton = ( 7 | emblaApi: EmblaCarouselType | undefined 8 | ): UseDotButtonType => { 9 | const [selectedIndex, setSelectedIndex] = React.useState(0); 10 | const [scrollSnaps, setScrollSnaps] = React.useState([]); 11 | 12 | const onDotButtonClick = React.useCallback( 13 | (index: number) => { 14 | if (!emblaApi) { 15 | return; 16 | } 17 | emblaApi.scrollTo(index); 18 | }, 19 | [emblaApi] 20 | ); 21 | 22 | const onInit = React.useCallback((emblaApi: EmblaCarouselType) => { 23 | setScrollSnaps(emblaApi.scrollSnapList()); 24 | }, []); 25 | 26 | const onSelect = React.useCallback((emblaApi: EmblaCarouselType) => { 27 | setSelectedIndex(emblaApi.selectedScrollSnap()); 28 | }, []); 29 | 30 | React.useEffect(() => { 31 | if (!emblaApi) { 32 | return; 33 | } 34 | 35 | onInit(emblaApi); 36 | onSelect(emblaApi); 37 | emblaApi.on("reInit", onInit).on("reInit", onSelect).on("select", onSelect); 38 | }, [emblaApi, onInit, onSelect]); 39 | 40 | return { 41 | selectedIndex, 42 | scrollSnaps, 43 | onDotButtonClick, 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /frontend/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "src/lib/utils"; 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ); 19 | } 20 | 21 | export { Input }; 22 | -------------------------------------------------------------------------------- /frontend/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as LabelPrimitive from "@radix-ui/react-label"; 5 | 6 | import { cn } from "src/lib/utils"; 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ); 22 | } 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /frontend/src/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | import * as ProgressPrimitive from "@radix-ui/react-progress"; 2 | import * as React from "react"; 3 | 4 | import { cn } from "src/lib/utils"; 5 | 6 | function Progress({ 7 | className, 8 | value, 9 | ...props 10 | }: React.ComponentProps) { 11 | return ( 12 | 20 | 25 | 26 | ); 27 | } 28 | 29 | export { Progress }; 30 | -------------------------------------------------------------------------------- /frontend/src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 5 | 6 | import { cn } from "src/lib/utils"; 7 | 8 | function Separator({ 9 | className, 10 | orientation = "horizontal", 11 | decorative = true, 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 25 | ); 26 | } 27 | 28 | export { Separator }; 29 | -------------------------------------------------------------------------------- /frontend/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "src/lib/utils"; 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 |
10 | ); 11 | } 12 | 13 | export { Skeleton }; 14 | -------------------------------------------------------------------------------- /frontend/src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | import { Toaster as Sonner, ToasterProps } from "sonner"; 2 | import { useTheme } from "src/components/ui/theme-provider"; 3 | 4 | const Toaster = ({ ...props }: ToasterProps) => { 5 | const { theme = "system" } = useTheme(); 6 | 7 | return ( 8 | 26 | ); 27 | }; 28 | 29 | export { Toaster }; 30 | -------------------------------------------------------------------------------- /frontend/src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SwitchPrimitive from "@radix-ui/react-switch"; 5 | 6 | import { cn } from "src/lib/utils"; 7 | 8 | function Switch({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | 27 | 28 | ); 29 | } 30 | 31 | export { Switch }; 32 | -------------------------------------------------------------------------------- /frontend/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "src/lib/utils"; 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 |