Compare commits

..

346 commits

Author SHA1 Message Date
Roland
d8ef0e70e0
chore: sync Wails CLI version in CI with go.mod (v2.14.0) (#2547)
Some checks are pending
Multiplatform Docker build & push / build (push) Waiting to run
Code quality - linting and typechecking / linting (push) Waiting to run
Backend testing with Postgres / test-postgres (push) Waiting to run
The Dependabot bump updated github.com/wailsapp/wails/v2 to v2.14.0 in
go.mod but the wails workflow still installed the CLI at v2.12.0. Align
the CI install and document in AGENTS.md that both must be updated
together.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 14:41:11 +07:00
Roland
d2cebc5c6f
fix: replace react-lottie with lottie-react for Vite 8 compatibility (#2546)
Vite 8 changed CJS default-import interop: with "type": "module" set,
a default import of a CJS dependency now resolves to the whole
module.exports object instead of its .default export. react-lottie is
CJS-only, so <Lottie> received an object as the element type and
crashed LottieLoading/LottieSuccess with "Element type is invalid".

Swap to the maintained, ESM-built lottie-react, aliasing it to its ES
build since its browser field points at a UMD build with the same
interop hazard. No other dependency is affected by the interop change.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 23:52:50 +07:00
Roland
edd283cdb2
fix: update encryption scheme for node migration files (#2539)
* fix: update encryption scheme for node migration files

Migration files are now encrypted with AES-CTR using a key derived via
Argon2 with a 32-byte salt, the same derivation used for encrypted
configuration values. Files created by earlier versions can still be
restored: the restore path detects the scheme by trial-decrypting the
archive header and checking for the ZIP file signature, which also
rejects an incorrect unlock password up front instead of extracting
garbage.

The migration screen now also tells users to never share their
migration file with anyone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: reword migration file warning

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: read full migration file header before detecting cipher scheme

io.ReadAtLeast can return once the smallest scheme's header is read,
which truncates the larger current-scheme header when the reader
delivers short reads (e.g. a network request body). Read the full
header and only tolerate a short read that still covers the smallest
scheme.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: extract migration files to a staging directory during restore

If extraction failed partway through, the partially populated restore
directory was left in the working directory, and the next startup would
apply the incomplete restore. Extract to a staging directory and only
move it into place after every entry has been extracted successfully.
Also reject archives that contain no files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: assert traversal-specific error in restore backup test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 22:14:49 +07:00
Roland
1c7abc62e9
chore: add Bark terms link and exit disclosure to security page (#2544)
* chore: add Bark terms link and exit disclosure to security page

Link Second's Terms of Service from the Bark setup security screen, note
that the hub must stay online so automatically refreshed funds do not
expire, and clarify (via tooltip) that unilateral exit is not built into
Alby Hub yet and must be executed manually with the wallet data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: improve copy

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 22:04:43 +07:00
Roland
1c7c026e54
chore: make vite config compatible with native config loader (#2542)
Replace __dirname with import.meta.dirname and use Vite's native
resolve.tsconfigPaths option instead of the vite-tsconfig-paths plugin.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 21:40:43 +07:00
Roland
5125418188
chore(deps): bump dependencies to fix Dependabot alerts (#2541)
* chore(deps): bump google.golang.org/grpc to v1.82.1 and edwards25519 to v1.1.1

Fixes Dependabot alerts GHSA-hrxh-6v49-42gf (gRPC-Go xDS RBAC and HTTP/2
vulnerabilities) and GHSA-fw7p-63qq-7hpr (edwards25519 MultiScalarMult).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(deps): bump react-router to 7.18.2 and refresh vulnerable transitive deps

Bumps react-router 7.14.2 -> 7.18.2 and re-resolves fast-uri, js-yaml,
brace-expansion, minimatch, picomatch, flatted and @babel packages to
patched versions, clearing the remaining open npm Dependabot alerts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 19:57:27 +07:00
Roland
363c22f6d3
fix: switch unlock rate limiter from per-IP to global (#2540)
Some checks are pending
Multiplatform Docker build & push / build (push) Waiting to run
Code quality - linting and typechecking / linting (push) Waiting to run
Backend testing with Postgres / test-postgres (push) Waiting to run
The unlock endpoints were rate limited per client IP, which is derived
from request headers and so is chosen by the caller. Switch to a single
global rate limiter (one bucket for all callers) and apply it to every
endpoint that verifies the unlock password: start, unlock, backup,
mnemonic, apps, autoswap, unlock-password and auto-unlock. A small burst
keeps unlocking and immediately performing an action working.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 16:25:56 +07:00
Roland
3b3c37dd0c
fix: remove legacy acceptance of empty unlock password check (#2534)
* fix: remove legacy acceptance of empty unlock password check

CheckUnlockPassword previously treated a missing or empty
UnlockPasswordCheck value as a match — a legacy compatibility path from
before the canary was always written. It now requires the stored value
to be present and to equal the expected string.

StartApp checks for the canary up front and, if it is missing, stops
with a message asking the user to restore from a backup rather than
continuing. A new IsUnlockPasswordCheckSet helper reports whether the
value is present.

keys.Init now returns the error from reading NostrSecretKey instead of
ignoring it, so a read failure aborts instead of generating and saving a
new key.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: add operation context to unlock password check errors

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 14:48:06 +07:00
Roland
979644cf68
fix: limit LSP opening fees for JIT channel invoices (#2535)
* fix: limit LSP opening fees for JIT channel invoices

JIT channel invoices are now created with a maximum LSP opening fee
instead of no limit: the fee the LSP advertises in its LSPS2 opening fee
menu for the payment size, bounded by an absolute ceiling of 5000 sats
or 10% of the payment, whichever is greater. Invoice creation fails if
the LSP quotes a fee above this limit.

The minimum JIT payment size calculation now uses the same ceiling so
the advertised receivable range matches what invoice creation accepts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: validate invoice expiry range and guard LSPS2 cache reads

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 13:38:13 +07:00
Roland
f4010e239a
fix: validate swap out invoice before payment (#2536)
Verify the invoice returned when creating a swap out before storing and
paying it:

- the invoice payment hash must match the payment hash of the locally
  generated preimage
- the invoice amount must not exceed the requested amount plus the
  quoted service and miner fees (with a small rounding tolerance)
- the lockup address is checked against the swap tree, matching the
  checks already performed for swap in and refunds
- the invoice is verified again directly before it is paid

Also renames AlbySwapServiceFee to AlbySwapServiceFeePercentage for
clarity.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 12:33:40 +07:00
Roland
4c5bef42c6
fix: require full access api key for log endpoint (#2537)
Move GET /api/log/:type from the read-only API group to the
full-access group, matching /api/swaps/mnemonic. Add tests asserting
a readonly token receives 403 from the log endpoint and a full-access
token can still read it.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 12:29:03 +07:00
Adithya Vardhan
0c24ab84c1
fix: avoid leaking raw postgres error details in duplicate key response (#2538) 2026-08-11 18:45:59 +05:30
dependabot[bot]
459b825cfb
build(deps): bump github.com/lightningnetwork/lnd from 0.21.0-beta to 0.21.1-beta (#2517)
build(deps): bump github.com/lightningnetwork/lnd

Bumps [github.com/lightningnetwork/lnd](https://github.com/lightningnetwork/lnd) from 0.21.0-beta to 0.21.1-beta.
- [Release notes](https://github.com/lightningnetwork/lnd/releases)
- [Changelog](https://github.com/lightningnetwork/lnd/blob/master/docs/release_branch_management.md)
- [Commits](https://github.com/lightningnetwork/lnd/compare/v0.21.0-beta...v0.21.1-beta)

---
updated-dependencies:
- dependency-name: github.com/lightningnetwork/lnd
  dependency-version: 0.21.1-beta
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-11 17:07:03 +05:30
dependabot[bot]
e3278beffb
build(deps): bump github.com/mattn/go-sqlite3 from 1.14.48 to 1.14.49 (#2514)
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.48 to 1.14.49.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.48...v1.14.49)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-sqlite3
  dependency-version: 1.14.49
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-11 16:36:30 +05:30
dependabot[bot]
852b30fa02
build(deps): bump gorm.io/driver/postgres from 1.6.0 to 1.6.2 (#2515)
Bumps [gorm.io/driver/postgres](https://github.com/go-gorm/postgres) from 1.6.0 to 1.6.2.
- [Commits](https://github.com/go-gorm/postgres/compare/v1.6.0...v1.6.2)

---
updated-dependencies:
- dependency-name: gorm.io/driver/postgres
  dependency-version: 1.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-11 16:36:15 +05:30
dependabot[bot]
ffc705e536
build(deps): bump github.com/wailsapp/wails/v2 from 2.12.0 to 2.14.0 (#2513)
* build(deps): bump github.com/wailsapp/wails/v2 from 2.12.0 to 2.13.0

Bumps [github.com/wailsapp/wails/v2](https://github.com/wailsapp/wails) from 2.12.0 to 2.13.0.
- [Release notes](https://github.com/wailsapp/wails/releases)
- [Commits](https://github.com/wailsapp/wails/compare/v2.12.0...v2.13.0)

---
updated-dependencies:
- dependency-name: github.com/wailsapp/wails/v2
  dependency-version: 2.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: bump wails version to v2.14.0

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Adithya Vardhan <imadithyavardhan@gmail.com>
2026-08-11 16:22:33 +05:30
dependabot[bot]
d3847fdaae
build(deps-dev): bump vite from 5.4.19 to 8.2.0 in /frontend (#2516)
* build(deps-dev): bump vite from 5.4.19 to 8.2.0 in /frontend

Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.19 to 8.2.0.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@8.2.0/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.2.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix: remove stale react paths override in tsconfig

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Adithya Vardhan <imadithyavardhan@gmail.com>
2026-08-11 16:20:48 +05:30
dependabot[bot]
7c7dfa6876
build(deps): bump lucide-react from 1.7.0 to 1.28.0 in /frontend (#2518)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 1.7.0 to 1.28.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.28.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-11 15:48:31 +05:30
dependabot[bot]
621db07fc8
build(deps): bump @fontsource-variable/figtree from 5.2.10 to 5.3.0 in /frontend (#2519)
Some checks are pending
Multiplatform Docker build & push / build (push) Waiting to run
Code quality - linting and typechecking / linting (push) Waiting to run
Backend testing with Postgres / test-postgres (push) Waiting to run
build(deps): bump @fontsource-variable/figtree in /frontend

Bumps [@fontsource-variable/figtree](https://github.com/fontsource/font-files/tree/HEAD/fonts/variable/figtree) from 5.2.10 to 5.3.0.
- [Changelog](https://github.com/fontsource/font-files/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fontsource/font-files/commits/HEAD/fonts/variable/figtree)

---
updated-dependencies:
- dependency-name: "@fontsource-variable/figtree"
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-11 15:40:39 +05:30
Roland
4402d2fff8
fix: remove request bodies from error logs (#2533)
The Wails request router included the full request body in its error
log entries, and the HTTP app creation handler logged the whole request
struct on failure. Log only the route, method and error instead,
matching the existing behavior of the /api/mnemonic handler, and log
only the route and method for requests in the desktop frontend.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 15:49:26 +07:00
Roland
5f9a88843c
fix: validate LND and CLN credential files during setup (#2528)
The setup API accepts file paths for the LND certificate and macaroon
and for the CLN lightning directory. Previously the raw file contents
were read and stored without any validation.

Validate these inputs before persisting them:

- LND cert: parse the PEM and store only the re-encoded certificate(s),
  discarding any other PEM blocks (e.g. a bundled private key).
- LND macaroon: unmarshal and store the re-marshalled macaroon.
- CLN lightning directory: verify it contains the TLS credentials
  (ca.pem, client.pem, client-key.pem) that CLN loads at connect time,
  including the hold subdirectory when configured.

On failure, return a generic error to the client and log the detail
server-side. File paths remain supported for Umbrel-style installs.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 15:05:56 +07:00
Roland
3d22993389
fix: validate return_to redirect URLs (#2532)
return_to query parameters are now parsed and only http and https URLs
are used for redirects, both in the frontend and when the createApp API
adds the connection parameters to the URL.

The production frontend build now also includes the same
Content-Security-Policy meta tag that is served as a header in http
mode, so the policy also applies where no HTTP headers are set, e.g. in
the desktop app.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 14:57:51 +07:00
Roland
0b0cbbd985
fix: make event assertions in tests wait for async event consumption (#2531)
The mock event consumer waited a fixed 10ms before returning consumed
events, which was not always enough on slow CI runners and caused flaky
failures (e.g. TestMarkSettled_App_BudgetWarning missing its
nwc_budget_warning event). It also appended to the events slice from
concurrent goroutines without synchronization, a data race that could
drop events.

- guard the consumed events slice with a mutex and return copies
- add WaitForConsumedEvents which polls until the expected number of
  events arrived (up to 5s) instead of relying on a fixed sleep
- use it in tests that assert on consumed events; tests asserting that
  no event was published keep the short grace period
- normalize event order in the keysend self-payment test, matching the
  existing approach in the self-payment test, since async publishing
  does not guarantee ordering

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 14:48:41 +07:00
Roland
fa5cc3511e
fix: prevent backup restore from writing outside the restore directory (#2529)
Archive entry names come from the uploaded backup and were joined to the
restore directory without validation, so an entry name containing ".."
segments could resolve to a path outside it. Reject entries whose name is
absolute or escapes the restore directory, and confirm the cleaned
destination path stays within it before writing.

Add a test covering rejection of an entry that points outside the restore
directory.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 23:42:45 +07:00
Roland
037765794d
fix: keep showing migration success page after creating migration file (#2527)
Some checks are pending
Multiplatform Docker build & push / build (push) Waiting to run
Code quality - linting and typechecking / linting (push) Waiting to run
Backend testing with Postgres / test-postgres (push) Waiting to run
* fix: keep showing migration success page after creating migration file

After creating a node migration file the hub is halted and the Alby
OAuth token is intentionally removed, so visiting the homepage sent the
user through /start into the Alby OAuth flow. Track the halted state
in memory and redirect back to the migration success page instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: synchronize migration flag access and propagate zip close error

Make nodeMigrationFileCreated an atomic.Bool since it is written by
CreateBackup and read by GetInfo on concurrent HTTP handler goroutines,
and finalize the migration archive explicitly so a failed zip close
returns an error instead of reporting a corrupt backup as success.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: return minimal info response after migration file is created

Once a migration file is created the hub is halted and the database is
closed. GetInfo previously only worked because every config key it reads
happened to be served from the config cache; any cache miss on an
error-propagating read would fail /api/info. Return early with a minimal
response instead so the migration success page does not depend on cache
state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 15:37:26 +07:00
Roland
56d118a851
fix: remove dollar sign from linux install scripts (#2525) 2026-08-10 15:33:20 +07:00
Roland
ffee8cbcbe
feat: enable migration from postgres to sqlite (#2524)
Allows users running Alby Hub on postgres (e.g. Alby Cloud) to create a
migration file from Settings -> Migrate Alby Hub. The contents of the
postgres database are copied into a temporary local sqlite database
which is included in the migration file, so it can be imported into a
fresh sqlite-based hub.

- extract the db_migrate CLI copy logic into a shared db.MigrateDB
- also copy the swaps and forwards tables (previously silently dropped)
- only require VSS in the source when migrating to postgres
- show a hint on the migrate page when running on postgres
- show database storage type and VSS status on the about page
- don't log an error when removing non-existent db files before restore

Closes #2500

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 14:20:41 +07:00
Roland
6d0cb6fd2c
fix: bark onboarding, migration messaging and receive settlement for bark 0.6.0 (#2523)
Some checks failed
Multiplatform Docker build & push / build (push) Has been cancelled
Code quality - linting and typechecking / linting (push) Has been cancelled
Backend testing with Postgres / test-postgres (push) Has been cancelled
* fix: update bark onboarding and backup messaging for seed-based recovery

Since bark 0.6.0, offchain funds are recoverable from the mnemonic
alone via the seed-derived recovery mailbox. Remove the outdated
warnings that the recovery phrase is not sufficient, show the standard
recovery guidance for bark during onboarding, and expose the
seed-recovery scan result as a 'recoveryreport' custom node command so
users migrating to a new device can verify their funds were restored.

Closes #2512

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: settle bark lightning receives in the new delivering state

bark 0.6.0 added a 'delivering' receive state between preimage reveal
and settlement. The receive claim handler only treated
'preimage-revealed' and 'settled' as paid, so claimed receives were
published without a preimage and the transactions service rejected the
settlement ('no preimage in payment'), leaving paid invoices pending
forever.

Recognize all states at or past preimage reveal via a receiveIsPaid
helper (a positive allowlist, so an unknown future state degrades to
pending rather than falsely settled), only mark the transaction settled
when the preimage is present, and prefer bark's own settled_at
timestamp when available.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: replace import channels checkbox with LDK-specific warning

The 'I don't have another Alby Hub to migrate or open channels'
checkbox on the import recovery phrase screen only applied to LDK but
was required for every backend, and its claim that channel funds are
always lost is wrong when dynamic channel backups (VSS) are enabled.

Remove the checkbox and the channels bullet from the import screen and
show the caveat on the Security & Recovery page instead, only when a
mnemonic was imported and the LDK backend was chosen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 16:39:02 +07:00
Roland
d198b19bef
feat: enable typing card name when choosing other card (#2511)
* feat: enable typing card name when choosing other card

Closes #2457

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: reset connect-card dialog form on open and show empty name validation error

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: use shadcn Button for other-card option in connect dialog

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 13:38:46 +07:00
Roland
5f4e52bd88
fix: publish transaction events only after the database transaction commits (#2520)
* fix: publish transaction events only after the database transaction commits

markTransactionSettled and markPaymentFailed published nwc_payment_sent /
nwc_payment_received / nwc_payment_failed (and checkBudgetUsage published
nwc_budget_warning) while still inside the caller's database transaction, so
connected apps and the Alby API could be notified of a payment whose row was
never committed, and subscribers reading the database in response to an event
could race with the commit.

Every function that writes transaction state now owns its own database
transaction and publishes its events only after the commit succeeds:

- markTransactionSettled and markPaymentFailed open their own transaction;
  callers no longer wrap them in db.Transaction
- new createSettledTransactionFromNotification inserts transactions reported
  by LNClient notifications for payments the hub has no record of (external
  payments, received keysends) directly in their settled state, removing the
  transient PENDING row and the zombie row left behind on duplicate events
- markPaymentFailed now refuses to mark a settled transaction as failed,
  replacing CancelHoldInvoice's in-transaction ACCEPTED re-check and also
  protecting the SendPaymentSync error path from a racing settle
- checkBudgetUsage returns the budget warning event instead of publishing it

Closes #2506

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: serialize payment failure with settlement and propagate lock errors

Address review findings on the previous commit:

- markPaymentFailed now takes the same payment-hash row lock as settlement
  (postgres), so the settled-state guard cannot be bypassed by a concurrent
  settle between the state check and the update; it also returns not-found
  instead of publishing an event when the transaction row no longer exists,
  and reports whether this call transitioned the row so CancelHoldInvoice
  only publishes nwc_hold_invoice_canceled when it performed the cancellation
- findSettledTransaction propagates errors from the lock query and the
  settled-transaction lookup instead of treating a failed lookup as
  "no settled transaction exists", which could defeat the dedup guard
- TestMarkSettled_Twice no longer shares one transaction struct between
  concurrent goroutines and collects errors instead of asserting inside them

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: mark failed keysend payments via markPaymentFailed

The SendKeysend failure path updated the transaction directly, which never
zeroed the fee reserve, recorded no failure reason, published no
nwc_payment_failed event, and had no guard against overwriting a
concurrently settled payment. Route it through markPaymentFailed like
SendPaymentSync, and allow MockLn keysends to fail so the path is testable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 13:38:28 +07:00
Roland
6175489cb0
chore: remove unused argon2-wasm-esm dependency (#2508)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 17:55:38 +07:00
Roland
bdce8fe8d2
fix: use scope constant in get_budget permission query (#2510)
Some checks are pending
Multiplatform Docker build & push / build (push) Waiting to run
Code quality - linting and typechecking / linting (push) Waiting to run
Backend testing with Postgres / test-postgres (push) Waiting to run
* fix: use scope constant in get_budget permission query

The get_budget controller filtered the app_permissions scope column with
models.PAY_INVOICE_METHOD, which only matched because the method and
scope constants share the same string value. Use
constants.PAY_INVOICE_SCOPE like every other scope lookup, and document
why the unchecked First result is safe.

Fixes #2503

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: return error from get_budget on unexpected permission query failure

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 12:39:07 +07:00
Roland
d94f6933f5
fix: remove avatar from lightning address QR (#2509)
The avatar overlay made the QR code hard to scan, especially for
short lightning addresses. Without center content the QR also drops
back to a lower error correction level, improving scannability.

Fixes #2507

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 11:20:01 +07:00
Alchemist
35d666d469
feat: filter transactions (#2464)
* feat: filter transactions

* fix: harden transaction filters

* refactor: use explicit nullable transaction filters with HideFailed polarity

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: set transaction filters in a dialog from wallet actions menu

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: filter transactions by search term and type

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: reject invalid transaction filters and reset page synchronously

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: parse complete minimum amount value in transactions filter dialog

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 10:52:14 +07:00
Josip
3b3e784fa6
fix: fallback to outgoing payments in Phoenixd LookupInvoice (#2447)
* fix: fallback to outgoing payments in Phoenixd LookupInvoice

LookupInvoice only queried /payments/incoming/{hash}, returning 404 for
outgoing payments. This caused all outgoing Lightning payments to remain
permanently stuck as PENDING in Alby Hub.

The fix tries incoming first (preserving existing behavior), then falls
back to listing outgoing payments and matching by paymentHash.

Fixes #2442

* fix: amount and fees in phoenix payment to transaction

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
2026-08-06 17:24:10 +07:00
Roland
fbaff5d8a0
fix: show icon and proper name for lightning node backend on about page (#2501)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 17:21:00 +07:00
Peter
355454a4cc
fix: Mandatory upgrade for bark SDK (#2502)
Some checks are pending
Multiplatform Docker build & push / build (push) Waiting to run
Code quality - linting and typechecking / linting (push) Waiting to run
Backend testing with Postgres / test-postgres (push) Waiting to run
chore: update bark bindings to v0.15.0

Moves from bark 0.4.0 to 0.6.0. The Ark server now requires protocol version 5
(hashlock clauses) to start a lightning receive, which only bark 0.6.0 sends, so
older clients are refused outright and cannot generate invoices.

No client changes needed: v0.15.0 only adds to the surface we use.
2026-08-06 10:43:41 +07:00
Roland
806dfd4e1b
chore: bump ldk node dependencies (#2498)
Some checks are pending
Multiplatform Docker build & push / build (push) Waiting to run
Code quality - linting and typechecking / linting (push) Waiting to run
Backend testing with Postgres / test-postgres (push) Waiting to run
- rust-lightning to 0.2.4
2026-08-05 15:51:25 +07:00
Roland
d3455eee7d
fix: explain API access is unavailable in the desktop app (#2499)
Creating a developer token in the Wails build failed with a confusing
"Unhandled route: POST /api/unlock" error, because the desktop app does
not expose an HTTP API for the token to be used against. Hide the token
creation form in the desktop build and show an explanatory message
instead.

Fixes #2471

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 15:26:40 +07:00
Roland
9971aa1ac8
fix: update bark icon (#2497)
Replace the bark.jpg icon with the new light and dark SVG icons.

Fixes #2446

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 15:08:03 +07:00
Roland
43d37d75d1
fix: bump go-nostr to fix duplicate relay connections (#2496)
Picks up getAlby/go-nostr#6, which shares relay connections in
SimplePool when dials fail, closes relay websockets on pool close,
and closes previous subscriptions before re-subscribing on CLOSED.

The shared per-relay-URL connect backoff is now enabled by default
in the fork, so no hub-side pool option is needed
(nostr.WithPenaltyBox is deprecated).

Fixes #2481

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 14:50:08 +07:00
Roland
8a9ba49807
chore: bump node version in dockerfile to 22 (#2495) 2026-08-05 14:29:40 +07:00
Roland
4484046ff7
fix: use async LDK event polling to avoid polling delay (#2494)
Switch from polling node.NextEvent() every second to node.NextEventAsync(),
which parks the goroutine until an event arrives without blocking an OS
thread or an LDK thread, as LDK is migrating to async event handling.

Guard event handling with a mutex held by Shutdown() so in-flight handlers
finish before the node is stopped and destroyed, and drop events that
arrive after shutdown starts (LDK redelivers unhandled events on startup).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 23:14:30 +07:00
Roland
b051e5eb5f
fix: include payment hash when querying by payment request to ensure index is used (#2480) 2026-08-04 20:40:36 +07:00
dependabot[bot]
2bafad7a6c
build(deps): bump github.com/BoltzExchange/boltz-client/v2 from 2.12.0 to 2.12.5 (#2483)
build(deps): bump github.com/BoltzExchange/boltz-client/v2

Bumps [github.com/BoltzExchange/boltz-client/v2](https://github.com/BoltzExchange/boltz-client) from 2.12.0 to 2.12.5.
- [Release notes](https://github.com/BoltzExchange/boltz-client/releases)
- [Changelog](https://github.com/BoltzExchange/boltz-client/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BoltzExchange/boltz-client/compare/v2.12.0...v2.12.5)

---
updated-dependencies:
- dependency-name: github.com/BoltzExchange/boltz-client/v2
  dependency-version: 2.12.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 20:39:06 +07:00
dependabot[bot]
8f9d6f73f3
build(deps): bump github.com/labstack/echo/v4 from 4.15.2 to 4.15.4 (#2460)
Bumps [github.com/labstack/echo/v4](https://github.com/labstack/echo) from 4.15.2 to 4.15.4.
- [Release notes](https://github.com/labstack/echo/releases)
- [Changelog](https://github.com/labstack/echo/blob/v4.15.4/CHANGELOG.md)
- [Commits](https://github.com/labstack/echo/compare/v4.15.2...v4.15.4)

---
updated-dependencies:
- dependency-name: github.com/labstack/echo/v4
  dependency-version: 4.15.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 17:21:05 +07:00
dependabot[bot]
ec2ec911be
build(deps-dev): bump @commitlint/config-conventional from 20.5.0 to 21.2.0 in /frontend (#2487)
build(deps-dev): bump @commitlint/config-conventional in /frontend

Bumps [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) from 20.5.0 to 21.2.0.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v21.2.0/@commitlint/config-conventional)

---
updated-dependencies:
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 21.2.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 17:19:59 +07:00
Alchemist
816e0dda6e
feat: receive invoices to apps (#2466)
* feat: receive invoices to apps

* fix: handle cleared receive selector
2026-08-04 17:18:22 +07:00
saunter
417cb16d97
feat: refresh payment QR and status components (#2459)
* feat: refresh payment QR and status components

* fix: align payment success button spacing

* fix: invert payment QR colors in dark mode

* fix: address payment QR review feedback

* fix: flatten nested cards in payment review FixedFloat tiles

* chore: remove internal payment component review screen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: keep QR codes dark-on-light in dark mode

Inverted QR codes are unreadable by many scanner apps (e.g. Phoenix).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: René Aaron <rene@getalby.com>
Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 17:08:45 +07:00
dependabot[bot]
5b49783478
build(deps): bump golang.org/x/crypto from 0.52.0 to 0.54.0 (#2484)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.52.0 to 0.54.0.
- [Commits](https://github.com/golang/crypto/compare/v0.52.0...v0.54.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.54.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 16:33:30 +07:00
dependabot[bot]
98288463de
build(deps): bump gorm.io/gorm from 1.31.1 to 1.31.2 (#2482)
Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.31.1 to 1.31.2.
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.31.1...v1.31.2)

---
updated-dependencies:
- dependency-name: gorm.io/gorm
  dependency-version: 1.31.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 16:32:52 +07:00
dependabot[bot]
857f145784
build(deps): bump @fontsource-variable/inter from 5.2.8 to 5.3.0 in /frontend (#2485)
build(deps): bump @fontsource-variable/inter in /frontend

Bumps [@fontsource-variable/inter](https://github.com/fontsource/font-files/tree/HEAD/fonts/variable/inter) from 5.2.8 to 5.3.0.
- [Changelog](https://github.com/fontsource/font-files/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fontsource/font-files/commits/HEAD/fonts/variable/inter)

---
updated-dependencies:
- dependency-name: "@fontsource-variable/inter"
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 16:31:20 +07:00
dependabot[bot]
c99ca51ad5
build(deps): bump github.com/mattn/go-sqlite3 from 1.14.46 to 1.14.48 (#2486)
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.46 to 1.14.48.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.46...v1.14.48)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-sqlite3
  dependency-version: 1.14.48
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 16:29:20 +07:00
dependabot[bot]
fa03a9ba70
build(deps): bump @getalby/sdk from 7.0.0 to 8.0.3 in /frontend (#2488)
* build(deps): bump @getalby/sdk from 7.0.0 to 8.0.3 in /frontend

Bumps [@getalby/sdk](https://github.com/getAlby/js-sdk) from 7.0.0 to 8.0.3.
- [Release notes](https://github.com/getAlby/js-sdk/releases)
- [Commits](https://github.com/getAlby/js-sdk/compare/v7.0.0...v8.0.3)

---
updated-dependencies:
- dependency-name: "@getalby/sdk"
  dependency-version: 8.0.3
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: bump node version in workflows

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
2026-08-04 16:25:24 +07:00
hermes-alby
e0c173ae1e
docs: add security policy (#2491)
* docs: add security policy

* docs: link security policy from README

---------

Co-authored-by: Hermes Agent <hermes-agent@users.noreply.github.com>
2026-08-04 16:14:54 +07:00
hermes-alby
bf0ebe1f33
ci: support builds for fork pull requests (#2492)
ci: skip macOS signing for untrusted PRs

Co-authored-by: Hermes Agent <hermes-agent@users.noreply.github.com>
2026-08-04 16:14:30 +07:00
Roland
32af89bc8c
chore: bump rebalance fees to ensure payment succeeds (#2470)
Some checks failed
Multiplatform Docker build & push / build (push) Has been cancelled
Code quality - linting and typechecking / linting (push) Has been cancelled
Backend testing with Postgres / test-postgres (push) Has been cancelled
2026-07-29 14:31:18 +07:00
Peter
ce46a0f8a0
chore: update bark bindings to v0.12.1 (#2477)
Moves from bark 0.2.3 to 0.4.0, which changed the FFI surface:

- WalletOpen takes the network and a WalletOpenArgs, replacing WalletCreate
  and the separate RunDaemon call.
- Bolt11Invoice takes an optional anti-DoS token, unused here.
- LightningReceiveStatus is now LightningReceiveState, reporting progress
  via State rather than a PreimageRevealed bool.

Movements expose PaymentHash and sends expose a typed terminal state, so both
are read from those instead of the movement metadata JSON. A send movement that
is neither pending nor successful now resolves the SendPaymentSync waiter
instead of being ignored.
2026-07-29 14:25:25 +07:00
Roland
be17bc4e26
chore: replace alby mutinynet lsps2 with megalith mutinynet lsp (#2469)
Some checks are pending
Multiplatform Docker build & push / build (push) Waiting to run
Code quality - linting and typechecking / linting (push) Waiting to run
Backend testing with Postgres / test-postgres (push) Waiting to run
2026-07-27 19:54:37 +07:00
dependabot[bot]
afdb842a54
build(deps-dev): bump @tailwindcss/forms from 0.5.10 to 0.5.11 in /frontend (#2453)
build(deps-dev): bump @tailwindcss/forms in /frontend

Bumps [@tailwindcss/forms](https://github.com/tailwindlabs/tailwindcss-forms) from 0.5.10 to 0.5.11.
- [Release notes](https://github.com/tailwindlabs/tailwindcss-forms/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss-forms/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss-forms/compare/v0.5.10...v0.5.11)

---
updated-dependencies:
- dependency-name: "@tailwindcss/forms"
  dependency-version: 0.5.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 19:52:09 +07:00
Anshuman
754acfc9df
fix: update wave.space affiliate URL (#2475) 2026-07-27 19:51:07 +07:00
dependabot[bot]
005ee814a8
build(deps): bump github.com/lightningnetwork/lnd from 0.20.1-beta to 0.21.0-beta (#2402)
* build(deps): bump github.com/lightningnetwork/lnd

Bumps [github.com/lightningnetwork/lnd](https://github.com/lightningnetwork/lnd) from 0.20.1-beta to 0.21.0-beta.rc3.
- [Release notes](https://github.com/lightningnetwork/lnd/releases)
- [Changelog](https://github.com/lightningnetwork/lnd/blob/master/docs/release_branch_management.md)
- [Commits](https://github.com/lightningnetwork/lnd/compare/v0.20.1-beta...v0.21.0-beta.rc3)

---
updated-dependencies:
- dependency-name: github.com/lightningnetwork/lnd
  dependency-version: 0.21.0-beta.rc3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: remove unused LND wrapper interface and methods

* chore: bump LND to v0.21.0-beta

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Adithya Vardhan <imadithyavardhan@gmail.com>
2026-07-27 19:48:54 +07:00
dependabot[bot]
b6bce030d0
build(deps): bump github.com/mattn/go-sqlite3 from 1.14.45 to 1.14.46 (#2461)
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.45 to 1.14.46.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.45...v1.14.46)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-sqlite3
  dependency-version: 1.14.46
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 19:20:51 +07:00
dependabot[bot]
e6fcc3e3c1
build(deps-dev): bump typescript-eslint from 8.60.1 to 8.61.0 in /frontend (#2452)
build(deps-dev): bump typescript-eslint in /frontend

Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.60.1 to 8.61.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.61.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 19:19:40 +07:00
dependabot[bot]
270d23d273
build(deps-dev): bump @types/node from 25.8.0 to 25.9.3 in /frontend (#2450)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.8.0 to 25.9.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.9.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 19:18:50 +07:00
Michael Bumann
b70f40e42a
fix: require full access api key for swaps/mnemonic (#2473)
Some checks failed
Multiplatform Docker build & push / build (push) Has been cancelled
Code quality - linting and typechecking / linting (push) Has been cancelled
Backend testing with Postgres / test-postgres (push) Has been cancelled
the mnemonic could be considered a non read-only route because the
mnemonic could be used.
This moves this route to the full access group to require a full access
api key.
2026-07-14 21:50:37 +07:00
Alchemist
222031a97e
fix: soften channel routing warning (#2458)
Some checks failed
Multiplatform Docker build & push / build (push) Has been cancelled
Code quality - linting and typechecking / linting (push) Has been cancelled
Backend testing with Postgres / test-postgres (push) Has been cancelled
* fix: soften channel routing warning

* Update lnclient/ldk/ldk.go

Co-authored-by: Roland <33993199+rolznz@users.noreply.github.com>

---------

Co-authored-by: Roland <33993199+rolznz@users.noreply.github.com>
2026-06-19 15:48:18 +07:00
René Aaron
bf9c346a98
feat: use switch and improve copy on node settings page (#2441)
Some checks failed
Multiplatform Docker build & push / build (push) Has been cancelled
Code quality - linting and typechecking / linting (push) Has been cancelled
Backend testing with Postgres / test-postgres (push) Has been cancelled
* feat: use switch and improve copy on node settings page

* fix: allow disabling JIT channels when no liquidity source exists
2026-06-12 11:11:17 +07:00
Roland
b1c0d4eac8
fix: add fallback instructions if JIT LSP seems to be offline (#2443)
* fix: add fallback instructions if JIT LSP seems to be offline

* fix: duplicate invoice probe

* fix: properly check if lsps2 is enabled before setting jit request failed

* fix: don't render incorrect maximum receive amount if balances aren't loaded
2026-06-12 11:09:27 +07:00
Alchemist
a21c320fde
feat: simplify receive screen (#2426)
Some checks are pending
Multiplatform Docker build & push / build (push) Waiting to run
Code quality - linting and typechecking / linting (push) Waiting to run
Backend testing with Postgres / test-postgres (push) Waiting to run
* feat: simplify receive screen

* fix: use absolute receive routes

* chore: move other options to separate card, improve copy

* chore: also add accordion to receive invoice screen (for non-logged-in users)

* fix: JIT alert padding

* feat: explain other receive options with descriptive menu rows

* fix: move first channel jit alert outside of card

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
Co-authored-by: René Aaron <rene@twentyuno.net>
2026-06-11 19:20:35 +07:00
Roland
864c841c07
feat: add solvocard + add provider name to bitcoin card topup (#2440) 2026-06-11 15:56:52 +07:00
Roland
d7ebee5058
chore: add bhodl lsp as trusted 0 conf peer (#2439) 2026-06-11 14:48:26 +07:00
René Aaron
2f258138bc
fix: send referrer header on stories YouTube embed (#2435) 2026-06-11 13:17:27 +07:00
Anthonyushie
5249ebe7db
fix: add lightning tag to Freedomia card (#2437) 2026-06-11 13:15:39 +07:00
Roland
cc429c4553
chore: fix formatting (#2430)
Some checks are pending
Multiplatform Docker build & push / build (push) Waiting to run
Code quality - linting and typechecking / linting (push) Waiting to run
Backend testing with Postgres / test-postgres (push) Waiting to run
2026-06-10 14:51:37 +07:00
Adithya Vardhan
d8faf09109
fix: avoid mempool api failures during swaps by using boltz for fee and tx lookup (#2421)
* fix: avoid mempool api failures during swaps by using boltz for fee and tx lookup

* fix: ensure swap payment isn't made twice on refresh

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
2026-06-10 14:47:54 +07:00
frnandu
b55978d7bc
feat: just in time channels with lsps2 (#2275)
* feat: just in time channels with lsps2

* fix: clarify JIT receive channel fee

* fix: fees

* fix: fees 2

* fix: don't show low inbound when LSPS2 is active

* fix: remove the receive limit below the input if LSPS2 is being used

* fix: simplify

* fix: bring back fee % for outgoing

* fix: remove unneeded changes

* fix: typo

* fix: unneeded

* fix: don't show open first channel is LSPS2

* feat: clearer JIT channel fee copy on receive screen

* fix: add LSPS2 var info

* fix: don't duplicate JIT fee hint on create invoice form

* fix: make paymentDone a standard boolean

* fix: update to golang:1.26 in Dockerfile

* feat: read LSPS2 sources from channel suggestions, set minimum receive amount, update guide link

* docs:  update LDK_LSPS2_ADDRESSES to be used as an override

* fix: only show minimum jit receive amount on validation error

* fix: add more detail to receive error when receiving low amounts with jit

* fix: do not use JIT when user has public channels

* feat: add option to disable JIT

* fix: isTrusted check, add jit property to event

* fix: do not require node restart for toggling JIT

* chore: simplify JIT alert

* chore: add guide link on node settings JIT description

* feat: fetch the lsp2info to have access to params like minimum/maximum payment size

* refactor: share single learn-more link across JIT fee hint branches

* fix: remove variable amount invoice support

* fix: use lsps2info for min payment size and remove channelPeerSuggestion usage of minimumChannelSize

* fix: only do amount validation according to lsps2Info values if jit is enabled in settings

* feat: add jit first payment fee alert on receive via lightning address

* fix: remove unnecessary conditional

* fix: ensure at least one sat is left over when opening JIT channel

* chore: remove hardcoded suggestions

* chore: rename JIT enabled config variable

* fix: ui checks when JIT is disabled

* fix: amount input validation message

* fix: formatting

---------

Co-authored-by: anon <anon@anon.com>
Co-authored-by: saunter <68239231+stackingsaunter@users.noreply.github.com>
Co-authored-by: fmar <fmar@fmar>
Co-authored-by: René Aaron <rene@twentyuno.net>
Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
2026-06-10 14:47:07 +07:00
Roland
e5dc19ae68
feat: add readonly option for app store apps (#2415) 2026-06-10 13:17:13 +07:00
Roland
98e7d987bb
feat: pass selected provider to card topup app (#2416)
* feat: pass selected provider to card topup app

The Bitcoin Card Topup app (card.albylabs.com) now supports configuration
presets selected via a `provider` query param. Pass the provider chosen on
the Cards page through to the topup app's install link so its preset is
pre-applied, simplifying setup.

Closes #2384

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: move bitcoin card topup install guide component to a new file

* chore: remove accidentally committed worktree gitlinks

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: use more general copy for card topup app install guide

* fix: remove subtree commits

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:16:02 +07:00
Roland
c6f45e75e3
chore: update bark bindings to v0.8.0 (#2428)
* chore: update bark bindings to v0.8.0

* docs: update BARK_SERVER_ACCESS_TOKEN usage
2026-06-10 13:13:23 +07:00
René Aaron
3453b69a1c
feat: refine wallet empty states (#2382)
* fix: tighten inline empty state spacing on wallet pages

* chore: refine wallet empty states

- Replace placeholder icons (drum, link) with channel-specific icons (ZapIcon for lightning, BitcoinIcon for on-chain)
- Rewrite empty-state copy with warmer, less technical phrasing
- Drop redundant CTA (Receive button already sits above)
- Add subtle bg-accent surface to anchor the transactions section

* chore(empty-state): add variant prop, drop unused button props on wallet pages

* chore(empty-state): swap muted surface from bg-accent/40 to bg-muted

* chore(transactions): allow callers to override empty-state copy and icon

App transaction lists now show app-context messaging ('No transactions yet'
+ 'Payments made through this app will appear here.' with a ReceiptIcon)
instead of the wallet-specific lightning copy.

* chore(empty-state): drop unused 'none' variant

* chore(empty-state): default showButton to false

* chore(empty-state): drop showButton prop, derive from buttonText+buttonLink

* fix(empty-state): drop nested surface in app transactions card

Add 'none' variant and use it from AppTransactionList so the empty state
no longer renders a bg-muted box inside the already-bordered Card.

* fix(app-transactions): swap ReceiptIcon for ArrowDownUpIcon

ReceiptIcon renders a dollar sign — wrong for a bitcoin app.

* chore(empty-state): default variant to 'muted', CTA placeholders opt into 'dashed'

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
2026-06-10 13:00:43 +07:00
Roland
8fc5cb25b4
chore: prevent committing worktrees (#2427) 2026-06-10 12:39:51 +07:00
René Aaron
d1636cbcce
fix: reduce wallet balance/transaction polling interval to 10s (#2425)
Some checks are pending
Multiplatform Docker build & push / build (push) Waiting to run
Code quality - linting and typechecking / linting (push) Waiting to run
Backend testing with Postgres / test-postgres (push) Waiting to run
* fix: reduce wallet balance/transaction polling interval to 10s

The wallet dashboard polls /api/balances and /api/transactions every 3s
via SWR refreshInterval. For hubs left open in a browser tab, this produces
a high, continuous volume of identical requests around the clock with little
UX benefit, since SWR already revalidates on window focus.

Raise the interval for the balances and transactions-list hooks to 10s. The
single-transaction hook (used while waiting for a specific invoice to settle)
is intentionally left at 3s, where fast updates matter and polling is
short-lived.

* refactor: drop poll-interval comments, rationale moved to PR
2026-06-09 20:52:40 +07:00
Roland
2657c89aa8
feat: reframe AI agent inspiration tab around managing the hub (#2418)
* feat: reframe AI agent inspiration tab around managing the hub

Change the Node inspiration tab on the AI & Agents page to a Hub tab focused on managing Alby Hub itself (app connections, sub-wallets, budgets) rather than lightning channels. Channel/node prompts only show when the backend supports channel management.

Closes #2401

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: icon

* chore: name

* chore: remove stray .claude/worktrees gitlinks

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: René Aaron <rene@twentyuno.net>
2026-06-09 20:50:30 +07:00
Roland
10771b7c0a
chore: remove Bitrefill custom app in favor of standard NWC connection (#2420)
* chore: remove Bitrefill custom app in favor of standard NWC connection

Bitrefill now supports Nostr Wallet Connect directly, so the custom
embedded iframe app is no longer needed. Remove the internal Bitrefill
screen and route, convert the app store entry to a standard NWC
connectable app, and drop the embed.bitrefill.com frame-src CSP
exceptions from both the backend header and the dev Vite config.

Closes #2283

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: update bitrefill instructions and add mobile links

* chore: remove accidentally committed .claude/worktrees gitlinks

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 20:11:04 +07:00
Roland
4585ac6da3
chore: document branch naming conventions (#2417)
chore: document branch naming conventions in AGENTS.md

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:38:13 +07:00
Adithya Vardhan
ab22e60224
fix: align dockerfile go version with go.mod (#2424) 2026-06-09 19:23:39 +07:00
Adithya Vardhan
ee726c0df9
chore: remove claude worktrees folder (#2422) 2026-06-09 16:31:29 +07:00
Roland
91c634c85f
fix: show confirmation progress when opening public channel from LSP (#2419)
* fix: show confirmation progress when opening public channel from LSP

Public channels require 6 confirmations before they can be gossiped and
become usable (BOLT-7), but LDK accepts channels from trusted LSP peers
as 0-conf and reports ConfirmationsRequired as nil/0. As a result the
channel-opening screen rendered an indefinite blank loading spinner
instead of confirmation progress.

Override ConfirmationsRequired to 6 for announced channels so the UI
shows the "X/6 confirmations" progress card while the channel opens.

Fixes #2294

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: remove unnecessary link in comment

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:31:48 +07:00
dependabot[bot]
7b963e944f
build(deps): bump @base-ui/react from 1.4.1 to 1.5.0 in /frontend (#2405)
Bumps [@base-ui/react](https://github.com/mui/base-ui/tree/HEAD/packages/react) from 1.4.1 to 1.5.0.
- [Release notes](https://github.com/mui/base-ui/releases)
- [Changelog](https://github.com/mui/base-ui/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mui/base-ui/commits/v1.5.0/packages/react)

---
updated-dependencies:
- dependency-name: "@base-ui/react"
  dependency-version: 1.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 15:03:29 +07:00
dependabot[bot]
2215b04da6
build(deps-dev): bump eslint from 10.3.0 to 10.4.1 in /frontend (#2409)
Bumps [eslint](https://github.com/eslint/eslint) from 10.3.0 to 10.4.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.3.0...v10.4.1)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.4.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 15:01:12 +07:00
dependabot[bot]
2d4aab5bee
build(deps): bump tailwind-merge from 3.4.1 to 3.6.0 in /frontend (#2408)
Bumps [tailwind-merge](https://github.com/dcastil/tailwind-merge) from 3.4.1 to 3.6.0.
- [Release notes](https://github.com/dcastil/tailwind-merge/releases)
- [Commits](https://github.com/dcastil/tailwind-merge/compare/v3.4.1...v3.6.0)

---
updated-dependencies:
- dependency-name: tailwind-merge
  dependency-version: 3.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 14:29:27 +07:00
dependabot[bot]
6cacd12dcc
build(deps-dev): bump vite-plugin-pwa from 1.2.0 to 1.3.0 in /frontend (#2407)
Bumps [vite-plugin-pwa](https://github.com/vite-pwa/vite-plugin-pwa) from 1.2.0 to 1.3.0.
- [Release notes](https://github.com/vite-pwa/vite-plugin-pwa/releases)
- [Commits](https://github.com/vite-pwa/vite-plugin-pwa/compare/v1.2.0...v1.3.0)

---
updated-dependencies:
- dependency-name: vite-plugin-pwa
  dependency-version: 1.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 14:28:18 +07:00
dependabot[bot]
8c06706471
build(deps): bump github.com/btcsuite/btcd/chaincfg/chainhash from 1.1.0 to 1.2.0 (#2404)
build(deps): bump github.com/btcsuite/btcd/chaincfg/chainhash

Bumps [github.com/btcsuite/btcd/chaincfg/chainhash](https://github.com/btcsuite/btcd) from 1.1.0 to 1.2.0.
- [Release notes](https://github.com/btcsuite/btcd/releases)
- [Changelog](https://github.com/btcsuite/btcd/blob/master/CHANGES)
- [Commits](https://github.com/btcsuite/btcd/compare/btcutil/v1.1.0...btcutil/v1.2.0)

---
updated-dependencies:
- dependency-name: github.com/btcsuite/btcd/chaincfg/chainhash
  dependency-version: 1.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 14:10:33 +07:00
dependabot[bot]
ef1b4a088b
build(deps): bump github.com/mattn/go-sqlite3 from 1.14.44 to 1.14.45 (#2406)
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.44 to 1.14.45.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.44...v1.14.45)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-sqlite3
  dependency-version: 1.14.45
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 14:07:10 +07:00
dependabot[bot]
586c02251b
build(deps): bump github.com/go-gormigrate/gormigrate/v2 from 2.1.5 to 2.1.6 (#2403)
build(deps): bump github.com/go-gormigrate/gormigrate/v2

Bumps [github.com/go-gormigrate/gormigrate/v2](https://github.com/go-gormigrate/gormigrate) from 2.1.5 to 2.1.6.
- [Release notes](https://github.com/go-gormigrate/gormigrate/releases)
- [Changelog](https://github.com/go-gormigrate/gormigrate/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-gormigrate/gormigrate/compare/v2.1.5...v2.1.6)

---
updated-dependencies:
- dependency-name: github.com/go-gormigrate/gormigrate/v2
  dependency-version: 2.1.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 14:02:34 +07:00
Roland
64afc2227f
fix: add validation on MakeInvoice for zero and non-whole satoshi amounts (#2413) 2026-06-09 13:25:36 +07:00
daywalker90
6a15ebadad
feat: generate CLN invoice preimage ourselves to support sub-wallets (#2412)
Some checks are pending
Multiplatform Docker build & push / build (push) Waiting to run
Code quality - linting and typechecking / linting (push) Waiting to run
Backend testing with Postgres / test-postgres (push) Waiting to run
2026-06-08 21:20:51 +07:00
Alchemist
d2acf9ccb1
fix: ignore 404 when deleting a lightning address (#2323) (#2410)
fix: ignore 404 when deleting an already-deleted lightning address (#2323)

When a sub-wallet's lightning address was already removed on getalby.com, the DELETE call returns 404, which surfaced as a "Failed to delete lightning address" error toast. Treat 404 as success so deletion is idempotent.
2026-06-08 14:17:20 +07:00
Roland Bewick
5c23375c08 feat: add bark logger
Some checks failed
Multiplatform Docker build & push / build (push) Has been cancelled
Code quality - linting and typechecking / linting (push) Has been cancelled
Backend testing with Postgres / test-postgres (push) Has been cancelled
2026-06-05 16:40:43 +07:00
hermes-alby
a4dec48322
fix: update Bark FFI bindings (#2400)
fix: update bark ffi bindings

Co-authored-by: Hermes Agent <hermes-agent@users.noreply.github.com>
2026-06-05 13:01:04 +07:00
René Aaron
d2cd4a5ed2
fix: change first payment checklist item to receive-only (#2393) (#2394)
Some checks are pending
Multiplatform Docker build & push / build (push) Waiting to run
Code quality - linting and typechecking / linting (push) Waiting to run
Backend testing with Postgres / test-postgres (push) Waiting to run
Renames the onboarding checklist item from "Send or receive your
first payment" to "Receive your first payment" and marks it complete
when the wallet has a spendable balance (e.g. after opening an
outbound channel) in addition to having a transaction.
2026-06-04 17:45:42 +07:00
René Aaron
d36f050a11
feat: show progress while exporting transactions (#2396)
Exporting a wallet with thousands of transactions previously appeared to
do nothing — the handler silently paginated through every page with no
feedback. It now shows a loading toast that updates with the running
transaction count, and fetches 1000 transactions per page instead of 20
to cut round-trips.

Closes #2386
2026-06-04 17:36:47 +07:00
Roland
df67a2c24e
fix: link Windows CNG libs for bark FFI (#2397)
* fix: link Windows CNG libs for bark FFI

The bark FFI static library is built for the GNU/mingw target and embeds
Rust's getrandom/ring code, which references Windows CNG symbols such as
BCryptGenRandom. The upstream bark bindings only link -lbark_ffi_go, so the
mingw linker fails with "undefined reference to BCryptGenRandom".

cgo merges LDFLAGS across packages, so supply the missing Windows system
libraries from our own bark package without modifying the vendored module.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: move cgo directive out of Go doc comment for bark windows

The descriptive comment block was contiguous with the import "C" line, so
the entire block became the cgo C preamble and the C compiler tried to parse
the prose (unknown type name 'The', stray quotes/backticks). Separate the Go
documentation from the cgo preamble with a blank line and keep only the #cgo
directive in a /* */ block immediately preceding import "C".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: link Windows CNG libs for bark via extldflags

cgo #cgo LDFLAGS directives from our package are ordered before the bark
module on the link line, so the single-pass mingw linker discards -lbcrypt
before it sees the undefined BCryptGenRandom reference from libbark_ffi_go.a.

Append the Windows system libraries via -extldflags instead, which places
them after -lbark_ffi_go so the linker can resolve the symbols.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: link bark Windows CNG libs via CGO_LDFLAGS start-group

wails drops -ldflags=-extldflags, so the system libraries never reached the
external linker. Set them through CGO_LDFLAGS instead (read directly by cgo)
and wrap them with bark in a --start-group, so the linker re-scans the group
and resolves BCryptGenRandom regardless of library order.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: gate bark backend to platforms with prebuilt FFI libs

The bark FFI bindings ship no native library for 32-bit ARM Linux, so the
armv6 build failed to link bark's own FFI symbols. Constrain the real bark
implementation to bark's supported platforms (darwin/linux amd64+arm64,
windows amd64) and add a stub for everything else that returns an
"unsupported" error if the bark backend is selected at runtime.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 17:20:04 +07:00
dependabot[bot]
f25ce761ac
build(deps): bump github.com/btcsuite/btcd/btcec/v2 from 2.3.6 to 2.5.0 (#2347)
Bumps [github.com/btcsuite/btcd/btcec/v2](https://github.com/btcsuite/btcd) from 2.3.6 to 2.5.0.
- [Release notes](https://github.com/btcsuite/btcd/releases)
- [Changelog](https://github.com/btcsuite/btcd/blob/master/CHANGES)
- [Commits](https://github.com/btcsuite/btcd/compare/btcec/v2.3.6...btcec/v2.5.0)

---
updated-dependencies:
- dependency-name: github.com/btcsuite/btcd/btcec/v2
  dependency-version: 2.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 17:06:31 +07:00
hermes-alby
804075faff
ci: align Wails CLI version (#2398)
Co-authored-by: Hermes Agent <hermes-agent@users.noreply.github.com>
2026-06-04 16:17:43 +07:00
dependabot[bot]
1f647482a3
build(deps): bump golang.org/x/crypto from 0.50.0 to 0.52.0 (#2348)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.50.0 to 0.52.0.
- [Commits](https://github.com/golang/crypto/compare/v0.50.0...v0.52.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.51.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 16:09:59 +07:00
René Aaron
2af517d577
chore: remove redundant Alby Account, Go and Extension home cards (#2392)
These promo cards are now covered by the Stories widget (Alby account,
Alby Go and Alby Extension stories are live in the stories feed), so the
standalone home widgets are redundant.

Supersedes #2175, which patched the older inline cards that were since
refactored into widgets.
2026-06-04 16:05:12 +07:00
dependabot[bot]
fa3d516d02
build(deps): bump github.com/BoltzExchange/boltz-client/v2 from 2.11.3 to 2.12.0 (#2349)
build(deps): bump github.com/BoltzExchange/boltz-client/v2

Bumps [github.com/BoltzExchange/boltz-client/v2](https://github.com/BoltzExchange/boltz-client) from 2.11.3 to 2.12.0.
- [Release notes](https://github.com/BoltzExchange/boltz-client/releases)
- [Changelog](https://github.com/BoltzExchange/boltz-client/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BoltzExchange/boltz-client/compare/v2.11.3...v2.12.0)

---
updated-dependencies:
- dependency-name: github.com/BoltzExchange/boltz-client/v2
  dependency-version: 2.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 16:03:38 +07:00
dependabot[bot]
d03d70c6c9
build(deps): bump github.com/btcsuite/btcd/btcutil from 1.1.6 to 1.2.0 (#2350)
Bumps [github.com/btcsuite/btcd/btcutil](https://github.com/btcsuite/btcd) from 1.1.6 to 1.2.0.
- [Release notes](https://github.com/btcsuite/btcd/releases)
- [Changelog](https://github.com/btcsuite/btcd/blob/master/CHANGES)
- [Commits](https://github.com/btcsuite/btcd/compare/btcutil/v1.1.6...btcutil/v1.2.0)

---
updated-dependencies:
- dependency-name: github.com/btcsuite/btcd/btcutil
  dependency-version: 1.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 16:01:00 +07:00
dependabot[bot]
e042e01fb7
build(deps-dev): bump typescript-eslint from 8.59.2 to 8.60.1 in /frontend (#2352)
build(deps-dev): bump typescript-eslint in /frontend

Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.59.2 to 8.60.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.60.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.59.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 15:25:36 +07:00
dependabot[bot]
feb4aef935
build(deps-dev): bump @vitejs/plugin-react-swc from 4.3.0 to 4.3.1 in /frontend (#2353)
build(deps-dev): bump @vitejs/plugin-react-swc in /frontend

Bumps [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react-swc) from 4.3.0 to 4.3.1.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/v4.3.1/packages/plugin-react-swc)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react-swc"
  dependency-version: 4.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 15:13:52 +07:00
dependabot[bot]
89deef164d
build(deps-dev): bump @types/node from 25.6.2 to 25.8.0 in /frontend (#2355)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.6.2 to 25.8.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 14:52:39 +07:00
dependabot[bot]
94332c617d
build(deps-dev): bump tailwindcss from 4.2.4 to 4.3.0 in /frontend (#2356)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) from 4.2.4 to 4.3.0.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.0/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-version: 4.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 14:48:54 +07:00
Michael Bumann
bc49cb7d33
fix: stop retrying NIP47 info publish for deleted apps (#2391)
The NIP47 info publish queue re-enqueued every failed publish with an
incrementing backoff and no terminal condition. When an app connection
was deleted, PublishNip47Info fails the `db.First(&app, appId)` lookup
with gorm.ErrRecordNotFound on every attempt, so the item was retried
forever (observed as a steady stream of "Failed to publish NIP47 info
from queue" errors from affected instances).

Drop the queue item when the app no longer exists instead of requeuing.
All other errors (offline relay, timeouts) still retry with backoff.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 13:29:05 +07:00
Roland
84e56a23db
feat: bark backend (#2374)
* feat: bark backend

* fix: use real preimage

* fix: balance shows as 0 on startup after hours offline period

* chore: add bark-specific warnings to setup security page

* chore: update setup security page

* chore: use notifications instead of polling

* feat: bark sub-wallet support

* fix: tests

* feat: add env variables for mainnet support

* chore: add custom node commands for debugging, log next notification

* fix: notification handling

* fix: fees for outgoing payments, pending payment handling, decrease required tx confirmations

* chore: bump bark version

* chore: use better bark image

* fix: back button logic and imported mnemonic state in onboarding flow

* chore: update bark copy on security page

* chore: improve copy for first transaction checklist item

* fix: backup copy based on backend

* fix: remove unused/unnecessary code
2026-06-04 11:57:29 +07:00
René Aaron
56b1ce895f
feat: add Pi agent to AI page (#2388)
Some checks failed
Multiplatform Docker build & push / build (push) Has been cancelled
Code quality - linting and typechecking / linting (push) Has been cancelled
Backend testing with Postgres / test-postgres (push) Has been cancelled
Closes #2387
2026-06-02 17:15:29 +07:00
saunter
23dccc6c6f
feat: stories (#2172)
* feat: integrate Stories widget with backend endpoint

Add stories endpoint plumbing for HTTP and Wails, wire the Home Stories card
to fetch from /api/alby/stories, and keep it first in the right column.

Made-with: Cursor

* feat(home): story modal CTAs and preview fallback

- Add contextual actions in the story dialog (update hub with version,
  open Alby Go in-app, install extension) keyed by kind or title
- Use preview stories when the stories API request fails
- Pass hub version from useInfo into the update link

Made-with: Cursor

* feat(stories): polish modal, drop preview fallback

- Widen modal and put video edge-to-edge with overlay close button
- Drop verbose header and 'Watch on YouTube' button
- Remove previewStories fallback so widget hides until upstream API ships
- Tighten title line-height

* feat(stories): render cta from API instead of mapping by kind

Move CTA copy and URLs into the API response. Hub renders story.cta
directly, so adding new story types no longer requires a hub release.

* chore(csp): allow cdn.getalby-assets.com in img-src

* feat(stories): bump avatar size and add ring gap

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(stories): post-review cleanups

- Use react-router Link for in-tab CTA instead of plain <a>.
- Drop redundant www.youtube.com from frame-src (embeds always go through nocookie).
- Tighten stories endpoint status check from >= 300 to >= 400.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(stories): address CodeRabbit feedback

- Switch StoriesWidget to useSWR + swrFetcher (project convention).
- Guard story iframe with isYouTubeUrl so non-YouTube urls never embed.
- Wrap GetStories errors with fmt.Errorf("...: %w", err).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(stories): drop isYouTubeUrl guard

Stories are curated and always YouTube; the runtime check was
redundant. CSP frame-src still constrains the iframe source.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(stories): drop getYouTubeEmbedUrl, embed videoUrl as-is

The Alby API now sends canonical youtube-nocookie embed URLs with
autoplay/rel query strings (getAlby/getalby.com#2568), so the
runtime normalization is no longer needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(stories): use w-16 instead of arbitrary w-[73px]

Match the avatar's size token; no magic numbers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(stories): take videoId from API and assemble embed url locally

Pairs with getAlby/getalby.com#2568. The API now sends just the
YouTube videoId; the hub composes the canonical embed URL so the
domain/query-string format stays in one place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(stories): treat 3xx as non-success, matching file convention

The other status checks in alby_oauth_service.go all use >= 300;
align GetStories so redirects don't slip through.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(stories): move viewed-storage key to constants, widen story button

Address review feedback:
- Centralize the localStorage key for viewed stories in localStorageKeys
  alongside the other keys.
- Widen the story button from w-16 to w-20 so "Alby Extension" fits on
  one line and matches the other titles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(stories): bump story button to w-24 so titles fit one line

w-20 still wrapped "Alby Extension"; w-24 fits all current titles
without truncation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "chore(stories): bump story button to w-24 so titles fit one line"

This reverts commit 0b47438f50.

* chore(stories): split title words onto separate lines

Reserve two lines for every story title so avatars align regardless of
title length.

* chore(stories): align homeStoriesViewed key with sibling pattern

* chore(stories): fit titles on one line

* chore(stories): widen story button to w-21 for one-line titles

---------

Co-authored-by: René Aaron <rene@twentyuno.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 10:50:15 +02:00
René Aaron
81b1b2f695
fix: prevent scrollbar on incoming capacity page (#2383) 2026-05-29 14:11:09 +02:00
saunter
efee2b85e8
feat: new currency input (#2320)
* feat: add currency input to receive flows

* feat: use currency input in send flows (#2321)

* feat: use currency input in send flows

* feat: use currency input in swap flows (#2322)

* feat: use currency input in swap flows

* feat: add BTC denomination toggle to currency input (#2367)

* feat: add BTC denomination toggle to currency input

* fix: auto switch decimal bitcoin input to BTC

* chore: address feedback on currency input field (#2371)

* chore: address feedback on currency input field

* feat: make currency input units clickable

* fix: separate currency and unit click targets

* fix: remove persistent unit toggle highlight

* fix: tighten currency input unit spacing

* fix: make alternate bitcoin amount clickable

* fix: align context amount unit spacing

---------

Co-authored-by: saunter <68239231+stackingsaunter@users.noreply.github.com>

---------

Co-authored-by: Roland <33993199+rolznz@users.noreply.github.com>

---------

Co-authored-by: Roland <33993199+rolznz@users.noreply.github.com>

---------

Co-authored-by: Roland <33993199+rolznz@users.noreply.github.com>

* fix: tab highlight on currency input field buttons

* fix: undo incorrect copy change

* chore: undo unrelated change

* fix: limit min/max validation to 2 decimal places

* fix: input max amounts and context rows based on whether node has channel management

* fix: rename spending balance to lightning balance

* fix: rename spending balance to lightning balance

* fix: re-add anchor reserve alert to swap page

* fix: remove autocomplete from currency input field

* fix: number of decimals in getModeBound

* fix: remove important tailwind modifier

---------

Co-authored-by: Roland <33993199+rolznz@users.noreply.github.com>
Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
2026-05-27 23:11:45 +07:00
René Aaron
48ab9efe2c chore: use redotpay referral link on cards page 2026-05-26 14:21:03 +02:00
Roland
318c622887
chore: simplify kyc info on cards page (#2378) 2026-05-26 09:38:25 +02:00
René Aaron
e29fe81eb4
fix: stop prompting for bitcoin: protocol handler on every load (#2369)
* fix: stop prompting for bitcoin: protocol handler on every load

Browsers re-show the registerProtocolHandler prompt every time it is
called if the user dismissed (X'd) the previous one without explicitly
accepting or denying. Gate the call with sessionStorage so we ask at
most once per browser session.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: guard sessionStorage access against restricted/private modes

sessionStorage.getItem and setItem can throw in private browsing or
restricted storage modes. Move both inside the existing try/catch so an
exception doesn't break the hook.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: simplify protocol handler session flag to a boolean

sessionStorage is tab-scoped and ephemeral, so comparing the stored
handler URL gains nothing over a plain truthy check.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 20:38:29 +05:30
Roland
e3373474f8
chore: remove json tags from lnclient models (#2375)
* chore: remove json tags from lnclient models

these should not be passed through the API directly

* fix: properly return not implemented errors

* fix: json tags on TLVRecord
2026-05-25 20:26:07 +05:30
Roland
4053a4a722
Fix: card page mobile interface (#2377)
* chore: remove unnecessary regions

* chore: improve cards mobile UI, remove extra regions from RedotPay

* fix: add visit button for mobile provider cards rather than the whole card opening the provider url

* chore(cards): use Select for the region filter everywhere

Drop the mobile-only Select / desktop-only ToggleGroup split. Using a
single Select for the region filter across both viewports removes the
duplicated component, keeps the filter bar a single row at all widths,
and lets the feature toggles stay as pills (those carry icons and read
as a row of binary on/off filters).

* chore(cards): cluster Apple/Google Pay icons next to region badges

Drop the ml-auto on the mobile ProviderCard's pay icons so they sit
right after the region badges instead of floating at the far-right
edge with a large gap. Reads as a single group of card properties.

* chore(cards): rework mobile card spacing + use w-40 for region select

- Bump card padding to p-5 for more breathing room.
- Add a subtle border-t before the facts grid so KYC/Time/Cost/Fees
  read as a separate block from the header + regions row.
- Widen the facts grid's vertical gap (gap-y-4) so labels don't sit
  right against the value of the previous row.
- More space before the Visit CTA (mt-6) so it reads as a primary
  action, not a fifth fact.
- Swap w-[160px] on the region Select for w-40 per project Tailwind
  conventions (CodeRabbit nit on PR #2377).

---------

Co-authored-by: René Aaron <rene@twentyuno.net>
2026-05-24 22:44:26 +02:00
René Aaron
40dcf11a2b
feat(cards): provider directory routed through the standard app-install flow (#2366)
* feat: add cards directory page with provider listings

Adds a dedicated Cards screen that surfaces crypto debit card
providers users can top up from their Alby Hub balance, with region
and feature filters and a fees comparison table.

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(cards): connect-card flow with NWC top-up link

Iterates on the cards directory page (#ae087d0b) to wire up the full
connect-card flow described with the bitcoin-card-topup PWA at
card.albylabs.com.

- AI-style hero with three steps: Get a card → Connect it → 1-click top-ups
- Trimmed provider list to RedotPay / 2fiat / Freedomia (the providers
  we've validated end-to-end)
- New "Time to get" column so users see physical vs virtual at a glance
- Renamed "Add card" → "Connect card" everywhere; submit mints a real
  NWC connection via createApp (same pattern as the AI page) with
  scopes for the topup app (get_info / get_balance / list_transactions /
  lookup_invoice / make_invoice / pay_invoice / notifications)
- CardCreatedDialog shown once at creation with QR code + bookmarkable
  top-up link of the form

    https://card.albylabs.com/#label=...&address=0x...&chainId=42161&currency=USDC&nwc=<pairing-uri>

  Includes prominent "save this link — you won't see it again" warning
  (Alert with warning variant) and "scan with your phone's camera app
  (this is a URL, not a Lightning invoice)" caption under the QR
- Saved card tiles link to /apps/:appId so users get back to the NWC
  connection detail; no separate top-up affordance from the hub
- useUserCards hook persists cards in localStorage with appId pointing
  at the NWC connection; ready to migrate to deriving the list from
  /api/apps filtered by metadata.app_store_app_id = "bitcoin-card-topup"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(cards): derive cards from /api/apps, polish

Replaces the localStorage card store with a derivation from /api/apps
filtered by metadata.app_store_app_id = "bitcoin-card-topup". Card
provider/destination/chain/currency now ride on the NWC app's metadata,
so cards survive across devices, reloads and DB backups, and "forget
card" flows through the existing /apps/:id delete affordance.

- Gate /cards through DefaultRedirect so a locked hub redirects to
  /unlock (matches /wallet, /apps, etc.)
- Hub-generated top-up link now uses #?... prefix so bitcoin-connect's
  parser branches to the URLSearchParams path (works in all cases vs.
  the bare #... which mis-parsed for some users)
- Remove EmptyCards empty state (the Connect card button in the page
  header is the sole entry point)
- Drop the "Experimental" filter and badge — too small a catalog for
  it to be useful, and the toggle behavior confused users
- Replace "Top up via" column with "Card cost" (more decision-relevant)
- Verified all provider data from each provider's site; updated
  - RedotPay: KYC Full (not Light), regions add US/UK, fees ~2.2% + FX
  - 2fiat: Mastercard (not Visa), Apple Pay + Google Pay supported, KYC
    None (not Light), card cost $50, fees ~6.8%
  - Freedomia: Google Pay supported, card cost $5–30/mo subscription,
    fees 1.3–4.3%
- Add a hoverable info-icon tooltip on the "None" KYC badge so privacy-
  focused users keep the signal they want while cautious users see the
  merchant-of-record-fragility caveat
- Field label "Destination address" → "Top-up address" (matches what
  the user pastes from their provider)
- Connect dialog copy de-emdashed
- Various smaller copy/layout tweaks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(cards): tidy provider tiles, filter, and column header

- Switch Freedomia tile bg to bg-orange-500 to match the brand's
  orange F logo
- Drop the upscaled watermark logo from card-tile backgrounds for a
  cleaner surface
- Hide filter toggles whose criterion no provider satisfies, so the
  filter bar only offers actionable options
- Rename "Mobile pay" column header to "Mobile" — the icons already
  identify Apple/Google Pay

Co-Authored-By: Claude <noreply@anthropic.com>

* chore(cards): drop unused provider logos

We trimmed the catalog to RedotPay/2fiat/Freedomia in an earlier
commit; the other 8 PNGs were left behind. Remove them to keep the
asset directory tight.

Co-Authored-By: Claude <noreply@anthropic.com>

* chore(cards): downgrade RedotPay KYC from Full to Light

RedotPay only requires ID verification — no proof of address, employer
details, or source-of-funds questions. "Full" overstates it and may
deter users from even trying.

Adds a Light KYC tooltip alongside the existing None one so the
distinction is visible at a glance.

* chore(cards): tighten copy and mobile layout

- CardCreatedDialog: drop the "regular URL, not a Lightning invoice"
  caveat and replace with actionable "save it as an app to your
  homescreen" guidance.
- Hide the QR + scan instruction on mobile — the user is already on
  their phone, so scanning their own screen is nonsense.
- Provider table: switch wrapper from overflow-hidden to overflow-x-auto
  and set a 720px min-width so 9 columns scroll horizontally on narrow
  viewports instead of cramming/clipping.

* feat(cards): list Bringin and wavecard

Both already ship as suggested apps with their own NWC pairing flows, so
the table row's action arrow links to /appstore/<id> rather than the
stablecoin connect-card dialog. Marked via a new optional appStoreId
flag on Provider; the Connect-card dropdown filters these out so the
top-up form never offers a chain/currency for cards that don't need one.
Reuses the existing suggested-apps logos to avoid duplicating assets.

* chore(cards): retitle 3rd hero step to highlight top-up speed

"Top up in seconds — your card is funded in under a minute, ready to
spend on the go" lands the speed promise harder than "1-click top-ups"
did, and frames the value as on-the-go spending rather than mechanics.

* feat(cards): route app-store cards through the Connect dialog

Bringin and wavecard are now in the provider dropdown. Picking either
hides the address/network/currency fields and replaces the primary
button with an "Open setup guide" link to /appstore/<id>, where their
own NWC pairing flow lives. Keeps a single entry point for "connect a
card" without forcing the stablecoin form onto cards that don't use it.

Also tightens the hero subtitle to lead with the speed promise.

* chore(cards): link app-store cards straight to /apps/new

Skip the app-store detail page — /apps/new?app=<id> drops users into
the connection flow with the right app preselected, which is what they
actually wanted.

* chore(cards): correct Bringin and wavecard table data

Verified against bringin.app/bitcoin-debit-cards and wave.space/card:

- Both offer physical *and* virtual cards (added "Both" to cardType).
- wave.space is EEA-only for issuance (card itself is accepted globally)
  — regions changed from Global to EU.
- Direct Apple Pay isn't live for either; both currently work via Curve,
  which is too indirect to claim native Apple Pay support.
- Issuance is not free: Bringin charges a €3.49/mo subscription that
  bundles both cards; wave.space charges €2.99 virtual / €29.99 physical
  one-time.
- Conversion fees are 1% + ~0.5% LP spread for both, not flat ~1%.
- Bringin URL fixed to bringin.app (they migrated from bringin.xyz);
  wave.space URL points at the card landing page.

Doesn't change the dialog routing — picking Bringin/wavecard still drops
the user into /apps/new?app=<id> for the NWC pairing.

* chore(cards): tighten created-dialog copy

- Title and subtitle now describe the link instead of warning.
- Alert highlights the device-specific action ("save it on the phone
  you'll top up from") and drops the redundant secret-recovery prose.
- QR caption mentions bookmark as an alternative to home-screen install.

* refactor(cards): route every provider through the standard app flow

- Add a bitcoin-card-topup app store entry pointing to card.albylabs.com
  with a "visit + add to home screen + enter card details" install guide.
- Wire RedotPay and Freedomia to bitcoin-card-topup, and 2fiat to its
  existing app store entry. Bringin and wavespace already had theirs.
- Drop the custom Connect card dialog, address/network/currency form,
  and the one-shot CardCreatedDialog with embedded NWC link. Card config
  is now collected inside the topup app itself.
- Drop the "Your card connections" section and the useUserCards hook —
  connected cards show up in the standard /apps list like any other app.

* feat(cards): add Connect card picker dialog + clickable rows

- Bring back the Connect card button in the header; opens a lightweight
  provider-picker dialog where each tile routes straight to
  /apps/new?app=<appStoreId>.
- Drop the table's action column entirely; the whole provider row is now
  clickable and opens the provider's website in a new tab. This separates
  discovery (row click → learn more) from action (Connect card → setup).

* feat(cards): add Other card option + broaden install wording

- Add an "Other card" tile to the Connect card picker (dashed border,
  generic credit-card icon). Routes to /apps/new?app=bitcoin-card-topup
  so anyone holding a USDC/USDT card not in the listed providers can
  still set up the topup flow.
- Reword the install guide from "phone you'll top up from" to "device" —
  the topup app works equally well on a tablet or any browser.

* chore(cards): fix bitcoin-card-topup logo + broaden copy to "any crypto"

- Swap the placeholder 2fiat logo for the Alby logo (alby.png).
- Drop USDC/USDT specifics from the app description, extended
  description, and the picker dialog's "Other card" tile copy. From the
  user's perspective the topup app just takes a crypto card.

* chore(cards): use bitcoin-card-topup's own PWA icon as the app logo

Copy bitcoin-card-topup/public/shortcut-icon.png (the topup PWA's home-
screen icon) into suggested-apps/ and point the bitcoin-card-topup app
store entry at it, replacing the Alby-logo placeholder.

* fix(apps): align Connect-to-app header logo size with the appstore page

NewApp.tsx rendered the app logo at w-12 h-12 (48px) while
AppStoreDetailHeader uses w-14 h-14 (56px) — the logo visually jumps
between /apps/new?app=<id> and /appstore/<id>. Unify on w-14 h-14.

* fix(appstore): use AppHeader's standard icon/description slots

AppStoreDetailHeader was rendering the logo + title + description inside
a custom flex container nested into AppHeader's title prop, which gave
the icon a different vertical alignment than every other AppHeader use.
Pass them via the icon and description props instead so the header
layout stays consistent with /apps/new and the rest of the app.

* chore(cards): use Freedomia affiliate URL

* chore: add affiliate links

* fix: always show global cards

* fix: make 2fiat lightning native

* chore: update card instructions

* chore: optimize bitcoin card topup app image

* chore: improve bitcoin card topup copy

* chore: add card events, fix http service event endpoint url

* chore(cards): drop redundant 'Get a card' section heading

* chore(cards): tighten hero subtext + 3-step copy, hide filter footer when inactive

- Hero subtext drops the "one click" claim that's no longer accurate
  post-refactor and the em-dash.
- 3-step descriptions reworded for length and fix "Setup" -> "Set up";
  bottom step matches the "in seconds" heading instead of saying
  "under a minute".
- "Showing N of M providers" footer is hidden unless a filter is
  active — silent when there's nothing to clarify.

* chore(cards): apply Alby button gradient + brand stroke to hero card

Use the same vertical white→cream highlight and 1px #ffdf6f stroke that
alby.css applies to default primary buttons, so the hero card visual
reads as part of the same theme family instead of an isolated panel.

* chore(cards): reframe step 2 around the top-up link + drop hero gradient

- Step 2 title shifts from "Connect it" to "Get a top-up link" so the
  deliverable (a saveable link) is what users see, matching what
  actually happens at that step post-refactor.
- Drop the white→cream gradient overlay from the hero card; the flat
  yellow with the existing shadow is the look we want.

* chore: change wave.space kyc level

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
2026-05-24 12:22:48 +07:00
René Aaron
d58b9f3ead
fix: show BOLT-12 offer button for CLN backend (#2360)
* fix: show BOLT-12 offer button for CLN backend

The CLN backend implements MakeOffer and the /api/offers endpoint is
backend-agnostic, but the UI only surfaced the "Lightning Offer" button
when backendType === "LDK", leaving CLN users without a way to reach
the flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: gate BOLT-12 offer UI on supportsBolt12 from useInfo

Surface a supportsBolt12 capability on the info response (true for LDK
and CLN) so the frontend stops hardcoding backend-type checks for the
BOLT-12 offer button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:10:02 +07:00
Roland
3bb36611ae
fix: error log when failing to request delete lightning address endpoint (#2338) 2026-05-23 15:30:13 +07:00
Roland
ee001d16b4
fix: missing error handling in create connection controller (#2335) 2026-05-23 15:29:35 +07:00
hermes-alby
dd04326f76
chore: remove shut down suggested apps (#2372)
Co-authored-by: Hermes Agent <hermes-agent@users.noreply.github.com>
2026-05-23 15:26:54 +07:00
Roland
4a7123a4b9
Feat: satora rebrand (#2376)
* fix(appstore): rebrand LendaSwap as Satora

* chore: also show the previous name on the app card

---------

Co-authored-by: Lucas Soriano del Pino <lucas_soriano@fastmail.com>
2026-05-23 15:26:33 +07:00
daywalker90
12a3b114ff
CLN balance offer fix and makeoffer fix (#2373)
* fix: don't use deprecated balance fields for CLN

* fix: add missing amount for makeoffer for CLN
2026-05-23 13:55:44 +07:00
René Aaron
3a2f935a75
feat: add Hermes agent to AI page (#2358)
Closes #2357

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 13:04:35 +02:00
Roland
a0d3da2803
fix: slow app deletion due to unnecessary key derivation (#2342)
* fix: slow app deletion due to unnecessary key derivation

* chore: fix comment grammar

Co-authored-by: Adithya Vardhan <imadithyavardhan@gmail.com>

---------

Co-authored-by: Adithya Vardhan <imadithyavardhan@gmail.com>
2026-05-14 20:34:59 +07:00
Roland
55d665db74
fix: check lnclient is nil in GetPermittedMethods (#2343)
this caused a possible panic on shutdown
2026-05-14 17:07:37 +05:30
Roland
162965300e
docs: add README for keys (#2333) 2026-05-11 19:57:50 +05:30
Adithya Vardhan
861662664d
chore: bump react-dom to v19.2.6 to match react version (#2332) 2026-05-11 14:46:30 +05:30
dependabot[bot]
e566f6ac37
build(deps-dev): bump @types/node from 25.6.0 to 25.6.2 in /frontend (#2326)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.6.0 to 25.6.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.6.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 14:43:35 +07:00
dependabot[bot]
186f72715c
build(deps): bump tw-animate-css from 1.3.6 to 1.4.0 in /frontend (#2328)
Bumps [tw-animate-css](https://github.com/Wombosvideo/tw-animate-css) from 1.3.6 to 1.4.0.
- [Release notes](https://github.com/Wombosvideo/tw-animate-css/releases)
- [Commits](https://github.com/Wombosvideo/tw-animate-css/compare/v1.3.6...v1.4.0)

---
updated-dependencies:
- dependency-name: tw-animate-css
  dependency-version: 1.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 14:32:21 +07:00
dependabot[bot]
2e72407327
build(deps-dev): bump typescript-eslint from 8.59.1 to 8.59.2 in /frontend (#2329)
build(deps-dev): bump typescript-eslint in /frontend

Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.59.1 to 8.59.2.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.59.2/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.59.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 14:23:18 +07:00
dependabot[bot]
b9b9882fe1
build(deps): bump react from 19.2.5 to 19.2.6 in /frontend (#2330)
Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) from 19.2.5 to 19.2.6.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.6/packages/react)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 14:18:40 +07:00
dependabot[bot]
3458eb9503
build(deps): bump github.com/labstack/echo/v4 from 4.15.1 to 4.15.2 (#2325)
Bumps [github.com/labstack/echo/v4](https://github.com/labstack/echo) from 4.15.1 to 4.15.2.
- [Release notes](https://github.com/labstack/echo/releases)
- [Changelog](https://github.com/labstack/echo/blob/v4.15.2/CHANGELOG.md)
- [Commits](https://github.com/labstack/echo/compare/v4.15.1...v4.15.2)

---
updated-dependencies:
- dependency-name: github.com/labstack/echo/v4
  dependency-version: 4.15.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 14:17:05 +07:00
Roland
5b3f1bef9d
fix: update to alby go-nostr fork which reconnects after receiving CLOSED message (#2331) 2026-05-11 12:34:56 +07:00
Adithya Vardhan
9f8da1a003
chore: add new gpg key for im-adithya (#2318)
Adds my GPG key for signing releases
2026-05-07 17:56:48 +05:30
Adithya Vardhan
04d3712cb2
chore: add new gpg key for im-adithya 2026-05-07 17:51:38 +05:30
Roland
d5bdb435b5
feat: show last transaction amount on recently used apps (#2315)
* feat: show last transaction amount on recently used apps

* fix: icon size
2026-05-07 17:31:06 +05:30
Adithya Vardhan
261a668a2f
fix: allow creating apps without requiring superuser access (#2316) 2026-05-07 18:56:31 +07:00
Adithya Vardhan
a8f2c87122
fix: position and size sidebar hint x icon (#2313)
* fix: position and size sidebar hint x icon

* chore: remove unused name prop
2026-05-07 17:13:26 +05:30
Roland
d7481ee71a
fix: incorrect colors in close channel dialog (#2312)
* fix: incorrect colors in close channel dialog

* chore: undo copy change

* fix: text/button colors
2026-05-07 15:00:50 +05:30
Roland
5d71064b94
fix: allow pasting BIP-21 URL into the wallet send input (#2310)
* fix: allow pasting BIP-21 URL into the wallet send input

* fix: remove replace in navigation from bip21 link
2026-05-07 14:40:28 +05:30
Roland
fb7986dd16
fix: link colors in themes with brighter primary colors (#2311)
* fix: link colors in themes with brighter primary colors

* fix: change link button variant color
2026-05-07 14:22:42 +07:00
Roland
d8d7452e75
feat: add swap in button to insufficient lightning balance alert (#2309)
* feat: add swap in button to insufficient lightning balance alert

* fix: casing
2026-05-07 12:45:58 +05:30
Adithya Vardhan
011c030512
fix: replace payment flow intermediate screens in browser history (#2308)
* fix: replace payment flow intermediate screens in browser history

* fix: add break all to ln address container

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
2026-05-07 13:46:56 +07:00
René Aaron
bb9249c767
feat: replace wallet balance toggle with Lightning / On-chain tabs (#2306)
* feat: prototype balance switcher variants on wallet screen

Adds two switcher styles (icon-only segmented control and tab-style
control) alongside the original "Spending Balance ⇅" toggle, with a
floating dev-only variant toggle for side-by-side comparison.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: replace balance toggle with tab switcher

Drops the icon-only and original variants plus the dev preview toggle,
keeping just the Lightning / On-chain tab switcher and a wider gap to
the balance underneath.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: use shadcn Tabs primitive for balance switcher

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: type balance switcher icons with LucideIcon

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: only render balance switcher when on-chain wallet exists

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: note lucide-react *Icon suffix convention in AGENTS.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
2026-05-07 13:01:17 +07:00
Roland
15be274f24
chore: rename spending balance to lightning balance (#2305)
* chore: rename spending balance to lightning balance

* chore: align AutoSwap threshold label capitalization

Match sentence case used by the form label and sibling rows in
ActiveSwapOutConfig.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: more renaming and copy fixes

* chore: more renaming

---------

Co-authored-by: René Aaron <rene@twentyuno.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 12:49:24 +07:00
René Aaron
a64345714e
fix: scope Alby theme default-variant styling to buttons only (#2302)
fix(alby-theme): scope default-variant styling to buttons only

The Alby theme's `[data-variant="default"]` selector was element-broad
(`:is(button, a, span)`) and matched any Radix component that exposes
`data-variant`. Wrapping a `DropdownMenuItem` around `<Link asChild>` made
the `<a>` inherit `data-variant="default"` and pick up the gold gradient +
1px border — the bug fixed symptomatically in #2286 and #2295.

Narrow the selector to `[data-slot~="button"][data-variant="default"]`.
Radix's Slot forwards `data-slot="button"` onto `<Button asChild>` children,
so DialogTrigger asChild and friends keep their styling, while
DropdownMenuItem (`data-slot="dropdown-menu-item"`) and Badge
(`data-slot="badge"`) no longer match.

Restore the idiomatic `<DropdownMenuItem asChild><Link/>` pattern that #2286
removed so users keep middle-click / cmd-click / "open in new tab" on menu
navigation items.
2026-05-06 17:15:16 +07:00
René Aaron
954c69028e
fix: hide transaction ID from on-chain transactions list (#2304)
Transaction IDs in the wallet on-chain tab list are noisy and not very
useful at a glance. The full TX ID is still shown in the transaction
detail modal with a copy button and a link to mempool.

Note: showing the bitcoin address in the detail modal (also suggested
in the issue) would require extending lnclient.OnchainTransaction with
an Address field. LDK's PaymentKindOnchain doesn't expose the address,
so this would only be populatable for LND/CLN — leaving it out for now.

Closes #2299
2026-05-06 16:58:20 +07:00
Roland
21e7b1d885
docs: add avoid useNavigate point to AGENTS.md (#2300) 2026-05-06 11:01:00 +02:00
daywalker90
12c454f3ae
feat: add notifications for CLN backend (#2287)
* fix: protoc command for cln bindings documentation

* feat: update cln proto bindings to v26.04

* feat: add notifications for CLN backend

starting with nwc_lnclient_payment_sent, nwc_lnclient_payment_failed,
nwc_lnclient_payment_received, nwc_payment_forwarded, nwc_channel_ready,
nwc_channel_closed
2026-05-06 13:11:03 +07:00
Adithya Vardhan
099ad146df
chore: bump protobuf hex-display replace to v1.33.0 (#2293)
* chore: bump protobuf hex-display replace to v1.33.0

* chore: update protobuf replace comment for lnd v0.20.1-beta
2026-05-06 13:06:59 +07:00
Adithya Vardhan
f9fd5e9993
chore: bump lnd to v0.20.1-beta (#2285) 2026-05-04 15:07:23 +05:30
Adithya Vardhan
b9d15f54d3
fix: avoid highlighted border on menu buttons in Alby theme (#2286) 2026-05-04 15:06:55 +05:30
dependabot[bot]
fef6fc0afc
build(deps): bump github.com/mattn/go-sqlite3 from 1.14.42 to 1.14.44 (#2276)
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.42 to 1.14.44.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.42...v1.14.44)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-sqlite3
  dependency-version: 1.14.44
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 14:10:23 +05:30
dependabot[bot]
fa978b6faf
build(deps): bump @scure/bip39 from 2.0.1 to 2.2.0 in /frontend (#2277)
Bumps [@scure/bip39](https://github.com/paulmillr/scure-bip39) from 2.0.1 to 2.2.0.
- [Release notes](https://github.com/paulmillr/scure-bip39/releases)
- [Commits](https://github.com/paulmillr/scure-bip39/compare/2.0.1...2.2.0)

---
updated-dependencies:
- dependency-name: "@scure/bip39"
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 13:55:49 +05:30
dependabot[bot]
66467b580a
build(deps): bump dayjs from 1.11.13 to 1.11.20 in /frontend (#2278)
Bumps [dayjs](https://github.com/iamkun/dayjs) from 1.11.13 to 1.11.20.
- [Release notes](https://github.com/iamkun/dayjs/releases)
- [Changelog](https://github.com/iamkun/dayjs/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/iamkun/dayjs/compare/v1.11.13...v1.11.20)

---
updated-dependencies:
- dependency-name: dayjs
  dependency-version: 1.11.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 13:42:00 +05:30
dependabot[bot]
80291dcb90
build(deps-dev): bump @commitlint/cli from 20.5.0 to 20.5.3 in /frontend (#2279)
Bumps [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) from 20.5.0 to 20.5.3.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.5.3/@commitlint/cli)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.5.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 13:37:50 +05:30
dependabot[bot]
a73338e168
build(deps-dev): bump typescript-eslint from 8.58.2 to 8.59.1 in /frontend (#2281)
build(deps-dev): bump typescript-eslint in /frontend

Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.58.2 to 8.59.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.59.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.59.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 13:31:05 +05:30
dependabot[bot]
386a849124
build(deps-dev): bump eslint from 10.2.0 to 10.3.0 in /frontend (#2280)
Bumps [eslint](https://github.com/eslint/eslint) from 10.2.0 to 10.3.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.2.0...v10.3.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 13:26:20 +05:30
daywalker90
28706ecf31
feat: add CLN backend (#2026)
* feat: add CLN as a lnclient backend

* feat: add hold invoice support for CLN backend

* fix: reduce CLN form to just addresses and lightning dir

* feat: add README for CLN grpc go code generation

* fix: cln backend does not support keysend with given preimages

* fix: hold invoice notifications in CLN backend

* fix: remove dead code in CLN backend from ListTransactions

* fix: env example CLN_ADDRESS_HOLD with different port to show it's a different service

* fix: cleanup of CLN ressources in all cases

* fix: cln backend's GetNetworkGraph only fetches specified nodeId's

* fix: cln backend: only advertise hold methods for nip47 if hold plugin enabled

* fix: cln's Shutdown should not stop CLN itself

* fix: relax the LND README line regarding env configuration

* fix: more nil checks in clnInvoiceToTransaction

* fix: prevent feerate overflow in CLN's RedeemOnchainFunds

* fix: don't access nil errors for empty reponses of certain CLN methods

* fix: nil instead of empty string in cln's GetNetworkGraph return types

* fix: nil checks for created_at in cln's clnInvoiceToTransaction

* fix: set minimum tls version to 1.2 for cln backend grpc connections

* fix: cln's subscribeOpenHoldInvoices doesn't give up as fast

* fix: deduplicate graph edges in cln's GetNetworkGraph

* fix: print the error string, not pointer address, in cln's ListChannels

* fix: remove cln's ListTransactions completely

* fix: use ListPeers instead of ListPeerChannels in cln's ListPeers

* feat: cln's MakeHoldInvoice supports minCltvExpiryDelta

* fix: use named return err in NewCLNService

* fix: cln listpeers log message

* fix: incorrect import

* fix: compile errors after rename

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
2026-05-03 12:36:13 -07:00
Adithya Vardhan
fe1f338ed3
feat: allow payments from app connections (#2267)
* feat: allow payments from app connections

* feat: add searchable pay-from combobox

* chore: add constant for apps limit and sort apps in list

* fix: add serverside filtering, default combobox value

* chore: rename appId to fromAppId in send payment request

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
2026-05-03 12:27:38 -07:00
im-adithya
a10c328876 fix: stretch theme preview labels to full width 2026-05-01 16:00:01 +05:30
Adithya Vardhan
f9702c6998
fix: preserve badge background clipping (#2273)
* fix: update codex logo

* fix: preserve badge background clipping
2026-05-01 15:48:09 +05:30
Adithya Vardhan
0c421375e5
chore: update to use sat/msat suffixes everywhere (#2271)
* chore: align internal models and methods to use sat/msat suffixes

* chore: update request structs to use sat/msat suffixes

* fix: default max amount to 0 during app creation

* fix: simplify helper functions

* chore: remove unused lnclient probing methods

* chore: remove unused json tags on swap struct

* chore: update frontend to use sat/msat suffixes everywhere

* chore: use request types in frontend

* fix: linting

* fix: remove deprecated sat and msat fields in frontend

* chore: further changes

* fix: avoid sat/msat resolver variable shadowing

* fix: validate Support Alby amounts
2026-05-01 15:41:56 +05:30
Adithya Vardhan
663b805868
feat: simplify onchain receive routing (#2268)
* feat: show onchain options in receive screen

* fix: wallet receive mode routing and navigation issue

* fix: receive on-chain link in command palette
2026-05-01 13:00:45 +05:30
Adithya Vardhan
1394733729
fix: simplify themes (#2270) 2026-04-30 20:39:13 +02:00
René Aaron
925cba450c
refactor(frontend): extract DetailRow for transaction details dialog (#2269)
refactor(frontend): extract TransactionDetailRow for both detail dialogs

Pulls the repeated `<div><label/><value+copy/></div>` shape out of
TransactionItem and OnchainTransactionItem into a shared component with
an optional `copyable` prop. Replaces ~16 hand-written rows in the
lightning dialog and 4 in the on-chain dialog. Lightning also gets
parent-driven `gap-6` spacing in place of per-row `mt-6` / `mt-8`.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 21:03:32 +05:30
René Aaron
25ce056eb8
fix(frontend): auto-balance home dashboard columns (#2255)
* fix(frontend): auto-balance home dashboard columns

Replace the fixed two-column grid with a CSS multi-column layout so
widgets flow and auto-balance regardless of which ones render (several
widgets can return null based on user state). Group the Alby ecosystem
cross-promo cards so they stay together within a column.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: extract Alby home widgets

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-04-30 20:46:36 +05:30
saunter
30ef3c254a
feat(frontend): wallet on-chain mode (#2217)
* feat(frontend): add wallet on-chain balance mode

Show on-chain balances and transactions in Wallet.
Keep receive and send flows aligned with the active balance mode.

Made-with: Cursor

* fix(frontend): gate on-chain toggle on channel management and fix isLast naming

Hide the balance mode toggle for backends without on-chain support
(PHOENIX, CASHU). Rename misleading `isLast` prop to `showSeparator`
in PendingClosedChannelsAlert.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(frontend): inline on-chain balance display per page

Drop the shared OnchainBalanceSummary component. The two call sites
(compact card on Channels, large hero on Wallet) diverged enough that
sharing required threading className overrides for every internal node.
Inline each shape next to its surrounding context.

Also drop the pending-closed-channels alert from Wallet — it's
channel-management context and belongs on the Channels page only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(frontend): a11y and small cleanups on wallet on-chain mode

- Add aria-label to the balance-mode toggle and aria-hidden on its
  icon so screen readers announce it as a mode switch.
- Drop the unnecessary :index suffix on the pending-closed-channels
  item key — (fundingTxId, fundingTxVout) is unique.
- Replace trailing mb-4 on each on-chain transaction row with
  space-y-4 on the list container so the last row has no dangling
  margin.
- Use named useState import in Wallet to match repo convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(frontend): move OnchainTransactionsTable to components/wallet and drop node page copy

Wallet is now the single place to review on-chain transaction history,
so drop the duplicate <OnchainTransactionsTable /> from the Node page
and move the component to components/wallet/ to reflect its new home.
Pure move — no content changes to the component itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(frontend): simplify OnchainTransactionsTable to its one use case

Wallet is now the sole consumer of this component, so remove the
reusability props that were only there for the now-dropped Channels
page render: wrapInCard, title, className, contentClassName,
showEmptyState, and the four emptyState* props. The card-wrapping
branch and its CardHeader/CardTitle dependencies are gone with it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(frontend): add on-chain transaction details dialog

Open a lightweight details dialog from an on-chain transaction row
showing amount, status, confirmations, date/time, and full transaction
id. Mempool becomes a secondary action in the dialog footer (hidden
when no mempool URL is configured).

Diverges slightly from saunter's original implementation: body content
lives in a sibling div of DialogDescription instead of inside it, so
the block elements don't nest inside a <p> (invalid HTML / React
hydration warning).

Folds in https://github.com/getAlby/hub/pull/2218 (original author: saunter).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(frontend): polish on-chain pending transaction state

Treat unconfirmed on-chain transactions as pending in the Wallet UI.
Keep the backend "unconfirmed" state value unchanged — this only
affects UI labels and the variable name (isUnconfirmed → isPending).

Folds in https://github.com/getAlby/hub/pull/2221 (original author: saunter).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(frontend): use "transactions" instead of "payments" in on-chain empty state

Aligns terminology with bitcoin.design convention: "transaction" for
on-chain, "payment" for lightning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(frontend): reset receive-onchain flow on "Receive Another Payment"

The success screen's "Receive Another Payment" button linked to
/wallet/send (typo) and later to /wallet/receive/onchain (still broken
since the route is unchanged, so React Router wouldn't remount and the
local state kept showing the success card).

Replace the LinkButton with a Button that resets the local state
(txId, confirmed/pending amount, start timestamp) and requests a fresh
on-chain address, taking the user back to "Waiting for Payment…" with
a new QR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(frontend): address review feedback on wallet on-chain mode

- OnchainTransactionsTable: don't flash the empty state while SWR is
  still loading; only render it once transactions is defined AND empty.
- OnchainTransactionsTable: add DialogDescription to the details dialog
  so DialogContent has an accessible description (fixes Radix's dev
  warning and improves screen-reader flow).
- OnchainTransactionsTable: swap the copy-tx-id native button for the
  shadcn Button in ghost/icon-xs — kept the row-level button native
  because the shadcn variants (justify-center, h-9, bg) fight the
  custom-shaped row surface.
- Wallet: dynamic aria-label on the balance-mode toggle so the current
  state is announced (previously "Switch balance mode" hid the visible
  label).
- Wallet: align on "On-chain Balance" (repo convention — ~18 other uses)
  instead of "On-Chain Balance".
- Wallet: gate the "Open Your First Channel" alert on channels being
  loaded so it doesn't flash while useChannels() is resolving.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(frontend): unify copy buttons in transaction details dialogs

Both the Lightning (TransactionItem) and on-chain (OnchainTransactionsTable)
transaction details dialogs now use the shadcn Button component for
inline copy-to-clipboard affordances — variant=ghost, size=icon-sm,
muted foreground to match the surrounding value text. Previously Lightning
used a bare CopyIcon with onClick and on-chain used a mix; now both
dialogs look and behave the same.

Also drops the now-redundant "Copy Transaction ID" footer button from
the on-chain dialog (the inline copy icon next to the tx id does the
same thing). Footer hosts only "View on Mempool" and renders only when
a mempool URL is configured.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(frontend): second review pass — pluralization and null-guard fixes

- OnchainTransactionsTable: pluralize the confirmations tooltip so a
  single-confirmation tx reads "1 confirmation" instead of
  "1 confirmations".
- PendingClosedChannelsAlert: guard the mempool funding-tx link on
  info?.mempoolUrl being resolved — previously the URL interpolated to
  "undefined/tx/..." during the brief window before useInfo settles.
  Falls back to omitting the funding-tx link rather than rendering a
  broken one.
- PendingClosedChannelsAlert: only render "with {details}" when the
  details arrays are non-empty. Avoids the dangling "with." sentence
  in the unlikely case that pendingBalancesFromChannelClosures > 0
  while both pendingBalancesDetails and pendingSweepBalancesDetails
  are empty.

Other items in the review were already addressed in earlier commits:
capitalization (6a8f47b3), row mb-4 → space-y-4 on parent (c51826bb),
and the title/emptyState* props (gone with the component simplification).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(frontend): drop redundant mempoolUrl guards

Backend always populates info.mempoolUrl — MEMPOOL_API env defaults to
https://mempool.space/api in config/models.go and GetMempoolUrl
(config/config.go:206) returns it with /api trimmed. There's no code
path that leaves the URL empty, so guarding the "View on Mempool"
button and the "funding tx" link against it being falsy was defensive
against a state the backend won't produce. Matches the convention
used elsewhere (ReceiveOnchain etc.).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(frontend): guard against negative incoming and drop dead mb-1

In the Channels-page inline on-chain balance block:

- Switch the "+X incoming" guard from spendable !== total to
  total > spendable, matching the wallet hero. Prevents a negative
  displayed amount in the unlikely event spendable briefly exceeds total.
- Drop mb-1 from the inline <span> wrapping FormattedBitcoinAmount;
  margin-bottom is a no-op on inline elements. Parent <div className="mb-1">
  already provides the intended spacing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(frontend): drop unreachable short-txid branch in subtitle

Bitcoin transaction ids are always 64 hex chars, so the
`tx.txId.length > 22` ternary's false branch is dead code.
Always render the truncated head…tail form in the row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(frontend): group subtitle with other derived vars

Move the subtitle declaration up alongside Icon, isPending,
typeStateText, statusText, createdAt at the top of OnchainTransactionRow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(frontend): use ExternalLinkButton for "View on Mempool"

Replace the Button + onClick(openLink(...)) pattern with the repo's
ExternalLinkButton component. In http mode it renders as a real
<a target="_blank" rel="noreferer noopener"> so right-click-to-open,
middle-click, keyboard nav, and ctrl-click all work; in wails mode it
falls back to openLink. Previously the button always used the wails
path regardless of mode.

Also drops the now-unused openLink import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: redirect to send screen in onchain mode

* chore: align components

* feat(frontend): drive wallet balance mode from URL

Lift the spending/onchain toggle state into a `mode` query param so the
browser back button restores the previous mode after going through
send/receive flows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(frontend): split wallet dashboard into Lightning and Onchain routes

Replaces the single conditional Wallet component with two sibling route
components mounted under a shared WalletLayout that hosts the page header
and Outlet. The Lightning view (default `/wallet`) is the renamed and
trimmed-down former index.tsx; on-chain (`/wallet/onchain`) is a new
component with its own balance, action buttons, and transactions list.

The toggle UI stays — clicking it now navigates between the two real
routes instead of flipping a `?mode=` query param, so the browser back
button restores the previous tab for free.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(frontend): drop on-chain incoming indicator from balance header

Pending receives are already visible in the on-chain transactions list
below, so the duplicated +X incoming summary just adds noise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: René Aaron <rene@twentyuno.net>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-04-30 14:30:01 +02:00
René Aaron
ed7e10cf25
feat: user labels for transactions (#2265)
* feat: user labels poc

* feat: backend for transaction user labels

Add PATCH /api/transactions/:paymentHash/label that merges a
user-supplied {key:value} map into the existing transaction metadata
under the user_label key, preserving NIP-47 fields. Empty map clears
the labels. Trims whitespace, drops blank rows, caps key/value length.

Wire the frontend editor to call the endpoint and revalidate the
transactions SWR cache on success.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: include user labels as columns in CSV export

Collect the union of user_label keys across all exported transactions
and emit each one as its own label_<key> column. The existing metadata
JSON column is preserved so importers like Raccoin keep working.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: drop hardcoded label suggestions, autocomplete from prior keys

Open the editor with a single blank row instead of four pre-seeded
fields. Suggest label keys via a datalist populated from any
user_label keys already present in the SWR transactions cache, so
suggestions reflect the user's own taxonomy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: inline label editor and indicator-only list row

Replace the nested label dialog with an inline editor that toggles
within the existing transaction detail dialog, removing dialog
stacking. In the transactions list, replace per-label badges with a
single tag icon next to the timestamp so row height stays uniform; the
full labels remain visible in the detail view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: don't bump updated_at on metadata-only edits

GORM's Update auto-touches updated_at, which made the transactions
list reorder labeled transactions to the top. Switch
SetTransactionMetadata to UpdateColumn so only the metadata column
changes. Add regression test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: route PATCH transaction label requests in wails

The desktop app routes API calls through WailsRequestRouter rather
than HTTP. Add a handler for PATCH /api/transactions/:hash/label
before the existing transaction lookup so labels work in Wails too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: finalize backend

* chore: rename to labels

* chore: finalize frontend

* chore: use tx id to add user labels

* chore: address minor nits

* fix: linting

* fix: use explicit primary key lookups

* chore: simplify transaction csv label export

* fix(frontend): drop label count from transaction list badge

The count adds visual weight without informing any decision from the
list view. The icon-as-badge already signals labels exist; the actual
values are in the details dialog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(frontend): revert transaction list label indicator to bare icon

The Badge wrapper made the icon-only indicator wider than tall, which
looked off. Restores the pre-PR look — a small TagIcon next to the
timestamp — since the in-dialog editor is the place to see actual labels.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-04-30 13:13:07 +02:00
Adithya Vardhan
7195c09de3
feat: make Alby the default theme and rename default to classic (#2266)
* feat: make Alby the default theme and rename default to classic

* refactor(frontend): extract neutral base.css for theme defaults

Splits the global :root/.dark fallbacks out of alby.css into a dedicated
base.css so no single theme is special. Alby and Classic both become
pure override layers; Classic is now an empty layer that falls through
to base, eliminating the :root/.theme-classic dual-selector pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: René Aaron <rene@twentyuno.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 11:04:02 +02:00
René Aaron
51ec695a6d
fix(frontend): improve Earn page readability (#2264)
* fix(frontend): improve Earn page readability and polish

Use theme-safe link styling instead of text-primary (unreadable yellow
on white in the Alby theme), bolder reward amounts, and trophy/heart
icons for non-sat rewards.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: simplify reward icon handling

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-04-28 17:10:36 +05:30
René Aaron
54840ba2ed
refactor(frontend): drop time-based home greeting (#2254)
refactor(frontend): replace time-based greeting with static title

The previous time-of-day greeting used the browser's local hour but had
range bugs (e.g. midnight showed "Good Morning"). Rather than iterate on
the ranges, drop the greeting entirely and use "Home" to match the
sidebar nav.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 14:01:49 +05:30
dependabot[bot]
4c1c592a3c
build(deps): bump swr from 2.3.6 to 2.4.1 in /frontend (#2263)
Bumps [swr](https://github.com/vercel/swr) from 2.3.6 to 2.4.1.
- [Release notes](https://github.com/vercel/swr/releases)
- [Commits](https://github.com/vercel/swr/compare/v2.3.6...v2.4.1)

---
updated-dependencies:
- dependency-name: swr
  dependency-version: 2.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 16:58:27 +05:30
dependabot[bot]
eb7ecb086a
build(deps): bump react-router from 7.14.0 to 7.14.2 in /frontend (#2262)
Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) from 7.14.0 to 7.14.2.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.14.2/packages/react-router)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.14.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 16:50:21 +05:30
dependabot[bot]
ba7d10086e
build(deps-dev): bump eslint-plugin-react-hooks from 7.0.1 to 7.1.1 in /frontend (#2261)
* build(deps-dev): bump eslint-plugin-react-hooks in /frontend

Bumps [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) from 7.0.1 to 7.1.1.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/eslint-plugin-react-hooks@7.1.1/packages/eslint-plugin-react-hooks)

---
updated-dependencies:
- dependency-name: eslint-plugin-react-hooks
  dependency-version: 7.1.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: disable react-hooks set-state-in-effect

* chore: suppress react-hooks set-state-in-render in NewApp

* fix: avoid mutating order before submission

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-04-27 16:46:41 +05:30
dependabot[bot]
a4f81fd907
build(deps-dev): bump @commitlint/config-conventional from 20.0.0 to 20.5.0 in /frontend (#2260)
build(deps-dev): bump @commitlint/config-conventional in /frontend

Bumps [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) from 20.0.0 to 20.5.0.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.5.0/@commitlint/config-conventional)

---
updated-dependencies:
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 20.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 16:45:39 +05:30
dependabot[bot]
008a1c7f74
build(deps-dev): bump @tailwindcss/vite from 4.1.11 to 4.2.4 in /frontend (#2259)
build(deps-dev): bump @tailwindcss/vite in /frontend

Bumps [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) from 4.1.11 to 4.2.4.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.4/packages/@tailwindcss-vite)

---
updated-dependencies:
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.2.4
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 15:58:29 +05:30
dependabot[bot]
eca9599047
build(deps): bump github.com/BoltzExchange/boltz-client/v2 from 2.11.2 to 2.11.3 (#2257)
build(deps): bump github.com/BoltzExchange/boltz-client/v2

Bumps [github.com/BoltzExchange/boltz-client/v2](https://github.com/BoltzExchange/boltz-client) from 2.11.2 to 2.11.3.
- [Release notes](https://github.com/BoltzExchange/boltz-client/releases)
- [Changelog](https://github.com/BoltzExchange/boltz-client/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BoltzExchange/boltz-client/compare/v2.11.2...v2.11.3)

---
updated-dependencies:
- dependency-name: github.com/BoltzExchange/boltz-client/v2
  dependency-version: 2.11.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 15:52:15 +05:30
René Aaron
12557618ad
refactor(frontend): redesign Pro upgrade modal (#2251)
* refactor(frontend): redesign Pro upgrade dialog and fix theme bleed

- Drop the `premium` button variant; default buttons now use the alby
  theme gradient via theme tokens instead of hard-coded amber
- Fix alby button gradient: target `data-variant` only (so Radix
  asChild wrappers like DialogTrigger don't break the selector) and
  unlayer the override so it's not overridden by Tailwind utilities
- ExternalLink now forwards arbitrary props so ExternalLinkButton's
  data-variant attribute reaches the rendered <a>
- Recompose UpgradeDialog: Pro badge -> headline -> stacked $3 / mo
  price -> 6-feature grid -> CTA with inline price -> guarantee

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(frontend): apply V1 upgrade modal design

Restructure the Pro upgrade dialog to match the V1 "Refined default"
design from the Claude Design handoff:

- Pro pill: primary-tinted Badge with sparkles glyph
- New headline ("Do more with your Hub.") + open-source subhead
- Price block: $3 / month inline with "Billed $36 yearly" caption
- Single-column feature list with primary-tinted check circles
- CTA drops the inline price (already prominent above) and the
  guarantee tightens to "30-day money-back guarantee"

All color treatments use theme tokens (bg-primary/10, border-primary/40,
text-foreground) so the look adapts per hub theme.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(frontend): inline Pro badge in upgrade dialog heading

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: copy

* fix(frontend): compose onClick in ExternalLink non-http branch

Spreading props after setting onClick silently dropped caller-provided
handlers. Compose them instead so callers' onClick runs, with
openLink skipped if the event is prevented.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: add sr-only to dialog header

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-04-27 15:44:04 +05:30
René Aaron
1c69f2f261
feat: redesign settings pages with improved UI (#2224)
* feat: redesign settings pages with improved UI

- Add icons and group nav into sections
- Visual theme picker with color previews
- Segmented light/dark/system toggle
- Card-based layout for settings sections
- Alby Account: profile info with ProBadge
- Locked themes trigger UpgradeDialog
- Sidebar: startsWith for active states

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address review feedback

- Sidebar: boundary-aware route matching
- Theme cards: use button for keyboard a11y
- Appearance toggle: add aria-pressed
- Alby Account: show email when name exists

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use CSS variables for theme previews

Use actual theme classes with CSS variables instead
of hardcoded hex colors for theme preview cards.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: extract ThemePreview component

Use CSS theme classes directly on preview elements
with a theme-default wrapper for proper fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: mobile view

* revert: restore AlbyAccount page to master

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: align general settings with other settings pages

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: drop unused optional description change in SettingsHeader

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: improve theme picker accessibility and loading state

- Use role=radiogroup/radio semantics for theme grid and dark mode toggle
- Make locked theme cards keyboard-accessible via controlled UpgradeDialog
- Gate render on albyMe load when account connected to prevent lock flash
- Use cursor-not-allowed on disabled theme cards
- Mark ThemePreview aria-hidden (decorative)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: extract isPathActive helper in AppSidebar

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: move ThemePreview to components directory

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: align settings sidebar with main sidebar

* chore: polish appearance section in settings

* chore: add spacing for scroll in backup page

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-04-27 15:12:38 +05:30
René Aaron
2f3dc233d3
feat: improve Alby Account settings page (#2253)
* feat: improve Alby Account settings page

Redesigns the settings page with a unified profile card, groups
destructive actions into a "Danger Zone", and simplifies the copy
for the Switch/Disconnect flows.

Closes #2247

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: handle loading and failed state

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-04-27 10:55:22 +05:30
hermes-alby
d97ed7114f
chore: update ldk-node-go (#2256)
Co-authored-by: Hermes Alby <hermes-alby@users.noreply.github.com>
2026-04-25 14:50:33 +07:00
dependabot[bot]
3b1dae25ef
build(deps): bump @getalby/lightning-tools from 6.0.0 to 8.1.0 in /frontend (#2237)
build(deps): bump @getalby/lightning-tools in /frontend

Bumps [@getalby/lightning-tools](https://github.com/getAlby/js-lightning-tools) from 6.0.0 to 8.1.0.
- [Release notes](https://github.com/getAlby/js-lightning-tools/releases)
- [Commits](https://github.com/getAlby/js-lightning-tools/compare/v6.0.0...v8.1.0)

---
updated-dependencies:
- dependency-name: "@getalby/lightning-tools"
  dependency-version: 8.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 23:08:54 +05:30
dependabot[bot]
20ce0fadae
build(deps-dev): bump typescript-eslint from 8.57.1 to 8.58.2 in /frontend (#2238)
build(deps-dev): bump typescript-eslint in /frontend

Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.57.1 to 8.58.2.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.58.2/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.58.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 23:00:30 +05:30
dependabot[bot]
3632baeb12
build(deps-dev): bump @types/node from 25.3.3 to 25.6.0 in /frontend (#2239)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.3.3 to 25.6.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 22:59:13 +05:30
dependabot[bot]
f929e80300
build(deps-dev): bump prettier from 3.8.1 to 3.8.3 in /frontend (#2240)
Bumps [prettier](https://github.com/prettier/prettier) from 3.8.1 to 3.8.3.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.1...3.8.3)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.8.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 22:54:43 +05:30
dependabot[bot]
7eb0667411
build(deps): bump zustand from 4.5.7 to 5.0.12 in /frontend (#2241)
Bumps [zustand](https://github.com/pmndrs/zustand) from 4.5.7 to 5.0.12.
- [Release notes](https://github.com/pmndrs/zustand/releases)
- [Commits](https://github.com/pmndrs/zustand/compare/4.5.7...v5.0.12)

---
updated-dependencies:
- dependency-name: zustand
  dependency-version: 5.0.12
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 22:52:19 +05:30
Adithya Vardhan
dc7f3f4ccb
fix: use file paths instead of hex during LND onboarding (#2231) 2026-04-21 22:33:25 +05:30
hermes-alby
d4bd46f05a
fix: clarify connected peers navigation in LDK alert (#2250)
Co-authored-by: Hermes Alby <hermes-alby@users.noreply.github.com>
2026-04-21 21:42:00 +05:30
Roland
963c7e139c
fix: remove bzip install command (agent can figure it out) (#2246) 2026-04-21 20:59:46 +07:00
René Aaron
fd977f157e
feat: add Sats4AI to app store (#2232)
* feat: add Sats4AI to app store

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: pad Sats4AI logo to square

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 11:22:40 +02:00
René Aaron
6e8e8cb056
fix(frontend): use npx skills add for agent skill install prompts (#2236)
* fix(frontend): use npx skills add for agent skill install prompts

Agents WebFetch the GitHub URL and summarize the skill content, losing
important details. Switch to `npx -y skills add` commands which install
the skill directly, matching the pattern already used for payments-skill.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(frontend): use npx skills add for Bitrefill skill prompt too

Bitrefill is also on the skills registry as bitrefill/agents.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 11:01:30 +02:00
Roland
0a36ebaf65
chore: make install and update scripts more agent-friendly (#2245)
The download was too verbose.
This lead to the agent unable to read the whole output.
It then made incorrect decisions on what to do next.
2026-04-21 13:58:40 +07:00
Anshuman
1d3f9ecd41
feat: suffix balance/amount fields with explicit unit (Sat or Msat) (#2153)
* feat: add both Sat and Msat companion fields for all ambiguous balance/amount properties

* feat: add sat and msat companion fields to backend API responses

* chore: align frontend types and other missing fields

* chore: keep old formula for calculating total fee sat

* chore: remove unnecessary balance assignments in cashu and phoenix

* chore: add deprecated comment to non unit fields

* chore: rename callers and variable to specify units

* chore: remove msat fields for channel size and liquidity fields

* chore: drop msat fields from onchain channel size and liquidity responses

* chore: further changes

* chore: remove amount msat field onchain tx

* chore: remove msats from onchain balance response

* chore: remove msat fields for swaps

* chore: remove msat fields for punishment reserves

* chore: simplify rebalancing fee calculation

* chore: remove unnecessary fields

* chore: mark deprecated fields in frontend types

---------

Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-04-17 13:53:28 +05:30
saunter
46d3a8942a
feat(frontend): move ZapPlanner button to Wallet and move wallet mobile actions to overflow menu (#2200)
* feat(frontend): improve wallet page balance layout

Center the wallet balance area and tune spacing for a cleaner visual rhythm.
Move secondary actions to the header as ghost buttons and use a vertical more icon.

Made-with: Cursor

* feat(frontend): refine wallet header actions on mobile

Move wallet secondary actions into the mobile overflow menu and add Recurring.
Also remove the ZapPlanner card from Home to avoid duplicate entry points.

Made-with: Cursor

* refactor(frontend): separate wallet actions, add ProDropdownMenuItem and AlertAction

- Extract wallet navigation (Swap, Recurring, Buy) into dedicated WalletActionsMenu component
- Revert TransactionsListMenu to single-purpose (export transactions only)
- Move CSV export logic to shared transactions-utils
- Add ProDropdownMenuItem for reusable pro-gated dropdown items with consistent Pro badge
- Add AlertAction component to alert system for proper action button placement
- Use controlled mode in UpgradeDialog to avoid DialogTrigger data-slot conflicts
- Replace inline upgrade gating in Channels (Set Node Alias) and Settings (themes)
- Theme select now opens UpgradeDialog instead of disabling paid items
- Use AlertAction in SubwalletList for upgrade prompt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(frontend): consolidate export into WalletActionsMenu

Show wallet actions (Swap, Recurring, Buy) only on mobile,
Export Transactions on all breakpoints — removes the separate
TransactionsListMenu from the wallet page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(frontend): sanitize CSV export and fix subwallet upgrade copy

Prevent CSV formula injection by prepending a single quote to values
starting with =, +, -, or @. Fix grammar in sub-wallet upgrade prompt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(frontend): center fiat amount skeleton on wallet page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: René Aaron <rene@twentyuno.net>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 16:18:41 +02:00
saunter
ac88c8d573
feat(frontend): improve wallet balance section layout (#2199)
* feat(frontend): improve wallet page balance layout

Center the wallet balance area and tune spacing for a cleaner visual rhythm.
Move secondary actions to the header as ghost buttons and use a vertical more icon.

Made-with: Cursor

* fix: no important modifiers

* fix: remove important modifiers

* fix: use ghost for both occurrences

* fix: screenreader

* fix: center fiat amount skeleton on wallet page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: René Aaron <rene@twentyuno.net>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:34:50 +02:00
René Aaron
a7a0d891a4
feat: AI & Agents page (#2193)
* feat: ai

* fix: review feedback

* fix: new tabs, install via prompt

* fix: cleanup

* fix: visual improvements

* fix: connection names

* fix: cursor

* fix: copy

* fix: optimize images

* fix: agent card

* fix: cursor logo

* fix: avatar rounding

* fix: class

* fix: link with tab handling

* fix: example prompts, claude code using skill

* fix: capabilities

* feat: share connection instructions

* fix: mange + explore ai apps

* fix: always create a new connection

* fix: use externallink, cleanup

* fix: improved hero copy

* fix: copy

* fix: apply CodeRabbit auto-fixes

Fixed 1 file(s) based on 2 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

* fix: formatting

* fix: openclaw description

* fix: dim card classes

* fix: use tailwind divide classes

* fix: stop cursor blinking in header

* feat: use CLI auth flow for agent wallet onboarding

Replace connection secret embedding with @getalby/cli auth command
for CLI-based agents. The auth flow generates keys locally so the
secret never leaves the device or gets sent to the AI model.

- Generic agents skip connection creation, show auth prompt immediately
- Claude Code and Goose CLI tabs use auth instead of secret/MCP config
- Claude Web/Desktop and Goose Desktop still use MCP URLs as before
- "Waiting for agent to connect" only shown for MCP-based agents

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: simplify AI agent onboarding with CLI auth flow

- Use @getalby/cli auth for all CLI agents instead of embedding
  connection secrets in prompts — keys stay local, never sent to AI
- Split Claude dropdown into "Claude Code" (auth prompt) and
  "Claude Web/Desktop" (MCP setup via /apps/new)
- Goose uses generic auth prompt (no dedicated setup page needed)
- Remove Goose internal-app page/route (no longer linked)
- Remove standalone ClaudeConnectionInstructions and
  GooseConnectionInstructions components (inlined into consumers)
- Add agent logo map in AppAvatar for connections without full
  app store entries (goose, openclaw, cursor, codex, cline, opencode)
- Remove Goose from app store (prevents 404 on internal redirect)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add gemini, update mcp link

* fix: clarify Claude connector instructions (#2216)

fix: update Claude connector setup wording

Align Claude Web/Desktop steps with current Connectors labels and MCP URL wording.

Made-with: Cursor

Co-authored-by: René Aaron <rene@twentyuno.net>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: inline MCP setup on AI page, remove Claude internal-app

- Add generic McpSetup component for inline MCP URL connection
  creation (reusable for future agents with MCP support)
- Claude entry gets mcpInstructions for Web/Desktop connector setup
- Remove /internal-apps/claude page and route
- Remove Claude from app store, add to agentLogos map
- Support subfolder installs in auth prompt hub URL

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: update inspiration prompts and copy

- Add service names to prompts (bitrefill.com, ppq.ai, unhuman.store)
- Add "Creative" category with image generation + print-on-demand
- Replace weak automation example with spending analysis
- Rename heading to "What can your agent do?"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: improve AI page prompt cards UX

Make inspiration prompts clickable to copy (matching connect card),
use ChevronRightIcon consistently, and add discovery prompt to Services.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: update Services prompt to podcast search example

Replace untestable "bitcoin price data" prompt with a working
discover→fetch example using Pull That Up Jamie podcast search.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: use npx skills add for agent onboarding prompts

WebFetch summarizes SKILL.md content, losing critical details. Switch
to `npx -y skills add getAlby/bitcoin-payments-skill` which downloads
the full skill file via GitHub without summarization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update prompts

* fix: update skill install command to use correct repo name

The repo was renamed from getAlby/bitcoin-payments-skill to
getAlby/payments-skill.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add -y flag to skill install prompts for non-interactive install

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: redirect to app detail page after connection is established

After approving a new app connection, redirect to /apps/:id instead of
the apps list so the user sees the app they just created.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: match agent logos by app name fallback

When app_store_app_id metadata is not set, fall back to matching the
app name against the agent logos map so agent apps created via auth
show proper icons without needing app store entries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: saunter <68239231+stackingsaunter@users.noreply.github.com>
2026-04-14 16:36:43 +02:00
René Aaron
b7ce5340bd
fix: update AGENTS.md for common problems in ui development (#2179)
* fix: common problems in ui

* fix: add layout, spacing, and copy guidance to AGENTS.md

Address recurring agent mistakes: nested card layouts, inconsistent
spacing, and copy that doesn't match the user's context inside the wallet.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:08:13 +02:00
saunter
b46670c505
chore(frontend): shadcn v4 upgrade, Alby theme polish, hub logo (#2155)
* chore(frontend): upgrade shadcn/ui components for Tailwind v4

- Refresh vendored UI primitives from shadcn registry (radix-ui, lucide, etc.)
- Fix components.json tailwind.config for Tailwind v4 CLI
- Preserve Hub splits: buttonVariants, badgeVariants, hybrid tooltip + TouchProvider
- Keep custom alert/badge variants; sonner uses document dark class
- Remove duplicate use-mobile.ts (use use-mobile.tsx only)
- Card keeps shadow-sm to match pre-change product default

Made-with: Cursor

* feat(frontend): alby theme polish and theme-aware hub logo

- Alby: lighter primary gradient, 1px stroke, stacked focus ring
- Alby: neutral gray palette; dark primary button label matches light
- Hub logo: default/alby light & dark fills; mono for other themes
- Figtree for alby; map tailwind font-sans via --app-font-sans
- Link buttons: data-slot/data-variant for themed primary styles

Made-with: Cursor

* ci: fix fork pr macos builds in http and wails workflows

Select Xcode.app for CGO on macOS so Wails WebKit compiles.

Skip Apple signing/notarize/DMG when the PR head is a fork (no secrets);
upload an unsigned zip for Wails fork PRs.

Set fail-fast false on the HTTP build matrix so Linux jobs finish if macOS
fails.

Made-with: Cursor

* ci: gate macos signing on workflow_call build-release input

For workflow_call runs, require inputs.build-release for macOS signing.
Fork PR logic unchanged.

Made-with: Cursor

* ci: revert macos workflow signing fixes

Revert workflow_call gating and fork unsigned logic.
Remove Xcode selection step added for CGO builds.

Made-with: Cursor

* fix: fallback font

* fix: theme detection for sonner

* fix: accent color for alby theme

* fix: cleanup alby hub logo implementation

* fix: revert external link change

* fix: unify font loading

* fix: hub logo coloring / inversion

* fix: disable card shadows

* fix: remove old radix deps

* fix: stepper asChild

* fix: not selector

* fix: restore max-h-full on sheet to ensure full viewport height

The shadcn upgrade dropped max-h-full from the right/left sheet
variants, causing the sheet to not cover the full viewport height.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: René Aaron <rene@twentyuno.net>
Co-authored-by: René Aaron <100827540+reneaaron@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:05:54 +02:00
saunter
29e79e66f9
feat: polish Recently Used Apps widget on Home (#2168)
* feat: polish recently used apps widget

Match Home widget styles for the See All CTA, card shell, and chevron interaction.

Made-with: Cursor

* fix: move recently used apps widget to left column

Place Recently Used Apps in the left Home column above the Alby Go widget.

Made-with: Cursor

* fix: remove obsolete classes

---------

Co-authored-by: René Aaron <rene@twentyuno.net>
2026-04-14 13:28:45 +02:00
saunter
920d59b585
feat: add New Arrivals widget to Home (#2169)
* feat: add New Arrivals widget to Home

Show up to three curated latest apps in a New Arrivals card.
Place it in the left Home column above Alby Go.

Made-with: Cursor

* feat: make New Arrivals widget data-driven with addedDate

- Add addedDate field to AppStoreApp type
- Add addedDate to latest 3 apps (nadanada, LendaSwap, Bitrequest)
- Sort appStoreApps by addedDate (newest first), then alphabetically
- Remove hardcoded latestAppStoreAppIds list
- Remove duplicate getAppDestination function
- Align widget styling with other Home widgets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: sort by addedDate in NewArrivalsWidget, keep app store alphabetical

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: extract getAppStoreUrl utility and deduplicate app link logic

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace bitrequest with castamatic in new arrivals

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use descriptive alt text for app logos in SuggestedApps

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: René Aaron <rene@twentyuno.net>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 13:22:50 +02:00
saunter
4aea34108b
feat: align App of the Day widget UI (#2170)
* feat: align App of the Day widget UI

Use a chevron row layout like Recently Used Apps and place the widget above
Alby Go in the left Home column without changing selection logic.

Made-with: Cursor

* fix: remove obsolete classes

* fix: use theme classes

---------

Co-authored-by: René Aaron <rene@twentyuno.net>
2026-04-14 11:09:00 +02:00
dependabot[bot]
0466ba8547
build(deps): bump react and @types/react in /frontend (#2229)
* build(deps): bump react and @types/react in /frontend

Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) and [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react). These dependencies needed to be updated together.

Updates `react` from 19.2.3 to 19.2.5
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.5/packages/react)

Updates `@types/react` from 19.2.7 to 19.2.14
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: "@types/react"
  dependency-version: 19.2.14
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: update react-dom to v19.2.5 to match react

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-04-13 23:16:27 +05:30
dependabot[bot]
4e7829380e
build(deps): bump react-router-dom from 6.30.1 to 7.14.0 in /frontend (#2228)
* build(deps): bump react-router-dom from 6.30.1 to 7.14.0 in /frontend

Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.30.1 to 7.14.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.14.0/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-version: 7.14.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: migrate router imports to react-router v7

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-04-13 23:14:56 +05:30
dependabot[bot]
ea39b16ce2
build(deps-dev): bump @vitejs/plugin-react-swc from 3.11.0 to 4.3.0 in /frontend (#2230)
build(deps-dev): bump @vitejs/plugin-react-swc in /frontend

Bumps [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react-swc) from 3.11.0 to 4.3.0.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/v4.3.0/packages/plugin-react-swc)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react-swc"
  dependency-version: 4.3.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 22:30:07 +05:30
dependabot[bot]
1012eec508
build(deps-dev): bump @eslint/eslintrc from 3.3.4 to 3.3.5 in /frontend (#2227)
Bumps [@eslint/eslintrc](https://github.com/eslint/eslintrc) from 3.3.4 to 3.3.5.
- [Release notes](https://github.com/eslint/eslintrc/releases)
- [Changelog](https://github.com/eslint/eslintrc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslintrc/compare/eslintrc-v3.3.4...eslintrc-v3.3.5)

---
updated-dependencies:
- dependency-name: "@eslint/eslintrc"
  dependency-version: 3.3.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 22:25:58 +05:30
dependabot[bot]
9d1487de98
build(deps): bump golang.org/x/crypto from 0.49.0 to 0.50.0 (#2226)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.49.0 to 0.50.0.
- [Commits](https://github.com/golang/crypto/compare/v0.49.0...v0.50.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.50.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 22:23:40 +05:30
dependabot[bot]
26357c7ba2
build(deps): bump @stepperize/react from 5.1.9 to 6.1.0 in /frontend (#2205)
* build(deps): bump @stepperize/react from 5.1.9 to 6.1.0 in /frontend

Bumps [@stepperize/react](https://github.com/damianricobelli/stepperize/tree/HEAD/packages/react) from 5.1.9 to 6.1.0.
- [Release notes](https://github.com/damianricobelli/stepperize/releases)
- [Changelog](https://github.com/damianricobelli/stepperize/blob/main/packages/react/CHANGELOG.md)
- [Commits](https://github.com/damianricobelli/stepperize/commits/@stepperize/react@6.1.0/packages/react)

---
updated-dependencies:
- dependency-name: "@stepperize/react"
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix: migrate custom stepper and NewApp to stepperize v6

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-04-13 22:17:34 +05:30
Adithya Vardhan
d830561bc3
chore: ignore google.golang.org/grpc updates (#2225) 2026-04-13 18:16:24 +05:30
dependabot[bot]
f63056bffd
build(deps): bump github.com/mattn/go-sqlite3 from 1.14.38 to 1.14.42 (#2202)
* build(deps): bump github.com/mattn/go-sqlite3 from 1.14.38 to 1.14.41

Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.38 to 1.14.41.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.38...v1.14.41)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-sqlite3
  dependency-version: 1.14.41
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: update go-sqlite3 pkg to v1.14.42

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-04-13 18:04:40 +05:30
dependabot[bot]
292a4cb90b
build(deps): bump @radix-ui/react-tooltip from 1.2.7 to 1.2.8 in /frontend (#2204)
build(deps): bump @radix-ui/react-tooltip in /frontend

Bumps [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives) from 1.2.7 to 1.2.8.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 17:53:50 +05:30
dependabot[bot]
2ae7bed62f
build(deps): bump @radix-ui/react-progress from 1.1.7 to 1.1.8 in /frontend (#2206)
build(deps): bump @radix-ui/react-progress in /frontend

Bumps [@radix-ui/react-progress](https://github.com/radix-ui/primitives) from 1.1.7 to 1.1.8.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-progress"
  dependency-version: 1.1.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 17:48:15 +05:30
dependabot[bot]
94f52c65cf
build(deps-dev): bump tailwindcss from 4.1.16 to 4.2.2 in /frontend (#2207)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) from 4.1.16 to 4.2.2.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.2/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-version: 4.2.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 17:43:43 +05:30
dependabot[bot]
032bbd23a5
build(deps-dev): bump eslint from 10.0.2 to 10.2.0 in /frontend (#2208)
Bumps [eslint](https://github.com/eslint/eslint) from 10.0.2 to 10.2.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.0.2...v10.2.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 17:34:25 +05:30
René Aaron
02a50f01af
fix: align Pro badge styling with theme (#2198)
* fix: align Pro badge styling with theme

Use theme-aware colors (primary with opacity) instead of hardcoded
amber gradient, so the badge fits the dark theme while still standing out.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: increase Pro badge background opacity

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use solid primary colors for Pro badge visibility

Use bg-primary/text-primary-foreground instead of low-opacity tint
to ensure the badge is visible across all themes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:27:29 +02:00
René Aaron
e22146961b
fix: improve budget select UX (#2209)
* fix: budget select ui

* Update frontend/src/components/BudgetAmountSelect.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix: review feedback

* fix: expiry select, use default budget

* fix: further improvements

* fix: default expiry value

* fix: cleanup expiry options

* fix: address review feedback, cleanup

* fix: styling and input type

* fix: wrap permissions component with form for browser native validation (#2215)

* chore: simplify permissions component and keep prefilled fields editable

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-04-10 19:13:09 +05:30
René Aaron
1e2a0e6751
fix: remove custom text styles from checbox and radio labels (#2180)
* fix: remove custom text styles

* fix: add pointer cursor to radio and checkbox labels

* chore: remove redundant label classes

* chore: further changes

---------

Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-04-10 14:52:16 +05:30
René Aaron
4b841178f6
feat: add bip21 link handling + register global handlers (#2131)
* feat: add bip21 link handling + register global handlers

* fix: validate bitcoin address from bip21

* chore: add useRegisterProtocolHandler hook

* chore: use replace while navigating

* chore: normalize base path to remove trailing slash

---------

Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-04-10 13:35:23 +05:30
LightRider
8fd3f11ada
feat: add nadanada to app store suggestions (#2201)
* feat: add nadanada to app store suggestions

Add nadanada with app metadata, onboarding guide text, and a compliant 200x200 logo asset so it appears in the Alby Hub app store list.

Made-with: Cursor

* fix: update connection instructions

---------

Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-04-10 09:52:11 +05:30
Adithya Vardhan
6303c3ed6b
fix: fetch currencies through alby backend api (#2212)
* fix: fetch currencies through alby backend api

* fix: pass currency to bitcoin rate handler
2026-04-09 23:03:26 +05:30
im-adithya
50ce954b4a chore: add lendaswap ref link and fix linting 2026-04-09 23:00:35 +05:30
Lucas Soriano
24659e3476
feat: add LendaSwap to app store (#2149)
Non-custodial Bitcoin ↔ Stablecoin atomic swaps via NWC.
Uses pay_invoice for Lightning→EVM swaps and make_invoice for
EVM→Lightning swaps.

Categorized under payment-tools with install/finalize guides.
2026-04-09 22:55:06 +05:30
Adithya Vardhan
b1bd7c2587
feat: track last settled transaction time for apps (#2214)
* feat: track last settled transaction time for apps

* fix: use default subwallet ordering and app_id logging in event handler

* chore: rename to last_settled_transaction_at and remove last tx migration

* chore: split app settlement update and budget check

* chore: extract app display name helper into utils function
2026-04-09 16:26:45 +05:30
René Aaron
18efdcdf11
fix: one-click connection flow (#2211)
* fix: one-click connection flow shows waiting screen instead of hanging

When creating a connection via /apps/new?pubkey=..., the flow previously
hung after clicking "Connect" because the finalize step was excluded and
handleCreateApp returned early. Now the finalize step is included for the
pubkey flow with a simplified waiting screen that polls for the first
NWC request and redirects on success.

Closes #2197

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: success handling

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 20:49:47 +02:00
dependabot[bot]
5ebae65293
build(deps): bump github.com/wailsapp/wails/v2 from 2.11.0 to 2.12.0 (#2184)
Bumps [github.com/wailsapp/wails/v2](https://github.com/wailsapp/wails) from 2.11.0 to 2.12.0.
- [Release notes](https://github.com/wailsapp/wails/releases)
- [Commits](https://github.com/wailsapp/wails/compare/v2.11.0...v2.12.0)

---
updated-dependencies:
- dependency-name: github.com/wailsapp/wails/v2
  dependency-version: 2.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 23:42:26 +05:30
Adithya Vardhan
9b7656b4b0
chore: add other cryptocurrency to onchain receive button (#2210) 2026-04-07 19:46:16 +05:30
René Aaron
5f445eb879
feat: increase default app budget to 100k sats / month (#2194)
fix: update default budget to 100k
2026-04-07 19:35:49 +05:30
Adithya Vardhan
de60b2565c
fix: unlock error when node is not started (#2151)
* fix: unlock error when node is not started

* chore: use ErrLNClientNotStarted variable
2026-04-07 17:38:47 +05:30
Adithya Vardhan
51b867e992
fix: make HTTP start JWT secret loading idempotent (#2165)
* fix: make HTTP start JWT secret loading idempotent

* fix: check unlock password before loading jwt

* fix: use mutex to guard jwtsecret in config
2026-04-07 16:39:49 +05:30
frnandu
9e93cff8ea
fix: LND payments made externally from the hub are not visible (#2183)
fix: lnd payments made externally from the hub are not visible

(cherry picked from commit bd0d660faa)

Co-authored-by: anon <anon@anon.com>
2026-04-07 04:40:40 +05:30
Dunsin
e157c288b3
feat: add chain data source and address to about page (#2013)
* feat: add chain data source and address to about page

* fix: only show chainsource for ldk

* fix: redact chain-source address secrets and return complete bitcoind endpoint

* chore: make sanitize function readable and add tests

* fix: align ldk chain data source labels

---------

Co-authored-by: anon <anon@anon.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-04-07 03:25:13 +05:30
René Aaron
17d38850f9
feat: claude.md as symlink to agents.md (#2191)
* feat: claude.md as symlink to agents.md

* fix: use actual symlink for CLAUDE.md instead of include directive

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:57:09 +02:00
frnandu
da5b68da84
feat: add minCltvExpiryDelta for LDK (#2181)
fixes #2065

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Improvements**
* Hold invoices now support an optional minimum CLTV expiry delta with
validation to ensure values stay within protocol limits, allowing finer
control over confirmation timeout behavior.

* **Chores**
  * Updated a direct library dependency to a newer version.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-03 07:19:51 +00:00
Sergey B.
f2ea668df8
Adding checks for ok status code in responses (#2178)
* fix: checks for ok status code in responses

* refactor: replacing native fetch with useSWR

* chore: remove url from logs

* chore: add phoenixd error logs for non-success responses

* chore: add use currencies hook

* chore: use loading from currencies hook and filter out btc

---------

Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-03-31 23:21:26 +05:30
Adithya Vardhan
895edabba7
fix: return app metadata and lud16 in get_info without scope (#2152)
* fix: return app metadata and lud16 in get_info without scope

* chore: remove unnecessary no error check

* chore: rename nodeInfo to infoResponse in get info tests
2026-03-31 21:26:02 +05:30
Zachary Johnson
66151646b5
feat: upgrade to react 19 and add document metadata support (#1993)
* feat: upgrade to react 19; add document metadata support

* chore: add titles to missing pages

* chore: remove unnecessary fragments

---------

Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-03-31 20:52:41 +05:30
Adithya Vardhan
a4563beb37
chore: update Go toolchain to 1.25 and bump golang.org/x/oauth2 package (#2188) 2026-03-31 15:54:07 +05:30
dependabot[bot]
2ea06d3782
build(deps): bump golang.org/x/crypto from 0.48.0 to 0.49.0 (#2147)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.48.0 to 0.49.0.
- [Commits](https://github.com/golang/crypto/compare/v0.48.0...v0.49.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 14:47:54 +05:30
dependabot[bot]
8593847e2c
build(deps): bump github.com/mattn/go-sqlite3 from 1.14.34 to 1.14.38 (#2185)
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.34 to 1.14.38.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.34...v1.14.38)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-sqlite3
  dependency-version: 1.14.38
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 14:36:44 +05:30
dependabot[bot]
85ec65070a
build(deps): bump github.com/BoltzExchange/boltz-client/v2 from 2.11.1 to 2.11.2 (#2186)
build(deps): bump github.com/BoltzExchange/boltz-client/v2

Bumps [github.com/BoltzExchange/boltz-client/v2](https://github.com/BoltzExchange/boltz-client) from 2.11.1 to 2.11.2.
- [Release notes](https://github.com/BoltzExchange/boltz-client/releases)
- [Changelog](https://github.com/BoltzExchange/boltz-client/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BoltzExchange/boltz-client/compare/v2.11.1...v2.11.2)

---
updated-dependencies:
- dependency-name: github.com/BoltzExchange/boltz-client/v2
  dependency-version: 2.11.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 14:29:32 +05:30
dependabot[bot]
b9aeda1dd4
build(deps-dev): bump typescript-eslint from 8.56.1 to 8.57.1 in /frontend (#2159)
build(deps-dev): bump typescript-eslint in /frontend

Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.56.1 to 8.57.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.57.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.57.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 14:28:38 +05:30
dependabot[bot]
68af2bff8b
build(deps-dev): bump globals from 15.15.0 to 17.4.0 in /frontend (#2160)
Bumps [globals](https://github.com/sindresorhus/globals) from 15.15.0 to 17.4.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v15.15.0...v17.4.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 17.4.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 14:24:54 +05:30
dependabot[bot]
890abaf93e
build(deps): bump @radix-ui/react-navigation-menu from 1.2.13 to 1.2.14 in /frontend (#2161)
build(deps): bump @radix-ui/react-navigation-menu in /frontend

Bumps [@radix-ui/react-navigation-menu](https://github.com/radix-ui/primitives) from 1.2.13 to 1.2.14.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-navigation-menu"
  dependency-version: 1.2.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 14:17:30 +05:30
Adithya Vardhan
215e652cb3
chore: bump lucide-react to v1.7.0 (#2187)
* chore: bump lucide-react to v1.7.0

* chore: use Icon suffix for all lucide icons for consistency
2026-03-31 14:16:56 +05:30
dependabot[bot]
59191ad16d
build(deps-dev): bump prettier from 3.6.2 to 3.8.1 in /frontend (#2162)
Bumps [prettier](https://github.com/prettier/prettier) from 3.6.2 to 3.8.1.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.6.2...3.8.1)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.8.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 14:15:43 +05:30
anon
ccc963b1bd fix: uint64(65535) 2026-03-27 14:35:18 +01:00
anon
5a40e5b1c9 fix: uint64(65535) 2026-03-27 14:34:54 +01:00
anon
77b956b510 fix: 65535 2026-03-27 13:35:01 +01:00
anon
a1e9ed0e57 fix: go mod tidy 2026-03-27 13:33:09 +01:00
anon
56a8df5b69 fix: revert 2026-03-27 13:00:45 +01:00
anon
96da6237fc fix: revert 2026-03-27 13:00:33 +01:00
anon
bd0d660faa fix: lnd payments made externally from the hub are not visible 2026-03-27 12:56:10 +01:00
anon
350b72c297 feat: add minCltvExpiryDelta for LDK 2026-03-26 20:02:48 +01:00
saunter
7543313d60
feat: refresh Support Open Source widget on Home (#2171)
* feat: refresh support widget on Home

Rename Support Alby to Support Open Source, apply the updated card UI,
and place it in the right column under Recently Used Apps.

Made-with: Cursor

* fix: remove obsolete classes

---------

Co-authored-by: René Aaron <rene@twentyuno.net>
2026-03-26 15:01:22 +01:00
frnandu
928246d472
feat: add minCltvExpiryDelta for LND (#2139)
* feat: add minCltvExpiryDelta for LND

* feat: improve

* feat: test LastMinCltvExpiryDelta

* feat: fixes

* feat: fixes

* chore: address minor naming issue

* chore: run mockery

---------

Co-authored-by: anon <anon@anon.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-03-25 23:06:57 +05:30
frnandu
354099cbff
feat: encrypt xpub for auto swap outs (#1973)
* feat: encrypt xpub for auto swap out

* fix: include encryptionKey in the cache key (sha256)

* fix: make cache 2 level nested map

* chore: comment

* fix: missing import

* fix: use ValidateXpub

* fix: cleanup

* fix: cleanup

* fix: better name

* fix: cleanup

* fix: cleanup

* fix: cleanup

* fix: open dialog to ask password on autoswap xpub

* fix: reject extended private keys

* fix: fix amount comp

* chore: improve autoswap destination handling and form submission flow

* fix: clear cached xpub when enabling autoswaps

* fix: autoswap destination validation and xpub handling

* chore: add mutex for auto swap xpub

* chore: add copy icon for destination in auto swap info

---------

Co-authored-by: fmar <fmar@fmar.dev>
Co-authored-by: anon <anon@anon.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-03-25 22:28:16 +05:30
Adithya Vardhan
06b21139bc
fix: avoid blocking startup on boltz websocket (#2167) 2026-03-25 09:27:15 +05:30
saunter
c0e8df63c4
feat: remove card shadows and unify card grid spacing (#2154)
* style(frontend): remove card shadows and set card grid gap to 3

- Use shadow-none on shadcn Card and drop shadow-xs on UpgradeCard
- Standardize spacing between card groups to gap-3 (12px) across grids

Made-with: Cursor

* ci: fix macos gha builds for wails and fork prs

Select Xcode.app before CGO so Wails WebKit uses the full SDK (avoids
Foundation parsed as C on CLT-only toolchains).

Skip Apple signing/notarize/DMG when the PR head is a fork; upload an
unsigned zip instead. Add fail-fast: false to the HTTP build matrix.

Made-with: Cursor

* ci: require build-release for workflow_call macos signing

Gate cert import, codesign, DMG, notarize, and signed DMG upload on
inputs.build-release when the event is workflow_call. Keeps fork PR
unsigned zip path; push and internal PR behavior unchanged.

Made-with: Cursor

* ci: revert macos workflow signing fixes

Revert CI changes that attempted to fix macOS build failures;
keep PRs focused on frontend changes.

Made-with: Cursor

* fix: cleanup

---------

Co-authored-by: René Aaron <rene@twentyuno.net>
2026-03-24 11:43:15 +01:00
Adithya Vardhan
3cacb181b8
fix: update wails in workflow to v2.11.0 (#2156) 2026-03-23 16:42:58 +05:30
Adithya Vardhan
25c027b6d7
fix: handle invalid connect peer input gracefully (#2150) 2026-03-20 18:09:13 +05:30
Bitrequest
9719f66425
feat: add Bitrequest to App Store (#2148)
* Add Bitrequest app object to SuggestedAppData.tsx

* Add bitrequest.png logo to assets

* chore: minor guide changes

---------

Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-03-19 03:57:15 +05:30
dependabot[bot]
956c841b5e
build(deps): bump github.com/labstack/echo/v4 from 4.13.4 to 4.15.1 (#2110)
Bumps [github.com/labstack/echo/v4](https://github.com/labstack/echo) from 4.13.4 to 4.15.1.
- [Release notes](https://github.com/labstack/echo/releases)
- [Changelog](https://github.com/labstack/echo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/labstack/echo/compare/v4.13.4...v4.15.1)

---
updated-dependencies:
- dependency-name: github.com/labstack/echo/v4
  dependency-version: 4.15.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 02:29:09 +05:30
dependabot[bot]
1625273af0
build(deps-dev): bump lint-staged from 15.5.2 to 16.3.1 in /frontend (#2113)
Bumps [lint-staged](https://github.com/lint-staged/lint-staged) from 15.5.2 to 16.3.1.
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v15.5.2...v16.3.1)

---
updated-dependencies:
- dependency-name: lint-staged
  dependency-version: 16.3.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 02:18:02 +05:30
dependabot[bot]
412abdd23d
build(deps-dev): bump @commitlint/cli from 19.8.1 to 20.4.2 in /frontend (#2114)
Bumps [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) from 19.8.1 to 20.4.2.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.4.2/@commitlint/cli)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.4.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 02:02:38 +05:30
dependabot[bot]
8b1504ad4f
build(deps-dev): bump @types/node from 25.2.3 to 25.3.3 in /frontend (#2112)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.2.3 to 25.3.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.3.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 20:08:26 +05:30
dependabot[bot]
ee9a35b629
build(deps): bump @getalby/sdk from 6.0.1 to 7.0.0 in /frontend (#2111)
Bumps [@getalby/sdk](https://github.com/getAlby/js-sdk) from 6.0.1 to 7.0.0.
- [Release notes](https://github.com/getAlby/js-sdk/releases)
- [Commits](https://github.com/getAlby/js-sdk/compare/v6.0.1...v7.0.0)

---
updated-dependencies:
- dependency-name: "@getalby/sdk"
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 19:25:25 +05:30
René Aaron
c8ccdcb9b7
feat: agents.md (#2132)
* feat: agents.md

* fix: wrap in text blocks
2026-03-17 10:38:50 +01:00
molty21
e963d6a2bf
feat: make installation scripts agent-friendly with CLI args (#2144)
* feat: make installation scripts agent-friendly with CLI args

Add command-line argument support to install.sh and update.sh for both
x86_64 and aarch64 architectures:

- -d, --install-dir DIR    Set installation directory
- -s, --systemd            Auto-setup systemd service (install only)
- --no-systemd             Skip systemd setup (install only)
- -y, --yes                Non-interactive mode (auto-confirm prompts)
- -h, --help               Show usage information

This allows agents and automation tools to run the scripts without
interactive prompts, making them suitable for CI/CD pipelines and
automated deployments.

* fix: add argument validation for --install-dir flag

Add validation to ensure -d/--install-dir is provided with a valid value:
- Checks that argument exists (not empty)
- Checks that argument doesn't start with '-' (not another flag)
- Exits with error message if validation fails

Fixes CodeRabbit review feedback on PR #2144.

* feat: add --skip-verify flag to skip verification step

Add --skip-verify flag to install and update scripts:
- linux-x86_64/install.sh
- linux-x86_64/update.sh
- linux-aarch64/install.sh
- linux-aarch64/update.sh

When --skip-verify is passed, the scripts skip downloading and
calling verify.sh entirely.

Usage: ./install.sh --skip-verify

* fix: ensure install dir does not contain whitespace

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Apply suggestion from @rolznz

* Apply suggestion from @coderabbitai[bot]

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Apply suggestion from @rolznz

* Update scripts/linux-aarch64/install.sh

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: molty21 <moltbot21@agentmail.to>
Co-authored-by: Roland <33993199+rolznz@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-16 19:24:43 +07:00
Adithya Vardhan
15ef0fb68e
fix: register swap listeners before websocket subscribe (#2141)
* fix: register swap listeners before websocket subscribe

* fix: only pay swap invoice when lookup returns not found
2026-03-14 00:29:57 +07:00
Adithya Vardhan
fd86625984
fix: broadcast swap-out claim tx as soon as lockup hits mempool (#2137)
* fix: broadcast swap-out claim tx as soon as lockup hits mempool

* fix: broadcast claim transaction on both mempool or confirmed update
2026-03-13 18:14:52 +07:00
Anshuman
0b556a9225
feat: add Alby Hub Name to settings about page (#2126) 2026-03-13 12:25:47 +07:00
Adithya Vardhan
72ad7d849c
fix: remove bitcoin maxi mode and always show crypto actions (#2135) 2026-03-13 10:17:14 +07:00
dependabot[bot]
92bd197ad6
build(deps): bump github.com/BoltzExchange/boltz-client/v2 from 2.10.0 to 2.11.1 (#2127)
build(deps): bump github.com/BoltzExchange/boltz-client/v2

Bumps [github.com/BoltzExchange/boltz-client/v2](https://github.com/BoltzExchange/boltz-client) from 2.10.0 to 2.11.1.
- [Release notes](https://github.com/BoltzExchange/boltz-client/releases)
- [Changelog](https://github.com/BoltzExchange/boltz-client/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BoltzExchange/boltz-client/compare/v2.10.0...v2.11.1)

---
updated-dependencies:
- dependency-name: github.com/BoltzExchange/boltz-client/v2
  dependency-version: 2.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-12 15:05:32 +05:30
im-adithya
3ff3039f0a fix: add missing Castamatic NWC wallet step 2026-03-12 13:27:15 +05:30
Franco Solerio
35ad0f1b2d
feat: add castamatic to the app store (#2124) 2026-03-12 13:19:36 +05:30
Adithya Vardhan
580c465f66
fix: increase the default Alby Account budget from 150k to 250k (#2136)
* fix: increase the default Alby Account budget from 150k to 250k

* chore: add 250k to budget options

* chore: remove 150k option from budgets
2026-03-12 11:58:21 +05:30
René Aaron
23026e3920
feat: add referral program to earn page (#2120)
* feat: add referral program to earn page

* fix: copy

* fix: copy

* fix: make Alby earn screen responsive

---------

Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-03-12 11:57:53 +05:30
René Aaron
6233904149
feat: 404 page (#2122) 2026-03-05 18:29:03 +07:00
Roland
78d5ecaa96
chore: update PPQ to use alby affiliate link (#2117) 2026-03-04 18:02:38 +01:00
Roland
b984d7a913
fix: sub-wallet total spent in subwallet page ui (#2106) 2026-03-02 10:53:10 +07:00
Roland
4cb794b332
chore: update additional places to support multiple relays (#2104) 2026-02-28 13:30:48 +07:00
Naveen Kumar
6795640820
feat: add custom message support for subwallet transfers (#2069)
* feat: add custom message support for subwallet transfers

- Add optional message field to TransferRequest model
- Update Transfer function to use custom message with 'transfer' as fallback
- Update HTTP and Wails handlers to extract and pass message parameter
- Add message input field to IsolatedAppTopupDialog and IsolatedAppDrawDownDialog
- Maintains backward compatibility with empty message defaulting to 'transfer'

Closes #2066

* chore: rename

* chore: rename and simplify

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
2026-02-28 13:25:23 +07:00
Roland
a3bfd5eac1
feat: add second relay to default config (#2103) 2026-02-28 12:11:21 +07:00
Adithya Vardhan
7b9a141933
feat: add alby cli skill internal app (#2102)
* feat: add alby cli skill internal app

* chore: improvements for alby cli npx instructions

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
2026-02-28 12:04:37 +07:00
Adithya Vardhan
7bed6ecd86
feat: add fixed float along with bitcoin maxi mode (#2094)
* feat: add option to purchase channels with other crypto

* feat: add option to receive via other crypto

* feat: add crypto swap alert if sending fails

* feat: add bitcoin maxi mode

* fix: add invoice description on swapping

* chore: align top up wording everywhere

* chore: stop showing alert if recipient input is edited

* chore: change swap fee to 1%

* feat: add fixed float option to deposit and withdraw pages

* chore: extract fixed float swap in flow

* chore: add FixedFloatButton component

* chore: store bitcoin maxi setting in config

* fix: tests using mockery

* fix: issues
2026-02-28 11:54:28 +07:00
Adithya Vardhan
3aa167a7c4
fix: add err handling to db queries (#2064)
* fix: add err handling to db queries

* chore: add error handling to GetApp

* chore: wrap the original error

* fix: retrieve balance after err != nil check

* chore: minor code cleanup

* chore: add error handling to app permission listing in GetApp

* chore: convert GetBudgetUsageSat to GetBudgetUsage

* chore: add tests for GetBudgetUsage

* chore: separate budget window tests

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
2026-02-27 16:31:25 +07:00
Adithya Vardhan
4a924c3d11
feat: add lnvps to app store (#2100) 2026-02-27 16:22:30 +07:00
Adithya Vardhan
6f74804d17
feat: add ppq ai to app store (#2099)
* feat: add ppq ai to app store

* chore: improve PPQ app copy

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
2026-02-27 16:19:43 +07:00
Adithya Vardhan
53c10f1be4
feat: add alby sandbox to app store (#2098) 2026-02-27 15:54:47 +07:00
Adithya Vardhan
4efe735acf
fix: prevent LN client access during shutdown (#2096)
* fix: prevent LN client access during shutdown

* chore: run mockery

* chore: push lnClientShuttingDown inside stopLNClient

* fix: use atomic bool for synced access
2026-02-27 15:45:09 +07:00
Adithya Vardhan
3392e75a0c
fix: detect confirmed onchain deposits after tab switch (#2092)
* fix: detect confirmed onchain deposits after tab switch

* fix: filter confirmed utxos by start time
2026-02-27 12:20:33 +07:00
Roland
b49818de07
fix: intercept self hold payments based on invoice rather than payment hash (#2027)
* fix: intercept self hold payments based on invoice rather than payment hash

* fix: generate test invoices with long expiry

* fix: add timeout seconds for standard lnd payments
2026-02-27 10:22:24 +05:30
Adithya Vardhan
778f83237a
fix: change responsive button breakpoints and use them where necessary (#2093)
* fix: use md breakpoint for responsive buttons

* fix: replace header buttons with responsive buttons
2026-02-26 14:35:49 +05:30
Adithya Vardhan
a2af8fd598
fix: return optional total balance in list apps response for subwallets (#2057)
* fix: return optional total balance in list apps response for subwallets

* chore: add error handling to subwallet balance query

* chore: add METADATA_APPSTORE_APP_ID_KEY constant

* chore: add MAX_FREE_SUBWALLETS constant

* chore: use subwallet query and total count for limit check
2026-02-26 12:25:06 +05:30
Sergey B.
9701c726ac
Making shell scripts POSIX compliant (#2015)
* feat: usage of rsync for backups

* refactor: making scripts POSIX compliant
2026-02-26 12:19:30 +07:00
dependabot[bot]
c64f31f3f7
build(deps): bump github.com/mattn/go-sqlite3 from 1.14.32 to 1.14.34 (#2072)
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.32 to 1.14.34.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.32...v1.14.34)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-sqlite3
  dependency-version: 1.14.34
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 09:23:07 +05:30
René Aaron
b1cddd2719
feat: add trustpilot to review page again (#2089)
* feat: add trustpilot to review page again

* fix: review feedback
2026-02-26 09:14:42 +05:30
Anshuman
0a11125f0e
feat: remove default gossip peers (#2090)
feat: remove default gossip peers for LDK backend
2026-02-26 09:07:09 +05:30
Dunsin
3f63251ea3
fix: convert spaces to underscores in subwallet lightning address suggestion (#2086) 2026-02-26 08:35:39 +05:30
dependabot[bot]
3fcb2ba12e
build(deps): bump golang.org/x/crypto from 0.45.0 to 0.48.0 (#2073)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.45.0 to 0.48.0.
- [Commits](https://github.com/golang/crypto/compare/v0.45.0...v0.48.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 21:48:20 +05:30
dependabot[bot]
c9a140c254
build(deps): bump github.com/golang-jwt/jwt/v5 from 5.3.0 to 5.3.1 (#2074)
Bumps [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) from 5.3.0 to 5.3.1.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Commits](https://github.com/golang-jwt/jwt/compare/v5.3.0...v5.3.1)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v5
  dependency-version: 5.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 21:40:33 +05:30
Adithya Vardhan
58ac083eb6
chore: migrate to eslint v10 (#2091)
* chore: migrate to eslint v10

* fix: linting issues
2026-02-25 21:28:25 +05:30
dependabot[bot]
468666a5dc
build(deps): bump github.com/sirupsen/logrus from 1.9.3 to 1.9.4 (#2076)
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.9.3 to 1.9.4.
- [Release notes](https://github.com/sirupsen/logrus/releases)
- [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sirupsen/logrus/compare/v1.9.3...v1.9.4)

---
updated-dependencies:
- dependency-name: github.com/sirupsen/logrus
  dependency-version: 1.9.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 21:07:12 +05:30
dependabot[bot]
e7c69d8c32
build(deps): bump @radix-ui/react-checkbox from 1.3.2 to 1.3.3 in /frontend (#2077)
build(deps): bump @radix-ui/react-checkbox in /frontend

Bumps [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives) from 1.3.2 to 1.3.3.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-checkbox"
  dependency-version: 1.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 20:32:36 +05:30
dependabot[bot]
39914fe642
build(deps): bump tailwind-merge from 3.3.1 to 3.4.1 in /frontend (#2078)
Bumps [tailwind-merge](https://github.com/dcastil/tailwind-merge) from 3.3.1 to 3.4.1.
- [Release notes](https://github.com/dcastil/tailwind-merge/releases)
- [Commits](https://github.com/dcastil/tailwind-merge/compare/v3.3.1...v3.4.1)

---
updated-dependencies:
- dependency-name: tailwind-merge
  dependency-version: 3.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 20:29:46 +05:30
dependabot[bot]
b930a488e6
build(deps-dev): bump vite-tsconfig-paths from 5.1.4 to 6.1.1 in /frontend (#2079)
build(deps-dev): bump vite-tsconfig-paths in /frontend

Bumps [vite-tsconfig-paths](https://github.com/aleclarson/vite-tsconfig-paths) from 5.1.4 to 6.1.1.
- [Release notes](https://github.com/aleclarson/vite-tsconfig-paths/releases)
- [Commits](https://github.com/aleclarson/vite-tsconfig-paths/compare/v5.1.4...v6.1.1)

---
updated-dependencies:
- dependency-name: vite-tsconfig-paths
  dependency-version: 6.1.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 20:27:06 +05:30
dependabot[bot]
ae1ebaafd7
build(deps-dev): bump @types/node from 24.7.2 to 25.2.3 in /frontend (#2080)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.7.2 to 25.2.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.2.3
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 20:25:00 +05:30
Roland
4d563eb2f7
chore: bump ldk node dependencies (#2054) 2026-02-13 14:32:09 +07:00
Adithya Vardhan
52c9e5ef13
chore: remove unused frontend dependencies (#2063) 2026-02-12 15:17:52 +05:30
dependabot[bot]
fe23df1752
build(deps-dev): bump vite-plugin-pwa from 0.20.5 to 1.1.0 in /frontend (#1934)
* build(deps-dev): bump vite-plugin-pwa from 0.20.5 to 1.1.0 in /frontend

Bumps [vite-plugin-pwa](https://github.com/vite-pwa/vite-plugin-pwa) from 0.20.5 to 1.1.0.
- [Release notes](https://github.com/vite-pwa/vite-plugin-pwa/releases)
- [Commits](https://github.com/vite-pwa/vite-plugin-pwa/compare/v0.20.5...v1.1.0)

---
updated-dependencies:
- dependency-name: vite-plugin-pwa
  dependency-version: 1.1.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: upgrade to v1.2.0

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-02-12 14:11:02 +05:30
dependabot[bot]
87d04668e0
build(deps): bump @radix-ui/react-label from 2.1.7 to 2.1.8 in /frontend (#1932)
Bumps [@radix-ui/react-label](https://github.com/radix-ui/primitives) from 2.1.7 to 2.1.8.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 13:09:41 +05:30
dependabot[bot]
61ab9d5610
build(deps): bump @stepperize/react from 5.1.8 to 5.1.9 in /frontend (#1936)
Bumps [@stepperize/react](https://github.com/damianricobelli/stepperize/tree/HEAD/packages/react) from 5.1.8 to 5.1.9.
- [Release notes](https://github.com/damianricobelli/stepperize/releases)
- [Changelog](https://github.com/damianricobelli/stepperize/blob/main/packages/react/CHANGELOG.md)
- [Commits](https://github.com/damianricobelli/stepperize/commits/@stepperize/react@5.1.9/packages/react)

---
updated-dependencies:
- dependency-name: "@stepperize/react"
  dependency-version: 5.1.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 13:03:14 +05:30
Adithya Vardhan
f6d1d60b90
fix: set max height full on sheet component (#2062) 2026-02-12 12:37:21 +05:30
dependabot[bot]
0baa697433
build(deps): bump github.com/BoltzExchange/boltz-client/v2 from 2.9.1 to 2.10.0 (#1955)
build(deps): bump github.com/BoltzExchange/boltz-client/v2

Bumps [github.com/BoltzExchange/boltz-client/v2](https://github.com/BoltzExchange/boltz-client) from 2.9.1 to 2.10.0.
- [Release notes](https://github.com/BoltzExchange/boltz-client/releases)
- [Changelog](https://github.com/BoltzExchange/boltz-client/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BoltzExchange/boltz-client/compare/v2.9.1...v2.10.0)

---
updated-dependencies:
- dependency-name: github.com/BoltzExchange/boltz-client/v2
  dependency-version: 2.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-02-12 12:01:50 +05:30
Adithya Vardhan
9390eecf14
fix: update no-token API tests to expect 401 (#2059) 2026-02-12 09:56:48 +05:30
dependabot[bot]
169b0bacf9
build(deps): bump github.com/labstack/echo-jwt/v4 from 4.3.1 to 4.4.0 (#1954)
Bumps [github.com/labstack/echo-jwt/v4](https://github.com/labstack/echo-jwt) from 4.3.1 to 4.4.0.
- [Release notes](https://github.com/labstack/echo-jwt/releases)
- [Changelog](https://github.com/labstack/echo-jwt/blob/main/CHANGELOG.md)
- [Commits](https://github.com/labstack/echo-jwt/compare/v4.3.1...v4.4.0)

---
updated-dependencies:
- dependency-name: github.com/labstack/echo-jwt/v4
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-02-12 09:39:07 +05:30
dependabot[bot]
890d9a0a43
build(deps): bump golang.org/x/crypto from 0.44.0 to 0.45.0 (#1937)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.44.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.44.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-02-12 09:29:50 +05:30
dependabot[bot]
2c10cd7b18
build(deps): bump golang.org/x/oauth2 from 0.33.0 to 0.34.0 (#1964)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.33.0 to 0.34.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.33.0...v0.34.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-version: 0.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-02-12 09:28:38 +05:30
dependabot[bot]
a92a32e922
build(deps): bump google.golang.org/grpc from 1.76.0 to 1.77.0 (#1938)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.76.0 to 1.77.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.76.0...v1.77.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.77.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-02-12 09:07:39 +05:30
klabo
3bdbe870a9
feat: add Alby CLI to the app store (#2049)
* feat: add Alby CLI to the app store

Closes #2047

Adds Alby CLI (NWC CLI with lightning tools) to the Hub app store
under wallet-interfaces category. Includes install guide (npm/npx)
and connection guide (NWC_URL env var or -c flag).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: hide connection QR code for CLI app

CLI users will copy-paste the connection secret, so the QR code adds no value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: modify finalize guide

---------

Co-authored-by: Joel Klabo <max@klabo.world>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-02-10 23:17:14 +05:30
klabo
4353aa4e57
feat: add HIDE_UPDATE_BANNER environment variable (#2051)
* feat: add HIDE_UPDATE_BANNER environment variable

Add a new HIDE_UPDATE_BANNER env var that allows platform operators
(e.g. Start9) to suppress the built-in version update banner when
they provide their own update notification mechanism.

When set to true, the "What's New" widget is hidden and the header
banner only shows for VSS migration notices. The version comparison
against the Alby API is skipped entirely.

Closes #2048

* chore: simplify

* chore: undo changes

---------

Co-authored-by: Joel Klabo <max@klabo.world>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
2026-02-10 22:30:08 +05:30
klabo
801f517478
refactor: remove unused LNClient.ListTransactions method (#2046)
Transactions are listed from the database via transactionsService, not
from the LN backend. The LNClient.ListTransactions method was never
called and each backend's implementation was dead code.

Closes #2045

Co-authored-by: Joel Klabo <max@klabo.world>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 21:01:32 +05:30
Utkarsh raj
cccc39d018
fix: handle invalid zap data in transaction list (#2033)
* fix: handle invalid zap data in transaction list

* style: apply prettier formatting

---------

Co-authored-by: utkarshraj0001 <utkarshrajpandey0001@gmail.com>
2026-02-10 12:17:51 +07:00
Sergey B.
5281681776
feat: remove hasLdkDir check (#2014)
* feat: remove hasLdkDir check

* chore: regenerate mocks

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
2026-02-09 21:39:25 +07:00
Roland
2174f02e6c
docs: add LDK signet config (#2021) 2026-01-23 10:17:19 +07:00
Roland
71f0f05db6
feat: add sat sorter to App Store (#2022)
* Add files via upload

Add Sat Sorter logo

* Update SuggestedAppData.tsx

Add Sat Sorter to suggested apps

* Update frontend/src/components/connections/SuggestedAppData.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update SuggestedAppData.tsx

* Update SuggestedAppData.tsx

Fix Prettier formatting for Sat Sorter entry

* fix: linting error

---------

Co-authored-by: tom-morrow15 <devin@millerfam.co>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-01-23 10:16:54 +07:00
Keshav
797faa70fb
fix(ui): enable keyboard submission for dialogs #760 (#2005)
* fix(ui): enable keyboard submission for dialogs #760

* fix: add global max dialog height

* fix: routing fee dialog submit

* chore: remove unused code

* chore: correct copy for exporting pathfinding scores

* fix: return correct error message when refunding swap that is not found

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
2026-01-12 11:52:26 +07:00
Adithya Vardhan
ceb3a5f490
fix: conditional query param in swap in status navigation (#2011) 2026-01-12 10:45:00 +07:00
Roland
a5d45cbac0
feat: add fountain to the app store (#2009)
Also some minor fixes to other apps, and replaced testflight links with app store links
2026-01-07 21:26:23 +07:00
HODLmeTight
0061e94594
Add TunnelSats to suggested apps (#2001) 2026-01-07 21:00:36 +07:00
Roland
c5b7282865
chore: reduce alby oauth permissions (#2008)
(custodial alby account is shut down so there is no need to fetch balance or transfer funds now)
2026-01-07 20:34:17 +07:00
Roland
191958570a
feat: add alert to node page when user has any large LDK channel monitors (#2007) 2026-01-07 20:24:22 +07:00
Roland
47b91c445a
fix: increase vss client timeout (#2002) 2026-01-06 13:33:48 +07:00
Roland
4a9e6868fa
fix: max amount when swapping in from external wallet (#2004) 2026-01-06 11:42:45 +05:30
Roland
e80261a545
fix: only clear ldk VSS migration config value after successfully starting node (#2000) 2026-01-05 18:11:14 +07:00
Roland
1083047624
fix: github docker build runs out of space (#1991) 2025-12-17 01:35:32 +07:00
490 changed files with 99946 additions and 12983 deletions

View file

@ -6,6 +6,8 @@ AUTO_LINK_ALBY_ACCOUNT=false
# Optionally set LDK debug log level to get more info
#LDK_LOG_LEVEL=5
# Optionally set Bark debug log level to get more info
#BARK_LOG_LEVEL=5
# Optionally set Main application debug log level to get more info
#LOG_LEVEL=5
@ -40,4 +42,13 @@ FRONTEND_URL=http://localhost:5173
# Boltz API
#BOLTZ_API=https://api.testnet.boltz.exchange
#NETWORK=testnet
#NETWORK=testnet
# CLN Backend
#LN_BACKEND_TYPE=CLN
# CLN's grpc-host:grpc-port
#CLN_ADDRESS=127.0.0.1:9737
# CLN's lightning directory containing the grpc certificates, usually ~/.lightning/<network>/
#CLN_LIGHTNING_DIR=/path/to/.lightning/bitcoin
# CLN's hold plugin https://github.com/BoltzExchange/hold gRPC address
#CLN_ADDRESS_HOLD=127.0.0.1:9738

View file

@ -4,6 +4,8 @@ updates:
directory: /
schedule:
interval: weekly
ignore:
- dependency-name: google.golang.org/grpc
- package-ecosystem: npm
directory: /frontend
schedule:

View file

@ -9,6 +9,9 @@ jobs:
TAG: ${{ github.ref_name }}
runs-on: ubuntu-22.04
steps:
# see https://github.com/orgs/community/discussions/25678#discussioncomment-5242449
- name: Delete huge unnecessary tools folder
run: rm -rf /opt/hostedtoolcache
- uses: actions/checkout@v4
name: Check out code
- name: Install Go

View file

@ -25,6 +25,7 @@ on:
jobs:
build:
strategy:
fail-fast: false
matrix:
build:
[
@ -85,7 +86,7 @@ jobs:
- name: Setup NodeJS
uses: actions/setup-node@v4
with:
node-version: "20.x"
node-version: "22.x"
- name: Run tests
run: mkdir frontend/dist && touch frontend/dist/tmp && go test ./...
@ -128,7 +129,15 @@ jobs:
run: go build ${{ env.GOTAGS }} -o build/bin/${{ env.PACKAGE_NAME }}/bin/${{ env.EXEC_NAME }} -ldflags "-X 'github.com/getAlby/hub/version.Tag=${{ env.TAG }}'" cmd/http/main.go
- name: Import Code-Signing Certificates for macOS
if: runner.os == 'macOS'
if: >-
runner.os == 'macOS' &&
(
!github.event.pull_request ||
(
github.event.pull_request.head.repo.full_name == github.repository &&
github.actor != 'dependabot[bot]'
)
)
uses: Apple-Actions/import-codesign-certs@v3
with:
# The certificates in a PKCS12 file encoded as a base64 string
@ -165,7 +174,15 @@ jobs:
shell: bash
- name: Sign the MacOS binary and libraries
if: runner.os == 'macOS'
if: >-
runner.os == 'macOS' &&
(
!github.event.pull_request ||
(
github.event.pull_request.head.repo.full_name == github.repository &&
github.actor != 'dependabot[bot]'
)
)
run: |
/usr/bin/codesign -s "Developer ID Application: Alby Inc." -f -v --deep --timestamp --options runtime ./build/bin/${{ env.PACKAGE_NAME }}/bin/${{ env.EXEC_NAME }}
/usr/bin/codesign -s "Developer ID Application: Alby Inc." -f -v --deep --timestamp --options runtime ./build/bin/${{ env.PACKAGE_NAME }}/lib/*.dylib
@ -189,7 +206,16 @@ jobs:
cd ../../..
- name: Notarize the zip file
if: runner.os == 'macOS' && inputs.build-release
if: >-
runner.os == 'macOS' &&
inputs.build-release &&
(
!github.event.pull_request ||
(
github.event.pull_request.head.repo.full_name == github.repository &&
github.actor != 'dependabot[bot]'
)
)
run: |
echo "Notarizing Zip Files"
gon -log-level=info -log-json ./build/darwin/http/gon-notarize.json

View file

@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20.x
node-version: 22.x
cache: "yarn"
cache-dependency-path: frontend/yarn.lock

View file

@ -73,10 +73,10 @@ jobs:
- name: Setup NodeJS
uses: actions/setup-node@v4
with:
node-version: "20.x"
node-version: "22.x"
- name: Install Wails
run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.10.2
run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.14.0
shell: bash
- name: Install Linux Wails deps
@ -129,7 +129,15 @@ jobs:
shell: bash
- name: Import Code-Signing Certificates for macOS
if: runner.os == 'macOS'
if: >-
runner.os == 'macOS' &&
(
!github.event.pull_request ||
(
github.event.pull_request.head.repo.full_name == github.repository &&
github.actor != 'dependabot[bot]'
)
)
uses: Apple-Actions/import-codesign-certs@v3
with:
# The certificates in a PKCS12 file encoded as a base64 string
@ -190,7 +198,15 @@ jobs:
mv ./build/out/${{ env.PACKAGE_NAME }}.tar.bz2 ./build/bin/
- name: Sign the macOS binary
if: runner.os == 'macOS'
if: >-
runner.os == 'macOS' &&
(
!github.event.pull_request ||
(
github.event.pull_request.head.repo.full_name == github.repository &&
github.actor != 'dependabot[bot]'
)
)
run: |
echo "Signing Package"
/usr/bin/codesign -s "Developer ID Application: Alby Inc." -f -v --deep --timestamp --options runtime --entitlements ./build/darwin/entitlements.plist "./build/bin/${{ env.EXEC_NAME }}.app"
@ -222,7 +238,15 @@ jobs:
EOF
- name: Notarize the DMG image
if: runner.os == 'macOS'
if: >-
runner.os == 'macOS' &&
(
!github.event.pull_request ||
(
github.event.pull_request.head.repo.full_name == github.repository &&
github.actor != 'dependabot[bot]'
)
)
run: |
echo "Notarizing Zip Files"
gon -log-level=info -log-json ./build/darwin/gon-notarize.json

3
.gitignore vendored
View file

@ -30,4 +30,5 @@ glalby
*.db-shm
*.db-wal
*.db-journal
albyhub-data
albyhub-data
.claude/worktrees

232
AGENTS.md Normal file
View file

@ -0,0 +1,232 @@
# AGENTS.md
This file provides guidance for AI coding agents working on the Alby Hub repository.
## Project Overview
Alby Hub is a self-custodial **Nostr Wallet Connect (NWC)** service that bridges Lightning Network wallets with applications supporting the NIP-47 protocol. It supports multiple Lightning backends (LDK, LND, Phoenixd, Cashu) and runs as either a web server or a desktop app (via Wails).
## Tech Stack
- **Backend:** Go 1.25, Echo v4, GORM, SQLite (default) / PostgreSQL
- **Frontend:** React 18, TypeScript, Vite, Tailwind CSS 4, shadcn/ui, Radix UI, Zustand, SWR
- **Desktop:** Wails v2 (produces native desktop app using Go + web frontend)
- **Lightning:** LDK (embedded), LND (gRPC), Phoenixd, Cashu
- **Protocol:** Nostr NIP-47 (Nostr Wallet Connect)
## Project Structure
```text
hub/
├── api/ # HTTP API handlers and request/response models
├── alby/ # Alby account integration (OAuth, backups)
├── apps/ # App connection management
├── cmd/http/main.go # HTTP server entry point
├── config/ # Configuration management
├── db/ # Database layer, migrations, queries
├── events/ # Event pub/sub system
├── frontend/ # React frontend (see below)
├── http/ # HTTP service router
├── lnclient/ # LN abstraction interface + implementations
│ ├── ldk/ # Embedded LDK node
│ ├── lnd/ # LND gRPC client
│ ├── phoenixd/ # Phoenixd client
│ └── cashu/ # Cashu client
├── nip47/ # NIP-47 protocol implementation
│ ├── controllers/ # Per-method request handlers
│ ├── permissions/ # Permission validation
│ └── cipher/ # NIP-04 encryption
├── service/ # Core service orchestration
├── swaps/ # Boltz atomic swap integration
├── transactions/ # Transaction tracking and metadata
├── tests/ # Test helpers and utilities
└── wails/ # Wails desktop-specific handlers
```
### Frontend Structure
```text
frontend/src/
├── components/ # Reusable UI components
├── screens/ # Page-level route components
├── contexts/ # React context providers
├── hooks/ # Custom React hooks
├── state/ # Zustand client state stores
├── lib/ # Auth, backend type helpers
├── utils/ # Shared utilities (request.ts, swr.ts, formatting, etc.)
├── types.ts # Shared TypeScript types
└── routes.tsx # Route definitions
frontend/platform_specific/
├── http/ # Web-specific utilities (copied at build time)
└── wails/ # Desktop-specific utilities (copied at build time)
```
## Development Setup
### Prerequisites
- Node.js 20+
- Yarn
### Running in HTTP Mode (Primary)
```bash
# Terminal 1 Frontend (port 5173)
cd frontend
yarn install
yarn dev:http
# Terminal 2 Backend (port 8080)
cp .env.example .env # configure as needed
go run cmd/http/main.go
```
### Running in Desktop Mode (Wails)
```bash
wails dev -tags "wails"
```
**Wails versions must stay in sync:** the `github.com/wailsapp/wails/v2` version in `go.mod` and the Wails CLI version installed in `.github/workflows/wails.yml` (`go install ...cmd/wails@vX.Y.Z`) must match. When bumping one, always update the other — this is a common source of drift (e.g. via Dependabot updates to `go.mod` only).
## Testing
### Go Backend
```bash
# Run all tests
go test ./...
# Run specific test by name
go test ./... -run TestHandleGetInfoEvent
# Run with PostgreSQL (optional)
export TEST_DATABASE_URI="postgresql://user:password@localhost:5432/postgres"
go test ./...
```
Mocks are generated with `mockery` (config in `.mockery.yaml`); run it after changing any interface.
### Frontend
```bash
cd frontend
yarn lint # ESLint + TypeScript type check + Prettier
yarn tsc:compile # TypeScript only
yarn format # Prettier only
```
No Jest/Vitest tests exist; frontend quality is enforced via linting.
## Building
```bash
# HTTP production build
cd frontend && yarn build:http
go build -o main cmd/http/main.go
# Docker
docker build . -t albyhub:latest
```
## Key Architecture Patterns
### Request Flow
```text
HTTP Request / NIP-47 Nostr Event
→ HTTP Handler / NIP-47 Event Handler
→ api/ package (business logic)
→ LNClient interface
→ Backend implementation (LDK/LND/Phoenixd/Cashu)
```
### Event System
Services communicate via `events/` pub/sub. Prefer publishing events over direct inter-service calls. Key events use the `nwc_*` prefix (e.g., `nwc_payment_sent`, `nwc_payment_received`).
### Platform-Specific Frontend Code
Code under `frontend/platform_specific/http/` and `frontend/platform_specific/wails/` is swapped at build time. Any platform-specific frontend logic must have both variants.
### LNClient Interface
`lnclient/models.go` defines the interface all backends must implement. Changes to this interface require updates to all four implementations (LDK, LND, Phoenixd, Cashu) and their mocks.
## Database
- **Migrations:** `db/migrations/` — always add new migrations here; never modify existing ones.
- **SQLite:** WAL mode, 5s busy timeout, 20MB cache. Default for development and most deployments.
- **PostgreSQL:** Supported for production. If touching DB code, test with both.
- **ORM:** GORM with `go-gormigrate`. Use GORM conventions for new models.
## Coding Conventions
### Go
- Idiomatic Go; `gofmt` formatting expected.
- Structured logging via `logrus` with contextual fields — no `fmt.Print`.
- Error wrapping with `fmt.Errorf("context: %w", err)` for debugging.
- Use the event publisher for cross-service communication.
- New API endpoints belong in `api/api.go` with corresponding HTTP routes in `http/http_service.go`.
### TypeScript / React
- **Avoid using useNavigate** — Use <Link/> component where possible to ensure good browser UX.
- **Use shadcn/ui components** for all UI — do not create custom components unless no shadcn equivalent exists.
- **Do not modify core shadcn/ui components** — customize behavior by composing or wrapping them, not by editing the source files directly.
- **Prefer Tailwind utility classes** over custom `px` definitions or inline styles. Use Tailwind's spacing, sizing, and layout utilities instead of hardcoded pixel values.
- **Never use `!important` Tailwind modifiers** (e.g., `!px-12`, `!text-sm`). If a component's default styles need overriding, use a proper variant, compose with a wrapper, or extend the component — don't force specificity with `!`.
- **Use the theme system** for colors, border-radius, shadows, and other design tokens. Reference CSS variables / Tailwind theme tokens (e.g., `bg-primary`, `rounded-lg`, `shadow-sm`) rather than hardcoding hex values or arbitrary values. See `frontend/src/index.css` for available theme variables.
- **Keep layouts flat** — avoid nesting cards inside cards or wrapping elements in unnecessary bordered containers. Prefer clear, flat visual hierarchy.
- **Match existing spacing patterns** — before adding new components, check sibling components for consistent padding, margins, and gaps. Ensure sibling elements have equal dimensions where appropriate.
- **Write copy from the user's perspective** — Alby Hub IS the wallet; don't explain what a lightning wallet is or tell the user to "connect to a wallet" when they're already inside one. Keep UI copy concise and use the product's own vocabulary (sats, connections, apps).
- **Use the `*Icon` suffix when importing lucide-react icons** (e.g., `ZapIcon`, `BitcoinIcon`, `ArrowDownIcon`) — both forms are valid lucide exports, but this codebase consistently uses the suffixed alias. Don't mix styles.
- Strict TypeScript — no `any` types.
- Functional components with hooks only.
- SWR for server state; Zustand for client state (stores in `frontend/src/state/`).
- HTTP requests use the typed `request()` helper in `frontend/src/utils/request.ts`.
- New screens added to `frontend/src/routes.tsx`.
- ESLint + Prettier enforced via pre-commit hooks (husky).
### Branches
Use a type prefix followed by a short, dash-separated summary: `feat/`, `chore/`, or `fix/`. For example:
```text
feat/add-cashu-backend
fix/payment-timeout-crash
chore/bump-go-1.25
```
### Commits
Follow **Conventional Commits** format (`feat:`, `fix:`, `chore:`, etc.) — enforced by commitlint.
## Critical Files
| File | Purpose |
|------|---------|
| `cmd/http/main.go` | HTTP server entry point |
| `main_wails.go` | Desktop entry point |
| `api/api.go` | Primary API endpoint handlers |
| `service/service.go` | Core service initialization |
| `service/start.go` | Service startup sequence |
| `lnclient/models.go` | LNClient interface definition |
| `nip47/event_handler.go` | NIP-47 request dispatch |
| `db/migrations/` | Database schema history |
| `frontend/src/types.ts` | Shared TypeScript types |
| `frontend/src/routes.tsx` | Frontend routing |
## Security Considerations
- **Seed phrases** are AES-encrypted at rest; decrypted in-memory only when the LN node is running.
- **NIP-47 messages** use NIP-04 or NIP-44 v2 encryption per app keypair (NIP-44 v2 preferred; NIP-04 is the fallback default).
- **API authentication** uses JWT (golang-jwt v5).
- Never log sensitive data (seeds, macaroons, tokens).
- Validate all user input at system boundaries; trust internal service calls.
## CI/CD
CI runs Go tests (including PostgreSQL), frontend lint/type checks, and binary builds for Linux and macOS. All checks must pass before merging to `master`.

1
CLAUDE.md Symbolic link
View file

@ -0,0 +1 @@
AGENTS.md

View file

@ -1,4 +1,4 @@
FROM node:20-alpine AS frontend
FROM node:22-alpine AS frontend
# Set the base path for the frontend build
# This can be overridden at build time with --build-arg BASE_PATH=<url> e.g. --build-arg BASE_PATH=/hub
@ -9,7 +9,7 @@ COPY frontend ./frontend
RUN echo "Building frontend with base path $BASE_PATH"
RUN cd frontend && yarn install --network-timeout 3000000 && yarn build:http
FROM golang:1.24 AS builder
FROM golang:1.26.2 AS builder
ARG TARGETPLATFORM
ARG BUILDPLATFORM

View file

@ -14,7 +14,7 @@ The application can run in two modes:
Ideally the app runs 24/7 (on a node, VPS or always-online desktop/laptop machine) so it can be connected to a lightning address and receive online payments.
## Run on Alby Cloud
## Learn more about Alby Hub
Visit [albyhub.com](https://albyhub.com) to learn more and get started and get Alby Hub running in minutes.
@ -29,6 +29,7 @@ By default Alby Hub uses the embedded LDK based lightning node. Optionally it ca
- LND
- Phoenixd
- Cashu
- CLN
- want more? please open an issue.
## Development
@ -59,7 +60,7 @@ By default Alby Hub uses the embedded LDK based lightning node. Optionally it ca
Go to `/frontend`
1. `yarn install`
2. `yarn dev`
2. `yarn dev:http`
### HTTP Production build
@ -157,7 +158,7 @@ For more information on the Go pprof library, see the [official documentation](h
The following configuration options can be set as environment variables or in a .env file
- `RELAY`: default: "wss://relay.getalby.com/v1" (can support multiple separated by commas)
- `RELAY`: default: "wss://relay.getalby.com,wss://relay2.getalby.com" (supports multiple separated by commas)
- `DATABASE_URI`: A sqlite filename or postgres URL. Default is SQLite DB `nwc.db` without a path, which will be put in the user home directory: $XDG_DATA_HOME/albyhub/nwc.db
- `PORT`: The port on which the app should listen on (default: 8080)
- `WORK_DIR`: Directory to store NWC data files. Default: $XDG_DATA_HOME/albyhub
@ -206,13 +207,33 @@ Migration of the database is currently experimental. Please make a backup before
go run cmd/db_migrate/main.go -from .data/nwc.db -to postgresql://myuser:mypass@localhost:5432/nwc
#### Migration from Postgres to Sqlite
No manual steps are needed: create a migration file from Settings -> Migrate Alby Hub. The contents of the Postgres database will automatically be copied into a Sqlite database which is included in the migration file. Alternatively, run the migration tool manually:
go run cmd/db_migrate/main.go -from postgresql://myuser:mypass@localhost:5432/nwc -to .data/nwc.db
## Node-specific backend parameters
- `ENABLE_ADVANCED_SETUP`: set to `false` to force a specific backend type (combined with backend parameters below)
### CLN Backend parameters
Can be configured via env or the UI
- `LN_BACKEND_TYPE`: CLN
- `CLN_ADDRESS`: the CLN grpc address (grpc-host and grpc-port), e.g. `127.0.0.1:9737`
- `CLN_LIGHTNING_DIR`: CLN's lightning directory containing the grpc certificates, usually `~/.lightning/<network>`
Optional for hold invoice methods support:
- `CLN_ADDRESS_HOLD`: the CLN hold plugin grpc address (grpc-host and grpc-port), e.g. `127.0.0.1:9738`
If you are copying the certificates to another machine make sure you get the `ca.pem`, `client.pem` and `client-key.pem` from the lightning directory and optionally from the `hold` directory inside the lightning directory and keep the sub-directory structure of the hold directory.
### LND Backend parameters
Currently only LND can be configured via env. Other node types must be configured via the UI.
LND can be configured via env. Other node types may need to be configured via the UI.
_To configure via env, the following parameters must be provided:_
@ -230,6 +251,8 @@ _To configure via env, the following parameters must be provided:_
- `LDK_MAX_CHANNEL_SATURATION`: Sets the maximum portion of a channel's total capacity that may be used for sending a payment, expressed as a power of 1/2. See `max_channel_saturation_power_of_half` in [LDK docs](https://docs.rs/lightning/latest/lightning/routing/router/struct.PaymentParameters.html#structfield.max_channel_saturation_power_of_half).
- `LDK_MAX_PATH_COUNT`: Maximum number of paths that may be used by MPP payments.
- `LDK_LOG_LEVEL`: Log level for the LDK node. Higher is more verbose. Default: 3. This is separate from the main application log level, allowing you to enable more verbose LDK logging (e.g., level 4, 5 or 6) without enabling verbose logging for the entire application.
- `LDK_CHANNEL_MONITOR_WARNING_SIZE_BYTES`: If a channel monitor is larger than this value, a performance warning will be shown on the node page.
- `LDK_LSPS2_ADDRESSES`: Override the LSPS2 just-in-time (JIT) LSP provider for receiving. When set, Alby Hub can receive payments even without inbound liquidity: the configured LSP opens a channel on the fly and the fee is deducted from the incoming payment. Expected format is a single `<pubkey>@<host>:<port>`. When set, the "Open Your First Channel" prompts are hidden since the first channel is created automatically on the first receive.
#### LDK Network Configuration
@ -244,6 +267,12 @@ _To configure via env, the following parameters must be provided:_
- `LDK_ELECTRUM_SERVER=electrum.mutinynet.com:50001`
##### Signet
- `MEMPOOL_API=https://mempool.space/signet/api`
- `LDK_NETWORK=signet`
- `LDK_ESPLORA_SERVER=https://mempool.space/signet/api`
##### Testnet (Not recommended - try Mutinynet)
- `MEMPOOL_API=https://mempool.space/testnet/api`
@ -262,6 +291,16 @@ _To configure via env, the following parameters must be provided:_
See [Phoenixd](scripts/linux-x86_64/phoenixd/README.md)
### Bark
Bark connects to an [Ark](https://second.tech/) server. It can be configured via env.
- `LN_BACKEND_TYPE`: BARK
- `BARK_SERVER`: the Ark server URL. For signet use `https://ark.signet.2nd.dev`
- `BARK_ESPLORA_SERVER`: the Esplora server URL used for chain data. For signet use `https://esplora.signet.2nd.dev`.
- `BARK_SERVER_ACCESS_TOKEN`: an optional access token, only required if using a private Ark server.
- `BARK_LOG_LEVEL`: Log level for Bark. Higher is more verbose. Default: 3. This is separate from the main application log level, allowing you to enable more verbose Bark logging (e.g., level 4 or 5) without enabling verbose logging for the entire application.
### Alby OAuth
Create an OAuth client at the [Alby Developer Portal](https://getalby.com/developer) and set your `ALBY_OAUTH_CLIENT_ID` and `ALBY_OAUTH_CLIENT_SECRET` in your .env. If not running locally, you'll also need to change your `BASE_URL`.
@ -363,6 +402,8 @@ Once the user has authorized the app connection a `nwc:success` message is sent
If you need help contact support@getalby.com or reach out on Nostr: npub1getal6ykt05fsz5nqu4uld09nfj3y3qxmv8crys4aeut53unfvlqr80nfm
You can also visit the chat of our Community on [Telegram](https://t.me/getalby).
For security vulnerabilities, please follow our [security policy](SECURITY.md).
## ⚡Donations
Want to support the work on Alby?

7
SECURITY.md Normal file
View file

@ -0,0 +1,7 @@
# Security Policy
## Reporting a Vulnerability
Please report suspected security vulnerabilities privately by emailing [security@getalby.com](mailto:security@getalby.com). Do not open a public issue or disclose the vulnerability publicly until we have coordinated a fix.
Please include the affected version or component, the potential impact, and clear steps to reproduce the issue. We will acknowledge your report and keep you informed as we investigate and address it.

View file

@ -61,7 +61,7 @@ func NewAlbyOAuthService(db *gorm.DB, cfg config.Config, keys keys.Keys, eventPu
conf := &oauth2.Config{
ClientID: cfg.GetEnv().AlbyClientId,
ClientSecret: cfg.GetEnv().AlbyClientSecret,
Scopes: []string{"account:read", "balance:read", "payments:send"},
Scopes: []string{"account:read"},
Endpoint: oauth2.Endpoint{
TokenURL: albyOAuthAPIURL + "/oauth/token",
AuthURL: albyOAuthAuthUrl,
@ -93,7 +93,7 @@ func (svc *albyOAuthService) RemoveOAuthAccessToken() error {
return err
}
func (svc *albyOAuthService) CallbackHandler(ctx context.Context, code string, lnClient lnclient.LNClient) error {
func (svc *albyOAuthService) CallbackHandler(ctx context.Context, code string) error {
token, err := svc.oauthConf.Exchange(ctx, code)
if err != nil {
logger.Logger.WithError(err).Error("Failed to exchange token")
@ -408,7 +408,7 @@ func (svc *albyOAuthService) DeleteLightningAddress(ctx context.Context, address
res, err := client.Do(req)
if err != nil {
logger.Logger.WithError(err).Error("Failed to delete lightning address endpoint")
logger.Logger.WithError(err).Error("Failed to request delete lightning address endpoint")
return err
}
@ -418,6 +418,13 @@ func (svc *albyOAuthService) DeleteLightningAddress(ctx context.Context, address
return errors.New("failed to read response body")
}
if res.StatusCode == http.StatusNotFound {
// The lightning address was already deleted on the Alby account
// (e.g. removed previously). Treat as success so deletion is idempotent.
logger.Logger.WithField("address", address).Info("Lightning address already deleted on Alby account, ignoring 404")
return nil
}
if res.StatusCode >= 300 {
return fmt.Errorf("DELETE request to /internal/lightning_addresses/%s returned non-success status: %d %s", address, res.StatusCode, string(responseBody))
}
@ -525,7 +532,11 @@ func (svc *albyOAuthService) UnlinkAccount(ctx context.Context) error {
return nil
}
func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient.LNClient, budget uint64, renewal string) error {
func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient.LNClient, budgetSat uint64, renewal string) error {
if lnClient == nil {
return errors.New("LNClient not available")
}
svc.deleteAlbyAccountApps()
connectionPubkey, err := svc.createAlbyAccountNWCNode(ctx)
@ -547,7 +558,7 @@ func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient.
app, _, err := apps.NewAppsService(svc.db, svc.eventPublisher, svc.keys, svc.cfg).CreateApp(
ALBY_ACCOUNT_APP_NAME,
connectionPubkey,
budget,
budgetSat,
renewal,
nil,
scopes,
@ -1183,6 +1194,10 @@ func (svc *albyOAuthService) CreateLSPOrder(ctx context.Context, lsp, network st
}
func (svc *albyOAuthService) RequestAutoChannel(ctx context.Context, lnClient lnclient.LNClient, isPublic bool) (*AutoChannelResponse, error) {
if lnClient == nil {
return nil, errors.New("LNClient not available")
}
nodeInfo, err := lnClient.GetInfo(ctx)
if err != nil {
logger.Logger.WithError(err).Error("Failed to request own node info", err)
@ -1317,11 +1332,11 @@ func (svc *albyOAuthService) requestAutoChannel(ctx context.Context, url string,
}
var invoice string
var fee uint64
var feeSat uint64
if newAutoChannelResponse.Payment != nil {
invoice = newAutoChannelResponse.Payment.Bolt11.Invoice
fee, err = strconv.ParseUint(newAutoChannelResponse.Payment.Bolt11.FeeTotalSat, 10, 64)
feeSat, err = strconv.ParseUint(newAutoChannelResponse.Payment.Bolt11.FeeTotalSat, 10, 64)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": url,
@ -1335,16 +1350,16 @@ func (svc *albyOAuthService) requestAutoChannel(ctx context.Context, url string,
return nil, err
}
if fee != uint64(paymentRequest.MSatoshi/1000) {
if feeSat != uint64(paymentRequest.MSatoshi/1000) {
logger.Logger.WithFields(logrus.Fields{
"invoice_amount": paymentRequest.MSatoshi / 1000,
"fee": fee,
"fee": feeSat,
}).WithError(err).Error("Invoice amount does not match LSP fee")
return nil, errors.New("invoice amount does not match LSP fee")
}
}
channelSize, err := strconv.ParseUint(newAutoChannelResponse.LspBalanceSat, 10, 64)
channelSizeSat, err := strconv.ParseUint(newAutoChannelResponse.LspBalanceSat, 10, 64)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": url,
@ -1353,12 +1368,60 @@ func (svc *albyOAuthService) requestAutoChannel(ctx context.Context, url string,
}
return &AutoChannelResponse{
Invoice: invoice,
Fee: fee,
ChannelSize: channelSize,
Invoice: invoice,
Fee: feeSat,
FeeSat: feeSat,
ChannelSize: channelSizeSat,
ChannelSizeSat: channelSizeSat,
}, nil
}
func (svc *albyOAuthService) GetStories(ctx context.Context) ([]Story, error) {
client := &http.Client{Timeout: 10 * time.Second}
url := fmt.Sprintf("%s/stories", albyInternalAPIURL)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
logger.Logger.WithError(err).Error("Error creating request to stories endpoint")
return nil, fmt.Errorf("create stories request: %w", err)
}
setDefaultRequestHeaders(req)
res, err := client.Do(req)
if err != nil {
logger.Logger.WithError(err).Error("Failed to fetch stories from API")
return nil, fmt.Errorf("fetch stories: %w", err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": url,
}).Error("Failed to read response body")
return nil, fmt.Errorf("read stories response body: %w", err)
}
if res.StatusCode >= 300 {
logger.Logger.WithFields(logrus.Fields{
"body": string(body),
"status_code": res.StatusCode,
}).Error("stories endpoint returned non-success code")
return nil, fmt.Errorf("stories endpoint returned %d: %s", res.StatusCode, string(body))
}
var stories []Story
if err := json.Unmarshal(body, &stories); err != nil {
logger.Logger.WithFields(logrus.Fields{
"body": string(body),
"error": err,
}).Error("Failed to decode stories API response")
return nil, fmt.Errorf("decode stories response: %w", err)
}
return stories, nil
}
func setDefaultRequestHeaders(req *http.Request) {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "AlbyHub/"+version.Tag)
@ -1403,5 +1466,7 @@ func getEventWhitelist() []string {
// client-side events
"payment_failed_details",
"debit_card_url_clicked",
"debit_card_connect",
}
}

View file

@ -9,7 +9,6 @@ import (
"net/http"
"time"
"github.com/getAlby/hub/config"
"github.com/getAlby/hub/logger"
"github.com/sirupsen/logrus"
)
@ -17,19 +16,67 @@ import (
const albyInternalAPIURL = "https://getalby.com/api"
type albyService struct {
cfg config.Config
}
func NewAlbyService(cfg config.Config) *albyService {
albySvc := &albyService{
cfg: cfg,
}
return albySvc
func NewAlbyService() *albyService {
return &albyService{}
}
func (svc *albyService) GetBitcoinRate(ctx context.Context) (*BitcoinRate, error) {
func (svc *albyService) GetCurrencies(ctx context.Context) ([]Currency, error) {
client := &http.Client{Timeout: 10 * time.Second}
url := fmt.Sprintf("%s/rates", albyInternalAPIURL)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
logger.Logger.WithError(err).Error("Error creating request to currencies endpoint")
return nil, err
}
setDefaultRequestHeaders(req)
res, err := client.Do(req)
if err != nil {
logger.Logger.WithError(err).Error("Failed to fetch currencies from API")
return nil, err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": url,
}).Error("Failed to read response body")
return nil, errors.New("failed to read response body")
}
if res.StatusCode >= 300 {
logger.Logger.WithFields(logrus.Fields{
"body": string(body),
"status_code": res.StatusCode,
}).Error("Currencies endpoint returned non-success code")
return nil, fmt.Errorf("currencies endpoint returned non-success code: %s", string(body))
}
rawCurrencies := map[string]Currency{}
err = json.Unmarshal(body, &rawCurrencies)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"body": string(body),
"error": err,
}).Error("Failed to decode currencies API response")
return nil, err
}
currencies := []Currency{}
for _, currency := range rawCurrencies {
currencies = append(currencies, currency)
}
return currencies, nil
}
func (svc *albyService) GetBitcoinRate(ctx context.Context, currency string) (*BitcoinRate, error) {
client := &http.Client{Timeout: 10 * time.Second}
currency := svc.cfg.GetCurrency()
url := fmt.Sprintf("%s/rates/%s", albyInternalAPIURL, currency)
@ -123,6 +170,11 @@ func (svc *albyService) GetChannelPeerSuggestions(ctx context.Context) ([]Channe
return nil, err
}
for i := range suggestions {
suggestions[i].MinimumChannelSizeSat = suggestions[i].MinimumChannelSize
suggestions[i].MaximumChannelSizeSat = suggestions[i].MaximumChannelSize
}
logger.Logger.WithFields(logrus.Fields{"channel_suggestions": suggestions}).Debug("Alby channel peer suggestions response")
return suggestions, nil
}

View file

@ -9,7 +9,8 @@ import (
type AlbyService interface {
GetInfo(ctx context.Context) (*AlbyInfo, error)
GetBitcoinRate(ctx context.Context) (*BitcoinRate, error)
GetBitcoinRate(ctx context.Context, currency string) (*BitcoinRate, error)
GetCurrencies(ctx context.Context) ([]Currency, error)
GetChannelPeerSuggestions(ctx context.Context) ([]ChannelPeerSuggestion, error)
}
@ -22,8 +23,8 @@ type AlbyOAuthService interface {
GetUserIdentifier() (string, error)
GetLightningAddress() (string, error)
IsConnected(ctx context.Context) bool
LinkAccount(ctx context.Context, lnClient lnclient.LNClient, budget uint64, renewal string) error
CallbackHandler(ctx context.Context, code string, lnClient lnclient.LNClient) error
LinkAccount(ctx context.Context, lnClient lnclient.LNClient, budgetSat uint64, renewal string) error
CallbackHandler(ctx context.Context, code string) error
GetMe(ctx context.Context) (*AlbyMe, error)
UnlinkAccount(ctx context.Context) error
RequestAutoChannel(ctx context.Context, lnClient lnclient.LNClient, isPublic bool) (*AutoChannelResponse, error)
@ -31,6 +32,7 @@ type AlbyOAuthService interface {
RemoveOAuthAccessToken() error
CreateLightningAddress(ctx context.Context, address string, appId uint) (*CreateLightningAddressResponse, error)
DeleteLightningAddress(ctx context.Context, address string) error
GetStories(ctx context.Context) ([]Story, error)
}
type CreateLightningAddressResponse struct {
@ -48,9 +50,11 @@ type AutoChannelRequest struct {
}
type AutoChannelResponse struct {
Invoice string `json:"invoice"`
ChannelSize uint64 `json:"channelSize"`
Fee uint64 `json:"fee"`
Invoice string `json:"invoice"`
ChannelSize uint64 `json:"channelSize"` // deprecated
ChannelSizeSat uint64 `json:"channelSizeSat"`
Fee uint64 `json:"fee"` // deprecated
FeeSat uint64 `json:"feeSat"`
}
type AlbyInfoHub struct {
@ -101,8 +105,10 @@ type ChannelPeerSuggestion struct {
PaymentMethod string `json:"paymentMethod"`
Pubkey string `json:"pubkey"`
Host string `json:"host"`
MinimumChannelSize uint64 `json:"minimumChannelSize"`
MaximumChannelSize uint64 `json:"maximumChannelSize"`
MinimumChannelSize uint64 `json:"minimumChannelSize"` // deprecated
MinimumChannelSizeSat uint64 `json:"minimumChannelSizeSat"`
MaximumChannelSize uint64 `json:"maximumChannelSize"` // deprecated
MaximumChannelSizeSat uint64 `json:"maximumChannelSizeSat"`
MaximumChannelExpiryBlocks *uint32 `json:"maximumChannelExpiryBlocks"`
Name string `json:"name"`
Image string `json:"image"`
@ -113,6 +119,7 @@ type ChannelPeerSuggestion struct {
Description string `json:"description"`
Note string `json:"note"`
PublicChannelsAllowed bool `json:"publicChannelsAllowed"`
NodeAddress string `json:"nodeAddress"`
FeeTotalSat1m *uint32 `json:"feeTotalSat1m"`
FeeTotalSat2m *uint32 `json:"feeTotalSat2m"`
FeeTotalSat3m *uint32 `json:"feeTotalSat3m"`
@ -129,6 +136,13 @@ type LSPChannelOffer struct {
LspDescription string `json:"lspDescription"`
}
type Currency struct {
IsoCode string `json:"iso_code"`
Symbol string `json:"symbol"`
Name string `json:"name"`
Priority int `json:"priority"`
}
type BitcoinRate struct {
Code string `json:"code"`
Symbol string `json:"symbol"`
@ -141,6 +155,20 @@ type ErrorResponse struct {
Message string `json:"message"`
}
type StoryCta struct {
Label string `json:"label"`
URL string `json:"url"`
OpenInNewTab bool `json:"openInNewTab"`
}
type Story struct {
ID int `json:"id"`
Title string `json:"title"`
Avatar string `json:"avatar"`
VideoID string `json:"videoId,omitempty"`
Cta *StoryCta `json:"cta,omitempty"`
}
type LSPChannelPaymentBolt11 struct {
Invoice string `json:"invoice"`
FeeTotalSat string `json:"fee_total_sat"`

1038
api/api.go

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,32 @@ import (
"github.com/stretchr/testify/require"
)
func TestBuildReturnToUrl(t *testing.T) {
relayUrls := []string{"wss://relay.getalby.com/v1"}
walletPubkey := "6f8bf1b7d58ac41b2c793837ba528c1d0a4a1cd2e3f5b7c9d0e1f2a3b4c5d6e7"
assert.Equal(t,
"https://example.com?pubkey=6f8bf1b7d58ac41b2c793837ba528c1d0a4a1cd2e3f5b7c9d0e1f2a3b4c5d6e7&relay=wss%3A%2F%2Frelay.getalby.com%2Fv1",
buildReturnToUrl("https://example.com", relayUrls, walletPubkey, "", false))
// existing query parameters are preserved and lud16 is added
assert.Equal(t,
"https://example.com/path?foo=bar&lud16=user%40getalby.com&pubkey=6f8bf1b7d58ac41b2c793837ba528c1d0a4a1cd2e3f5b7c9d0e1f2a3b4c5d6e7&relay=wss%3A%2F%2Frelay.getalby.com%2Fv1",
buildReturnToUrl("https://example.com/path?foo=bar", relayUrls, walletPubkey, "user@getalby.com", false))
// isolated apps do not receive a lightning address
assert.Equal(t,
"http://example.com?pubkey=6f8bf1b7d58ac41b2c793837ba528c1d0a4a1cd2e3f5b7c9d0e1f2a3b4c5d6e7&relay=wss%3A%2F%2Frelay.getalby.com%2Fv1",
buildReturnToUrl("http://example.com", relayUrls, walletPubkey, "user@getalby.com", true))
// only http and https URLs are accepted
assert.Equal(t, "", buildReturnToUrl("", relayUrls, walletPubkey, "", false))
assert.Equal(t, "", buildReturnToUrl("example.com/path", relayUrls, walletPubkey, "", false))
assert.Equal(t, "", buildReturnToUrl("example://app", relayUrls, walletPubkey, "", false))
assert.Equal(t, "", buildReturnToUrl("javascript:void(0)", relayUrls, walletPubkey, "", false))
assert.Equal(t, "", buildReturnToUrl("::invalid::", relayUrls, walletPubkey, "", false))
}
func TestCreateApp_SuperuserScopeIncorrectPassword(t *testing.T) {
cfg := mocks.NewMockConfig(t)
cfg.On("CheckUnlockPassword", "").Return(false)

View file

@ -1,9 +1,11 @@
package api
import (
"bytes"
"errors"
"fmt"
"io"
"math"
"strings"
"time"
@ -16,12 +18,48 @@ import (
"crypto/rand"
"crypto/sha256"
"github.com/getAlby/hub/config"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/utils"
"golang.org/x/crypto/pbkdf2"
)
// zipMagic is the ZIP local file header signature "PK\x03\x04" — the first
// four bytes of every ZIP file, and therefore of every archive produced by
// CreateBackup. decryptingReader uses it to detect which cipher scheme the
// backup file was created with.
var zipMagic = []byte{'P', 'K', 0x03, 0x04}
// backupCipher describes one of the cipher schemes used for backup files,
// which are laid out as salt || iv || encrypted zip archive.
type backupCipher struct {
saltSize int
deriveKey func(password string, salt []byte) ([]byte, error)
newStream func(block cipher.Block, iv []byte) cipher.Stream
}
var backupCiphers = []backupCipher{
// current scheme, used for all new backup files
{
saltSize: 32,
deriveKey: func(password string, salt []byte) ([]byte, error) {
key, _, err := config.DeriveKey(password, salt)
return key, err
},
newStream: cipher.NewCTR,
},
// legacy scheme, kept to restore backup files created by older versions
{
saltSize: 8,
deriveKey: func(password string, salt []byte) ([]byte, error) {
return pbkdf2.Key([]byte(password), salt, 4096, 32, sha256.New), nil
},
//nolint:staticcheck // OFB is required to read files created by older versions
newStream: cipher.NewOFB,
},
}
func (api *api) CreateBackup(unlockPassword string, w io.Writer) error {
logger.Logger.Info("Creating backup to migrate Alby Hub to another device")
var err error
@ -38,8 +76,9 @@ func (api *api) CreateBackup(unlockPassword string, w io.Writer) error {
return errors.New("Please disable auto-unlock before using this feature")
}
if api.db.Dialector.Name() != "sqlite" {
return errors.New("Migration with non-sqlite backend is currently not supported")
dbBackend := api.db.Dialector.Name()
if dbBackend != "sqlite" && dbBackend != "postgres" {
return fmt.Errorf("migration with %s backend is currently not supported", dbBackend)
}
workDir, err := filepath.Abs(api.cfg.GetEnv().Workdir)
@ -49,17 +88,18 @@ func (api *api) CreateBackup(unlockPassword string, w io.Writer) error {
lnStorageDir := ""
if api.svc.GetLNClient() == nil {
lnClient := api.svc.GetLNClient()
if lnClient == nil {
return fmt.Errorf("node not running")
}
lnStorageDir, err = api.svc.GetLNClient().GetStorageDir()
lnStorageDir, err = lnClient.GetStorageDir()
if err != nil {
return fmt.Errorf("failed to get storage dir: %w", err)
}
logger.Logger.WithField("path", lnStorageDir).Info("Found node storage dir")
// Reset the routing data to decrease the LDK DB size
err = api.svc.GetLNClient().ResetRouter("ALL")
err = lnClient.ResetRouter("ALL")
if err != nil {
logger.Logger.WithError(err).Error("Failed to reset router")
return fmt.Errorf("failed to reset router: %w", err)
@ -75,6 +115,50 @@ func (api *api) CreateBackup(unlockPassword string, w io.Writer) error {
return errors.New("failed to remove oauth access token")
}
// Locate the main database file.
dbFilePath := api.cfg.GetEnv().DatabaseUri
if dbBackend == "postgres" {
// The migration file must contain a sqlite database, so copy the
// contents of the postgres database into a temporary sqlite database
// and add that to the archive instead.
dbFilePath = filepath.Join(workDir, "migration.db")
removeConvertedDb := func() {
for _, path := range []string{dbFilePath, dbFilePath + "-wal", dbFilePath + "-shm"} {
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
logger.Logger.WithError(err).WithField("path", path).Error("Failed to remove converted database file")
}
}
}
// Remove stale files from a previously failed migration attempt.
removeConvertedDb()
defer removeConvertedDb()
logger.Logger.WithField("path", dbFilePath).Info("Copying postgres database to sqlite")
sqliteDb, err := db.NewDB(dbFilePath, api.cfg.GetEnv().LogDBQueries)
if err != nil {
logger.Logger.WithError(err).Error("Failed to create sqlite database for migration")
return fmt.Errorf("failed to create sqlite database for migration: %w", err)
}
err = db.MigrateDB(api.db, sqliteDb)
if err != nil {
logger.Logger.WithError(err).Error("Failed to copy database contents to sqlite")
if stopErr := db.Stop(sqliteDb); stopErr != nil {
logger.Logger.WithError(stopErr).Error("Failed to stop sqlite database")
}
return fmt.Errorf("failed to copy database contents to sqlite: %w", err)
}
// Close the sqlite database to checkpoint the WAL before archiving it.
err = db.Stop(sqliteDb)
if err != nil {
logger.Logger.WithError(err).Error("Failed to stop sqlite database")
return fmt.Errorf("failed to close sqlite database: %w", err)
}
}
// Closing the database leaves the service in an inconsistent state,
// but that should not be a problem since the app is not expected
// to be used after its data is exported.
@ -125,8 +209,6 @@ func (api *api) CreateBackup(unlockPassword string, w io.Writer) error {
return err
}
// Locate the main database file.
dbFilePath := api.cfg.GetEnv().DatabaseUri
// Add the database file to the archive.
logger.Logger.WithField("nwc.db", dbFilePath).Info("adding nwc db to zip")
err = addFileToZip(dbFilePath, "nwc.db")
@ -151,8 +233,18 @@ func (api *api) CreateBackup(unlockPassword string, w io.Writer) error {
}
}
// Finalize the archive before reporting success; the deferred close
// only covers early returns.
err = zw.Close()
if err != nil {
logger.Logger.WithError(err).Error("Failed to finalize migration archive")
return fmt.Errorf("failed to finalize migration archive: %w", err)
}
logger.Logger.Info("Successfully created backup to migrate Alby Hub to another device")
api.nodeMigrationFileCreated.Store(true)
return nil
}
@ -203,8 +295,38 @@ func (api *api) RestoreBackup(unlockPassword string, r io.Reader) error {
return fmt.Errorf("failed to create zip reader: %w", err)
}
if len(zr.File) == 0 {
return errors.New("backup file contains no files")
}
restoreDir := filepath.Join(workDir, "restore")
// Extract into a staging directory and only move it to the restore
// directory once every entry has been extracted, so that a failed
// extraction cannot leave a partial restore directory behind, which
// would be applied on the next startup.
stagingDir, err := os.MkdirTemp(workDir, "albyhub-restore-")
if err != nil {
return fmt.Errorf("failed to create staging directory: %w", err)
}
defer os.RemoveAll(stagingDir)
extractZipEntry := func(zipFile *zip.File) error {
fsFilePath := filepath.Join(workDir, "restore", filepath.FromSlash(zipFile.Name))
// Entry names come from the archive and must not be trusted. Reject any
// name that is absolute or points outside the restore directory via
// ".." segments before joining it to a path.
entryName := filepath.FromSlash(zipFile.Name)
if !filepath.IsLocal(entryName) {
return fmt.Errorf("refusing to extract zip entry outside restore directory: %q", zipFile.Name)
}
fsFilePath := filepath.Join(stagingDir, entryName)
// Confirm the cleaned path is still contained within the staging
// directory.
if fsFilePath != stagingDir && !strings.HasPrefix(fsFilePath, stagingDir+string(os.PathSeparator)) {
return fmt.Errorf("refusing to extract zip entry outside restore directory: %q", zipFile.Name)
}
if err = os.MkdirAll(filepath.Dir(fsFilePath), 0700); err != nil {
return fmt.Errorf("failed to create directory for zip entry: %w", err)
@ -238,13 +360,20 @@ func (api *api) RestoreBackup(unlockPassword string, r io.Reader) error {
}
logger.Logger.WithField("count", len(zr.File)).Info("Extracted files")
if err = os.RemoveAll(restoreDir); err != nil {
return fmt.Errorf("failed to remove existing restore directory: %w", err)
}
if err = os.Rename(stagingDir, restoreDir); err != nil {
return fmt.Errorf("failed to move extracted files to restore directory: %w", err)
}
go func() {
logger.Logger.Info("Backup restored. Shutting down Alby Hub...")
api.svc.Shutdown()
// ensure no -shm or -wal files exist as they will stop the restore
for _, filename := range []string{"nwc.db", "nwc.db-shm", "nwc.db-wal"} {
err = os.Remove(filepath.Join(workDir, filename))
if err != nil {
if err != nil && !errors.Is(err, os.ErrNotExist) {
logger.Logger.WithError(err).WithField("filename", filename).Error("failed to remove old nwc db file before restore")
}
}
@ -258,12 +387,17 @@ func (api *api) RestoreBackup(unlockPassword string, r io.Reader) error {
}
func encryptingWriter(w io.Writer, password string) (io.Writer, error) {
salt := make([]byte, 8)
scheme := backupCiphers[0]
salt := make([]byte, scheme.saltSize)
if _, err := rand.Read(salt); err != nil {
return nil, fmt.Errorf("failed to generate salt: %w", err)
}
encKey := pbkdf2.Key([]byte(password), salt, 4096, 32, sha256.New)
encKey, err := scheme.deriveKey(password, salt)
if err != nil {
return nil, fmt.Errorf("failed to derive encryption key: %w", err)
}
block, err := aes.NewCipher(encKey)
if err != nil {
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
@ -284,9 +418,8 @@ func encryptingWriter(w io.Writer, password string) (io.Writer, error) {
return nil, fmt.Errorf("failed to write IV: %w", err)
}
stream := cipher.NewOFB(block, iv)
cw := &cipher.StreamWriter{
S: stream,
S: scheme.newStream(block, iv),
W: w,
}
@ -294,27 +427,61 @@ func encryptingWriter(w io.Writer, password string) (io.Writer, error) {
}
func decryptingReader(r io.Reader, password string) (io.Reader, error) {
salt := make([]byte, 8)
if _, err := io.ReadFull(r, salt); err != nil {
return nil, fmt.Errorf("failed to read salt: %w", err)
// Read the largest possible header (salt, IV and the first bytes of the
// archive) upfront, then trial-decrypt with each supported cipher scheme
// and pick the one that produces the ZIP signature.
maxHeaderSize := 0
minHeaderSize := math.MaxInt
for _, scheme := range backupCiphers {
headerSize := scheme.saltSize + aes.BlockSize + len(zipMagic)
maxHeaderSize = max(maxHeaderSize, headerSize)
minHeaderSize = min(minHeaderSize, headerSize)
}
iv := make([]byte, aes.BlockSize)
if _, err := io.ReadFull(r, iv); err != nil {
return nil, fmt.Errorf("failed to read IV: %w", err)
// Read the full header with io.ReadFull rather than io.ReadAtLeast: the
// reader may deliver short reads (e.g. a network request body), and
// stopping early could truncate the header of a scheme with a larger
// salt. A short file is only acceptable if it still covers the smallest
// scheme header.
header := make([]byte, maxHeaderSize)
n, err := io.ReadFull(r, header)
if err != nil && !(errors.Is(err, io.ErrUnexpectedEOF) && n >= minHeaderSize) {
return nil, fmt.Errorf("failed to read backup header: %w", err)
}
header = header[:n]
for _, scheme := range backupCiphers {
if len(header) < scheme.saltSize+aes.BlockSize+len(zipMagic) {
continue
}
salt := header[:scheme.saltSize]
iv := header[scheme.saltSize : scheme.saltSize+aes.BlockSize]
encrypted := header[scheme.saltSize+aes.BlockSize:]
encKey, err := scheme.deriveKey(password, salt)
if err != nil {
return nil, fmt.Errorf("failed to derive encryption key: %w", err)
}
block, err := aes.NewCipher(encKey)
if err != nil {
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
}
stream := scheme.newStream(block, iv)
decrypted := make([]byte, len(encrypted))
stream.XORKeyStream(decrypted, encrypted)
if !bytes.Equal(decrypted[:len(zipMagic)], zipMagic) {
continue
}
cr := &cipher.StreamReader{
S: stream,
R: r,
}
return io.MultiReader(bytes.NewReader(decrypted), cr), nil
}
encKey := pbkdf2.Key([]byte(password), salt, 4096, 32, sha256.New)
block, err := aes.NewCipher(encKey)
if err != nil {
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
}
stream := cipher.NewOFB(block, iv)
cr := &cipher.StreamReader{
S: stream,
R: r,
}
return cr, nil
return nil, errors.New("invalid unlock password or backup file")
}

261
api/backup_test.go Normal file
View file

@ -0,0 +1,261 @@
package api
import (
"archive/zip"
"bytes"
"encoding/hex"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"testing/iotest"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
"gorm.io/datatypes"
"github.com/getAlby/hub/config"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/logger"
test_db "github.com/getAlby/hub/tests/db"
"github.com/getAlby/hub/tests/mocks"
)
// TestCreateBackup creates a backup from the test database (sqlite by
// default, postgres when TEST_DATABASE_URI is set) and verifies that the
// archive contains a valid sqlite database with the expected data.
func TestCreateBackup(t *testing.T) {
logger.Init(strconv.Itoa(int(logrus.DebugLevel)))
workDir := t.TempDir()
gormDB, err := test_db.NewDB(t)
require.NoError(t, err)
defer test_db.CloseDB(gormDB)
appConfig := &config.AppConfig{
Workdir: workDir,
DatabaseUri: test_db.GetTestDatabaseURI(),
}
cfg, err := config.NewConfig(appConfig, gormDB)
require.NoError(t, err)
unlockPassword := ""
// Represent a fully set-up hub: the unlock-password canary is written during
// setup and is required for the password check to pass.
require.NoError(t, cfg.SaveUnlockPasswordCheck(unlockPassword))
app := &db.App{
Name: "test",
AppPubkey: "2b7dea2866958f17c568cf024e113db7a3baa9c253a9016889196b8d0b11c7ae",
Metadata: datatypes.JSON("{}"),
}
require.NoError(t, gormDB.Create(app).Error)
lnClient := mocks.NewMockLNClient(t)
lnClient.On("GetStorageDir").Return("", nil)
lnClient.On("ResetRouter", "ALL").Return(nil)
svc := mocks.NewMockService(t)
svc.On("GetLNClient").Return(lnClient)
svc.On("StopApp").Return()
albyOAuthSvc := mocks.NewMockAlbyOAuthService(t)
albyOAuthSvc.On("RemoveOAuthAccessToken").Return(nil)
theAPI := &api{
db: gormDB,
cfg: cfg,
svc: svc,
albyOAuthSvc: albyOAuthSvc,
}
var buf bytes.Buffer
err = theAPI.CreateBackup(unlockPassword, &buf)
require.NoError(t, err)
// The temporary database created when converting from postgres must
// not be left behind in the working directory.
entries, err := os.ReadDir(workDir)
require.NoError(t, err)
require.Empty(t, entries)
cr, err := decryptingReader(&buf, unlockPassword)
require.NoError(t, err)
decrypted, err := io.ReadAll(cr)
require.NoError(t, err)
zr, err := zip.NewReader(bytes.NewReader(decrypted), int64(len(decrypted)))
require.NoError(t, err)
dbFile, err := zr.Open("nwc.db")
require.NoError(t, err)
dbContents, err := io.ReadAll(dbFile)
require.NoError(t, err)
require.NoError(t, dbFile.Close())
restoredPath := filepath.Join(workDir, "restored.db")
require.NoError(t, os.WriteFile(restoredPath, dbContents, 0600))
restoredDB, err := db.NewDB(restoredPath, false)
require.NoError(t, err)
defer func() {
require.NoError(t, db.Stop(restoredDB))
}()
var restoredApp db.App
require.NoError(t, restoredDB.First(&restoredApp).Error)
require.Equal(t, app.Name, restoredApp.Name)
require.Equal(t, app.AppPubkey, restoredApp.AppPubkey)
}
// TestRestoreBackupRejectsPathTraversal verifies that a backup archive
// containing an entry whose name points outside the restore directory is
// rejected and that no file is written outside it.
func TestRestoreBackupRejectsPathTraversal(t *testing.T) {
logger.Init(strconv.Itoa(int(logrus.DebugLevel)))
gormDB, err := test_db.NewDB(t)
require.NoError(t, err)
defer test_db.CloseDB(gormDB)
if gormDB.Dialector.Name() != "sqlite" {
t.Skip("restore is only supported on sqlite")
}
workDir := t.TempDir()
appConfig := &config.AppConfig{
Workdir: workDir,
DatabaseUri: test_db.GetTestDatabaseURI(),
}
cfg, err := config.NewConfig(appConfig, gormDB)
require.NoError(t, err)
theAPI := &api{
db: gormDB,
cfg: cfg,
}
unlockPassword := ""
// The restore directory is <workDir>/restore, so a "../" entry targets a
// file directly in the working directory, one level above it.
const escapeEntryName = "../pwned.txt"
escapeTarget := filepath.Join(workDir, "pwned.txt")
var buf bytes.Buffer
cw, err := encryptingWriter(&buf, unlockPassword)
require.NoError(t, err)
zw := zip.NewWriter(cw)
// A valid entry before the malicious one, to verify that a partially
// extracted archive is not left behind when a later entry fails.
entryWriter, err := zw.Create("nwc.db")
require.NoError(t, err)
_, err = entryWriter.Write([]byte("backup contents"))
require.NoError(t, err)
entryWriter, err = zw.Create(escapeEntryName)
require.NoError(t, err)
_, err = entryWriter.Write([]byte("pwned"))
require.NoError(t, err)
require.NoError(t, zw.Close())
err = theAPI.RestoreBackup(unlockPassword, &buf)
require.ErrorContains(t, err, "refusing to extract zip entry outside restore directory")
_, statErr := os.Stat(escapeTarget)
require.True(t, os.IsNotExist(statErr), "traversal entry must not be written outside the restore directory")
// The failed restore must not leave a restore directory (which would be
// applied on the next startup) or any staging leftovers.
_, statErr = os.Stat(filepath.Join(workDir, "restore"))
require.True(t, os.IsNotExist(statErr), "failed restore must not leave a restore directory")
entries, err := os.ReadDir(workDir)
require.NoError(t, err)
for _, entry := range entries {
require.False(t, strings.HasPrefix(entry.Name(), "albyhub-restore-"), "failed restore must not leave a staging directory")
}
}
// legacyBackupFixture is a backup file created with the encryption scheme
// used by older versions (PBKDF2 key derivation), encrypted with the
// password "test-unlock-password". Its archive contains a single "nwc.db"
// entry with the contents "legacy backup contents".
const legacyBackupFixture = "0102030405060708101112131415161718191a1b1c1d1e1f8eca79631915f679a00cdd95d3f20d8d169eb9aa5d52642ca13b93886c3c7d7ba4b759462bc9dd8deccf638edcc9b5b9fda3d23dcd904cf6e99bc57ac59c4df6be5aa676542b7cbc9998029420c0ae5a6986c735150ababde5b382560acaebd5894aa4420924f1ced63fde570adc60c43b32e9e14a0ef60c379da5cac1be0000845992ea072ead036e336c7b859e8d018c4ef61667e3f520fe01"
// TestDecryptingReaderLegacyBackup verifies that backup files created by
// older versions can still be decrypted.
func TestDecryptingReaderLegacyBackup(t *testing.T) {
encrypted, err := hex.DecodeString(legacyBackupFixture)
require.NoError(t, err)
cr, err := decryptingReader(bytes.NewReader(encrypted), "test-unlock-password")
require.NoError(t, err)
decrypted, err := io.ReadAll(cr)
require.NoError(t, err)
zr, err := zip.NewReader(bytes.NewReader(decrypted), int64(len(decrypted)))
require.NoError(t, err)
dbFile, err := zr.Open("nwc.db")
require.NoError(t, err)
dbContents, err := io.ReadAll(dbFile)
require.NoError(t, err)
require.NoError(t, dbFile.Close())
require.Equal(t, "legacy backup contents", string(dbContents))
}
// TestDecryptingReaderFragmentedReader verifies that a backup file is
// decrypted correctly even when the reader delivers one byte at a time,
// which would truncate the header if it were not read in full.
func TestDecryptingReaderFragmentedReader(t *testing.T) {
var buf bytes.Buffer
cw, err := encryptingWriter(&buf, "test-unlock-password")
require.NoError(t, err)
zw := zip.NewWriter(cw)
entryWriter, err := zw.Create("nwc.db")
require.NoError(t, err)
_, err = entryWriter.Write([]byte("backup contents"))
require.NoError(t, err)
require.NoError(t, zw.Close())
cr, err := decryptingReader(iotest.OneByteReader(bytes.NewReader(buf.Bytes())), "test-unlock-password")
require.NoError(t, err)
decrypted, err := io.ReadAll(cr)
require.NoError(t, err)
zr, err := zip.NewReader(bytes.NewReader(decrypted), int64(len(decrypted)))
require.NoError(t, err)
dbFile, err := zr.Open("nwc.db")
require.NoError(t, err)
dbContents, err := io.ReadAll(dbFile)
require.NoError(t, err)
require.NoError(t, dbFile.Close())
require.Equal(t, "backup contents", string(dbContents))
}
// TestDecryptingReaderWrongPassword verifies that decryption fails upfront
// when the password does not match the backup file.
func TestDecryptingReaderWrongPassword(t *testing.T) {
var buf bytes.Buffer
cw, err := encryptingWriter(&buf, "test-unlock-password")
require.NoError(t, err)
zw := zip.NewWriter(cw)
entryWriter, err := zw.Create("nwc.db")
require.NoError(t, err)
_, err = entryWriter.Write([]byte("backup contents"))
require.NoError(t, err)
require.NoError(t, zw.Close())
_, err = decryptingReader(bytes.NewReader(buf.Bytes()), "wrong-password")
require.Error(t, err)
}

View file

@ -46,6 +46,15 @@ func (api *api) RequestEsploraApi(ctx context.Context, endpoint string) (interfa
return nil, errors.New("failed to read response body")
}
if res.StatusCode != http.StatusOK {
logger.Logger.WithFields(logrus.Fields{
"endpoint": endpoint,
"status_code": res.StatusCode,
"body": string(body),
}).Error("Esplora endpoint returned non-success code")
return nil, fmt.Errorf("esplora endpoint returned non-success code: %s", string(body))
}
var jsonContent interface{}
jsonErr := json.Unmarshal(body, &jsonContent)
if jsonErr != nil {

View file

@ -17,9 +17,9 @@ import (
)
func (api *api) RequestLSPOrder(ctx context.Context, request *LSPOrderRequest) (*LSPOrderResponse, error) {
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")
lnClient := api.svc.GetLNClient()
if lnClient == nil {
return nil, ErrLNClientNotStarted
}
if request.LSPType != lsp.LSP_TYPE_LSPS1 {
@ -28,7 +28,7 @@ func (api *api) RequestLSPOrder(ctx context.Context, request *LSPOrderRequest) (
logger.Logger.Info("Requesting own node info")
nodeInfo, err := api.svc.GetLNClient().GetInfo(ctx)
nodeInfo, err := lnClient.GetInfo(ctx)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"lspIdentifier": request.LSPIdentifier,
@ -46,7 +46,7 @@ func (api *api) RequestLSPOrder(ctx context.Context, request *LSPOrderRequest) (
logger.Logger.WithField("lspInfo", lspInfo).Info("Connecting to LSP node as a peer")
err = api.svc.GetLNClient().ConnectPeer(ctx, &lnclient.ConnectPeerRequest{
err = lnClient.ConnectPeer(ctx, &lnclient.ConnectPeerRequest{
Pubkey: lspInfo.Pubkey,
Address: lspInfo.Address,
Port: lspInfo.Port,
@ -57,9 +57,13 @@ func (api *api) RequestLSPOrder(ctx context.Context, request *LSPOrderRequest) (
return nil, err
}
invoice, fee, err := api.requestLSPS1Invoice(ctx, request, nodeInfo.Network, nodeInfo.Pubkey, lspInfo.MaxChannelExpiryBlocks, lspInfo.MinRequiredChannelConfirmations, lspInfo.MinFundingConfirmsWithinBlocks)
invoiceAmount := uint64(0)
incomingLiquidity := request.Amount
invoice, feeSat, err := api.requestLSPS1Invoice(ctx, lnClient, request, nodeInfo.Network, nodeInfo.Pubkey, lspInfo.MaxChannelExpiryBlocks, lspInfo.MinRequiredChannelConfirmations, lspInfo.MinFundingConfirmsWithinBlocks)
invoiceAmountSat := uint64(0)
incomingLiquiditySat := uint64(0)
resolvedAmountSat := ResolveToSat(request.AmountSat, nil, request.Amount, nil)
if resolvedAmountSat != nil {
incomingLiquiditySat = *resolvedAmountSat
}
if err != nil {
logger.Logger.WithError(err).Error("Failed to request invoice")
@ -73,15 +77,19 @@ func (api *api) RequestLSPOrder(ctx context.Context, request *LSPOrderRequest) (
return nil, err
}
invoiceAmount = uint64(paymentRequest.MSatoshi / 1000)
invoiceAmountSat = uint64(paymentRequest.MSatoshi / 1000)
}
newChannelResponse := &LSPOrderResponse{
Invoice: invoice,
Fee: fee,
InvoiceAmount: invoiceAmount,
IncomingLiquidity: incomingLiquidity,
OutgoingLiquidity: uint64(0), // JIT channel no longer supported
Invoice: invoice,
Fee: feeSat,
FeeSat: feeSat,
InvoiceAmount: invoiceAmountSat,
InvoiceAmountSat: invoiceAmountSat,
IncomingLiquidity: incomingLiquiditySat,
IncomingLiquiditySat: incomingLiquiditySat,
OutgoingLiquidity: uint64(0),
OutgoingLiquiditySat: uint64(0),
}
logger.Logger.WithFields(logrus.Fields{
@ -91,8 +99,8 @@ func (api *api) RequestLSPOrder(ctx context.Context, request *LSPOrderRequest) (
return newChannelResponse, nil
}
func (api *api) requestLSPS1Invoice(ctx context.Context, request *LSPOrderRequest, network, pubkey string, channelExpiryBlocks uint64, minRequiredChannelConfirmations uint64, minFundingConfirmsWithinBlocks uint64) (invoice string, fee uint64, err error) {
refundAddress, err := api.svc.GetLNClient().GetNewOnchainAddress(ctx)
func (api *api) requestLSPS1Invoice(ctx context.Context, lnClient lnclient.LNClient, request *LSPOrderRequest, network, pubkey string, channelExpiryBlocks uint64, minRequiredChannelConfirmations uint64, minFundingConfirmsWithinBlocks uint64) (invoice string, feeSat uint64, err error) {
refundAddress, err := lnClient.GetNewOnchainAddress(ctx)
if err != nil {
logger.Logger.WithError(err).Error("Failed to request onchain address")
return "", 0, err
@ -136,9 +144,16 @@ func (api *api) requestLSPS1Invoice(ctx context.Context, request *LSPOrderReques
token = "AlbyHub/" + version.Tag
}
amountSat := uint64(0)
resolvedAmountSat := ResolveToSat(request.AmountSat, nil, request.Amount, nil)
if resolvedAmountSat != nil {
amountSat = *resolvedAmountSat
}
lspBalanceSat := strconv.FormatUint(amountSat, 10)
lsps1ChannelRequest := &alby.LSPChannelRequest{
PublicKey: pubkey,
LSPBalanceSat: strconv.FormatUint(request.Amount, 10),
LSPBalanceSat: lspBalanceSat,
ClientBalanceSat: "0",
RequiredChannelConfirmations: requiredChannelConfirmations,
FundingConfirmsWithinBlocks: minFundingConfirmsWithinBlocks,
@ -155,7 +170,7 @@ func (api *api) requestLSPS1Invoice(ctx context.Context, request *LSPOrderReques
if channelResponse.Payment != nil {
invoice = channelResponse.Payment.Bolt11.Invoice
fee, err = strconv.ParseUint(channelResponse.Payment.Bolt11.FeeTotalSat, 10, 64)
feeSat, err = strconv.ParseUint(channelResponse.Payment.Bolt11.FeeTotalSat, 10, 64)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"lspIdentifier": request.LSPIdentifier,
@ -164,5 +179,5 @@ func (api *api) requestLSPS1Invoice(ctx context.Context, request *LSPOrderReques
}
}
return invoice, fee, nil
return invoice, feeSat, nil
}

View file

@ -2,34 +2,35 @@ package api
import (
"context"
"errors"
"io"
"time"
"github.com/getAlby/hub/alby"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/swaps"
)
type API interface {
CreateApp(createAppRequest *CreateAppRequest) (*CreateAppResponse, error)
UpdateApp(app *db.App, updateAppRequest *UpdateAppRequest) error
Transfer(ctx context.Context, fromAppId *uint, toAppId *uint, amountMsat uint64) error
Transfer(ctx context.Context, fromAppId *uint, toAppId *uint, amountMsat uint64, description string) error
DeleteApp(app *db.App) error
GetApp(app *db.App) *App
GetApp(app *db.App) (*App, error)
ListApps(limit uint64, offset uint64, filters ListAppsFilters, orderBy string) (*ListAppsResponse, error)
CreateLightningAddress(ctx context.Context, createLightningAddressRequest *CreateLightningAddressRequest) error
DeleteLightningAddress(ctx context.Context, appId uint) error
ListChannels(ctx context.Context) ([]Channel, error)
GetChannelPeerSuggestions(ctx context.Context) ([]alby.ChannelPeerSuggestion, error)
GetStories(ctx context.Context) ([]alby.Story, error)
GetLSPChannelOffer(ctx context.Context) (*alby.LSPChannelOffer, error)
ResetRouter(key string) error
ChangeUnlockPassword(changeUnlockPasswordRequest *ChangeUnlockPasswordRequest) error
SetAutoUnlockPassword(unlockPassword string) error
Stop() error
GetNodeConnectionInfo(ctx context.Context) (*lnclient.NodeConnectionInfo, error)
GetNodeStatus(ctx context.Context) (*lnclient.NodeStatus, error)
ListPeers(ctx context.Context) ([]lnclient.PeerDetails, error)
GetNodeConnectionInfo(ctx context.Context) (*NodeConnectionInfo, error)
GetNodeStatus(ctx context.Context) (*NodeStatus, error)
ListPeers(ctx context.Context) ([]PeerDetails, error)
ConnectPeer(ctx context.Context, connectPeerRequest *ConnectPeerRequest) error
DisconnectPeer(ctx context.Context, peerId string) error
OpenChannel(ctx context.Context, openChannelRequest *OpenChannelRequest) (*OpenChannelResponse, error)
@ -40,21 +41,20 @@ type API interface {
GetNewOnchainAddress(ctx context.Context) (string, error)
GetUnusedOnchainAddress(ctx context.Context) (string, error)
SignMessage(ctx context.Context, message string) (*SignMessageResponse, error)
RedeemOnchainFunds(ctx context.Context, toAddress string, amount uint64, feeRate *uint64, sendAll bool) (*RedeemOnchainFundsResponse, error)
RedeemOnchainFunds(ctx context.Context, toAddress string, amountSat uint64, feeRate *uint64, sendAll bool) (*RedeemOnchainFundsResponse, error)
GetBalances(ctx context.Context) (*BalancesResponse, error)
ListTransactions(ctx context.Context, appId *uint, limit uint64, offset uint64) (*ListTransactionsResponse, error)
ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error)
SendPayment(ctx context.Context, invoice string, amountMsat *uint64, metadata map[string]interface{}) (*SendPaymentResponse, error)
CreateInvoice(ctx context.Context, amount uint64, description string) (*MakeInvoiceResponse, error)
ListTransactions(ctx context.Context, appId *uint, limit uint64, offset uint64, filters ListTransactionsFilters) (*ListTransactionsResponse, error)
ListOnchainTransactions(ctx context.Context) ([]OnchainTransaction, error)
SendPayment(ctx context.Context, invoice string, amountMsat *uint64, metadata map[string]interface{}, fromAppId *uint) (*SendPaymentResponse, error)
CreateInvoice(ctx context.Context, amountMsat uint64, description string, toAppId *uint) (*MakeInvoiceResponse, error)
LookupInvoice(ctx context.Context, paymentHash string) (*LookupInvoiceResponse, error)
SetTransactionUserLabels(ctx context.Context, id uint, labels map[string]string) error
RequestMempoolApi(ctx context.Context, endpoint string) (interface{}, error)
GetInfo(ctx context.Context) (*InfoResponse, error)
GetMnemonic(unlockPassword string) (*MnemonicResponse, error)
SetNextBackupReminder(backupReminderRequest *BackupReminderRequest) error
Start(startRequest *StartRequest)
Setup(ctx context.Context, setupRequest *SetupRequest) error
SendPaymentProbes(ctx context.Context, sendPaymentProbesRequest *SendPaymentProbesRequest) (*SendPaymentProbesResponse, error)
SendSpontaneousPaymentProbes(ctx context.Context, sendSpontaneousPaymentProbesRequest *SendSpontaneousPaymentProbesRequest) (*SendSpontaneousPaymentProbesResponse, error)
GetNetworkGraph(ctx context.Context, nodeIds []string) (NetworkGraphResponse, error)
SyncWallet() error
GetLogOutput(ctx context.Context, logType string, getLogRequest *GetLogOutputRequest) (*GetLogOutputResponse, error)
@ -64,8 +64,6 @@ type API interface {
MigrateNodeStorage(ctx context.Context, to string) error
GetWalletCapabilities(ctx context.Context) (*WalletCapabilitiesResponse, error)
Health(ctx context.Context) (*HealthResponse, error)
SetCurrency(currency string) error
SetBitcoinDisplayFormat(format string) error
UpdateSettings(updateSettingsRequest *UpdateSettingsRequest) error
LookupSwap(swapId string) (*LookupSwapResponse, error)
ListSwaps() (*ListSwapsResponse, error)
@ -85,24 +83,33 @@ type API interface {
GetForwards() (*GetForwardsResponse, error)
}
var ErrLNClientNotStarted = errors.New("LNClient not started")
type App struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
AppPubkey string `json:"appPubkey"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
LastUsedAt *time.Time `json:"lastUsedAt"`
ExpiresAt *time.Time `json:"expiresAt"`
Scopes []string `json:"scopes"`
MaxAmountSat uint64 `json:"maxAmount"`
BudgetUsage uint64 `json:"budgetUsage"`
BudgetRenewal string `json:"budgetRenewal"`
Isolated bool `json:"isolated"`
WalletPubkey string `json:"walletPubkey"`
UniqueWalletPubkey bool `json:"uniqueWalletPubkey"`
Balance int64 `json:"balance"`
Metadata Metadata `json:"metadata,omitempty"`
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
AppPubkey string `json:"appPubkey"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
LastUsedAt *time.Time `json:"lastUsedAt"`
LastSettledTransactionAt *time.Time `json:"lastSettledTransactionAt"`
ExpiresAt *time.Time `json:"expiresAt"`
Scopes []string `json:"scopes"`
MaxAmount uint64 `json:"maxAmount"` // deprecated
MaxAmountSat uint64 `json:"maxAmountSat"`
MaxAmountMsat uint64 `json:"maxAmountMsat"`
BudgetUsage uint64 `json:"budgetUsage"` // deprecated
BudgetUsageSat uint64 `json:"budgetUsageSat"`
BudgetUsageMsat uint64 `json:"budgetUsageMsat"`
BudgetRenewal string `json:"budgetRenewal"`
Isolated bool `json:"isolated"`
WalletPubkey string `json:"walletPubkey"`
UniqueWalletPubkey bool `json:"uniqueWalletPubkey"`
Balance int64 `json:"balance"` // deprecated
BalanceSat int64 `json:"balanceSat"`
BalanceMsat int64 `json:"balanceMsat"`
Metadata Metadata `json:"metadata,omitempty"`
}
type ListAppsFilters struct {
@ -113,13 +120,18 @@ type ListAppsFilters struct {
}
type ListAppsResponse struct {
Apps []App `json:"apps"`
TotalCount uint64 `json:"totalCount"`
Apps []App `json:"apps"`
TotalCount uint64 `json:"totalCount"`
TotalBalance *int64 `json:"totalBalance,omitempty"` // deprecated
TotalBalanceSat *int64 `json:"totalBalanceSat,omitempty"`
TotalBalanceMsat *int64 `json:"totalBalanceMsat,omitempty"`
}
type UpdateAppRequest struct {
Name *string `json:"name"`
MaxAmountSat *uint64 `json:"maxAmount"`
MaxAmount *uint64 `json:"maxAmount"` // deprecated
MaxAmountSat *uint64 `json:"maxAmountSat"`
MaxAmountMsat *uint64 `json:"maxAmountMsat"`
BudgetRenewal *string `json:"budgetRenewal"`
ExpiresAt *string `json:"expiresAt"`
UpdateExpiresAt bool `json:"updateExpiresAt"`
@ -129,15 +141,19 @@ type UpdateAppRequest struct {
}
type TransferRequest struct {
AmountSat uint64 `json:"amountSat"`
FromAppId *uint `json:"fromAppId"`
ToAppId *uint `json:"toAppId"`
AmountSat *uint64 `json:"amountSat"`
AmountMsat *uint64 `json:"amountMsat"`
FromAppId *uint `json:"fromAppId"`
ToAppId *uint `json:"toAppId"`
Description string `json:"description"`
}
type CreateAppRequest struct {
Name string `json:"name"`
Pubkey string `json:"pubkey"`
MaxAmountSat uint64 `json:"maxAmount"`
MaxAmount *uint64 `json:"maxAmount"` // deprecated
MaxAmountSat *uint64 `json:"maxAmountSat"`
MaxAmountMsat *uint64 `json:"maxAmountMsat"`
BudgetRenewal string `json:"budgetRenewal"`
ExpiresAt string `json:"expiresAt"`
Scopes []string `json:"scopes"`
@ -153,8 +169,9 @@ type CreateLightningAddressRequest struct {
}
type InitiateSwapRequest struct {
SwapAmount uint64 `json:"swapAmount"`
Destination string `json:"destination"`
SwapAmount *uint64 `json:"swapAmount"` // deprecated
SwapAmountSat *uint64 `json:"swapAmountSat"`
Destination string `json:"destination"`
}
type RefundSwapRequest struct {
@ -163,25 +180,34 @@ type RefundSwapRequest struct {
}
type EnableAutoSwapRequest struct {
BalanceThreshold uint64 `json:"balanceThreshold"`
SwapAmount uint64 `json:"swapAmount"`
Destination string `json:"destination"`
BalanceThreshold *uint64 `json:"balanceThreshold"` // deprecated
BalanceThresholdSat *uint64 `json:"balanceThresholdSat"`
SwapAmount *uint64 `json:"swapAmount"` // deprecated
SwapAmountSat *uint64 `json:"swapAmountSat"`
Destination string `json:"destination"`
DestinationType string `json:"destinationType"`
UnlockPassword string `json:"unlockPassword"`
}
type GetAutoSwapConfigResponse struct {
Type string `json:"type"`
Enabled bool `json:"enabled"`
BalanceThreshold uint64 `json:"balanceThreshold"`
SwapAmount uint64 `json:"swapAmount"`
Destination string `json:"destination"`
Type string `json:"type"`
Enabled bool `json:"enabled"`
BalanceThreshold uint64 `json:"balanceThreshold"` // deprecated
BalanceThresholdSat uint64 `json:"balanceThresholdSat"`
SwapAmount uint64 `json:"swapAmount"` // deprecated
SwapAmountSat uint64 `json:"swapAmountSat"`
Destination string `json:"destination"`
}
type SwapInfoResponse struct {
AlbyServiceFee float64 `json:"albyServiceFee"`
BoltzServiceFee float64 `json:"boltzServiceFee"`
BoltzNetworkFee uint64 `json:"boltzNetworkFee"`
MinAmount uint64 `json:"minAmount"`
MaxAmount uint64 `json:"maxAmount"`
AlbyServiceFee float64 `json:"albyServiceFee"`
BoltzServiceFee float64 `json:"boltzServiceFee"`
BoltzNetworkFee uint64 `json:"boltzNetworkFee"` // deprecated
BoltzNetworkFeeSat uint64 `json:"boltzNetworkFeeSat"`
MinAmount uint64 `json:"minAmount"` // deprecated
MinAmountSat uint64 `json:"minAmountSat"`
MaxAmount uint64 `json:"maxAmount"` // deprecated
MaxAmountSat uint64 `json:"maxAmountSat"`
}
type ListSwapsResponse struct {
@ -195,8 +221,10 @@ type Swap struct {
Type string `json:"type"`
State string `json:"state"`
Invoice string `json:"invoice"`
SendAmount uint64 `json:"sendAmount"`
ReceiveAmount uint64 `json:"receiveAmount"`
SendAmount uint64 `json:"sendAmount"` // deprecated
SendAmountSat uint64 `json:"sendAmountSat"`
ReceiveAmount uint64 `json:"receiveAmount"` // deprecated
ReceiveAmountSat uint64 `json:"receiveAmountSat"`
PaymentHash string `json:"paymentHash"`
DestinationAddress string `json:"destinationAddress"`
RefundAddress string `json:"refundAddress"`
@ -240,8 +268,6 @@ type SetupRequest struct {
LNDAddress string `json:"lndAddress"`
LNDCertFile string `json:"lndCertFile"`
LNDMacaroonFile string `json:"lndMacaroonFile"`
LNDCertHex string `json:"lndCertHex"`
LNDMacaroonHex string `json:"lndMacaroonHex"`
// Phoenixd fields
PhoenixdAddress string `json:"phoenixdAddress"`
@ -249,6 +275,11 @@ type SetupRequest struct {
// Cashu fields
CashuMintUrl string `json:"cashuMintUrl"`
// CLN fields
CLNAddress string `json:"clnAddress"`
CLNLightningDir string `json:"clnLightningDir"`
CLNAddressHold string `json:"clnAddressHold"`
}
type CreateAppResponse struct {
@ -273,35 +304,47 @@ type InfoResponseRelay struct {
}
type InfoResponse struct {
BackendType string `json:"backendType"`
SetupCompleted bool `json:"setupCompleted"`
OAuthRedirect bool `json:"oauthRedirect"`
Running bool `json:"running"`
Unlocked bool `json:"unlocked"`
AlbyAuthUrl string `json:"albyAuthUrl"`
NextBackupReminder string `json:"nextBackupReminder"`
AlbyUserIdentifier string `json:"albyUserIdentifier"`
AlbyAccountConnected bool `json:"albyAccountConnected"`
Version string `json:"version"`
Network string `json:"network"`
EnableAdvancedSetup bool `json:"enableAdvancedSetup"`
LdkVssEnabled bool `json:"ldkVssEnabled"`
VssSupported bool `json:"vssSupported"`
StartupState string `json:"startupState"`
StartupError string `json:"startupError"`
StartupErrorTime time.Time `json:"startupErrorTime"`
AutoUnlockPasswordSupported bool `json:"autoUnlockPasswordSupported"`
AutoUnlockPasswordEnabled bool `json:"autoUnlockPasswordEnabled"`
Currency string `json:"currency"`
BitcoinDisplayFormat string `json:"bitcoinDisplayFormat"`
Relays []InfoResponseRelay `json:"relays"`
NodeAlias string `json:"nodeAlias"`
MempoolUrl string `json:"mempoolUrl"`
BackendType string `json:"backendType"`
SetupCompleted bool `json:"setupCompleted"`
OAuthRedirect bool `json:"oauthRedirect"`
Running bool `json:"running"`
Unlocked bool `json:"unlocked"`
AlbyAuthUrl string `json:"albyAuthUrl"`
NextBackupReminder string `json:"nextBackupReminder"`
AlbyUserIdentifier string `json:"albyUserIdentifier"`
AlbyAccountConnected bool `json:"albyAccountConnected"`
Version string `json:"version"`
Network string `json:"network"`
EnableAdvancedSetup bool `json:"enableAdvancedSetup"`
LdkVssEnabled bool `json:"ldkVssEnabled"`
LdkVssUrl string `json:"ldkVssUrl"`
VssSupported bool `json:"vssSupported"`
DatabaseType string `json:"databaseType"`
StartupState string `json:"startupState"`
StartupError string `json:"startupError"`
StartupErrorTime time.Time `json:"startupErrorTime"`
AutoUnlockPasswordSupported bool `json:"autoUnlockPasswordSupported"`
AutoUnlockPasswordEnabled bool `json:"autoUnlockPasswordEnabled"`
Currency string `json:"currency"`
BitcoinDisplayFormat string `json:"bitcoinDisplayFormat"`
Relays []InfoResponseRelay `json:"relays"`
NodeAlias string `json:"nodeAlias"`
MempoolUrl string `json:"mempoolUrl"`
ChainDataSourceType string `json:"chainDataSourceType,omitempty"`
ChainDataSourceAddress string `json:"chainDataSourceAddress,omitempty"`
JitChannelsLiquiditySource string `json:"jitChannelsLiquiditySource,omitempty"`
JitChannelsMinPaymentSizeMsat *uint64 `json:"jitChannelsMinPaymentSizeMsat,omitempty"`
JitChannelsMaxPaymentSizeMsat *uint64 `json:"jitChannelsMaxPaymentSizeMsat,omitempty"`
JitChannelsEnabled bool `json:"jitChannelsEnabled"`
HideUpdateBanner bool `json:"hideUpdateBanner"`
SupportsBolt12 bool `json:"supportsBolt12"`
NodeMigrationFileCreated bool `json:"nodeMigrationFileCreated"`
}
type UpdateSettingsRequest struct {
Currency string `json:"currency"`
BitcoinDisplayFormat string `json:"bitcoinDisplayFormat"`
JitChannelsEnabled *bool `json:"jitChannelsEnabled"`
}
type SetNodeAliasRequest struct {
@ -324,23 +367,83 @@ type AutoUnlockRequest struct {
UnlockPassword string `json:"unlockPassword"`
}
type ConnectPeerRequest = lnclient.ConnectPeerRequest
type OpenChannelRequest = lnclient.OpenChannelRequest
type OpenChannelResponse = lnclient.OpenChannelResponse
type CloseChannelResponse = lnclient.CloseChannelResponse
type UpdateChannelRequest = lnclient.UpdateChannelRequest
type ConnectPeerRequest struct {
Pubkey string `json:"pubkey"`
Address string `json:"address"`
Port uint16 `json:"port"`
}
type OpenChannelRequest struct {
Pubkey string `json:"pubkey"`
AmountSats int64 `json:"amountSats"`
Public bool `json:"public"`
}
type OpenChannelResponse struct {
FundingTxId string `json:"fundingTxId"`
}
type CloseChannelResponse struct {
}
type UpdateChannelRequest struct {
ChannelId string `json:"channelId"`
NodeId string `json:"nodeId"`
ForwardingFeeBaseMsat uint32 `json:"forwardingFeeBaseMsat"`
ForwardingFeeProportionalMillionths uint32 `json:"forwardingFeeProportionalMillionths"`
MaxDustHtlcExposureFromFeeRateMultiplier uint64 `json:"maxDustHtlcExposureFromFeeRateMultiplier"`
}
type NodeConnectionInfo struct {
Pubkey string `json:"pubkey"`
Address string `json:"address"`
Port int `json:"port"`
}
type NodeStatus struct {
IsReady bool `json:"isReady"`
InternalNodeStatus interface{} `json:"internalNodeStatus"`
}
type PeerDetails struct {
NodeId string `json:"nodeId"`
Address string `json:"address"`
IsPersisted bool `json:"isPersisted"`
IsConnected bool `json:"isConnected"`
}
type OnchainTransaction struct {
AmountSat uint64 `json:"amountSat"`
CreatedAt uint64 `json:"createdAt"`
State string `json:"state"`
Type string `json:"type"`
NumConfirmations uint32 `json:"numConfirmations"`
TxId string `json:"txId"`
}
type PendingBalanceDetails struct {
ChannelId string `json:"channelId"`
NodeId string `json:"nodeId"`
Amount uint64 `json:"amount"` // deprecated
AmountSat uint64 `json:"amountSat"`
FundingTxId string `json:"fundingTxId"`
FundingTxVout uint32 `json:"fundingTxVout"`
}
type RebalanceChannelRequest struct {
ReceiveThroughNodePubkey string `json:"receiveThroughNodePubkey"`
AmountSat uint64 `json:"amountSat"`
ReceiveThroughNodePubkey string `json:"receiveThroughNodePubkey"`
AmountSat *uint64 `json:"amountSat"`
AmountMsat *uint64 `json:"amountMsat"`
}
type RebalanceChannelResponse struct {
TotalFeeSat uint64 `json:"totalFeeSat"`
TotalFeeSat uint64 `json:"totalFeeSat"`
TotalFeeMsat uint64 `json:"totalFeeMsat"`
}
type RedeemOnchainFundsRequest struct {
ToAddress string `json:"toAddress"`
Amount uint64 `json:"amount"`
Amount *uint64 `json:"amount"` // deprecated
AmountSat *uint64 `json:"amountSat"`
FeeRate *uint64 `json:"feeRate"`
SendAll bool `json:"sendAll"`
}
@ -349,13 +452,61 @@ type RedeemOnchainFundsResponse struct {
TxId string `json:"txId"`
}
type OnchainBalanceResponse = lnclient.OnchainBalanceResponse
type BalancesResponse = lnclient.BalancesResponse
type OnchainBalanceResponse struct {
Spendable int64 `json:"spendable"` // deprecated
SpendableSat int64 `json:"spendableSat"`
Total int64 `json:"total"` // deprecated
TotalSat int64 `json:"totalSat"`
Reserved int64 `json:"reserved"` // deprecated
ReservedSat int64 `json:"reservedSat"`
PendingBalancesFromChannelClosures uint64 `json:"pendingBalancesFromChannelClosures"` // deprecated
PendingBalancesFromChannelClosuresSat uint64 `json:"pendingBalancesFromChannelClosuresSat"`
PendingBalancesDetails []PendingBalanceDetails `json:"pendingBalancesDetails"`
PendingSweepBalancesDetails []PendingBalanceDetails `json:"pendingSweepBalancesDetails"`
InternalBalances interface{} `json:"internalBalances"`
}
type LightningBalanceResponse struct {
TotalSpendable int64 `json:"totalSpendable"` // deprecated
TotalSpendableSat int64 `json:"totalSpendableSat"`
TotalSpendableMsat int64 `json:"totalSpendableMsat"`
TotalReceivable int64 `json:"totalReceivable"` // deprecated
TotalReceivableSat int64 `json:"totalReceivableSat"`
TotalReceivableMsat int64 `json:"totalReceivableMsat"`
NextMaxSpendable int64 `json:"nextMaxSpendable"` // deprecated
NextMaxSpendableSat int64 `json:"nextMaxSpendableSat"`
NextMaxSpendableMsat int64 `json:"nextMaxSpendableMsat"`
NextMaxReceivable int64 `json:"nextMaxReceivable"` // deprecated
NextMaxReceivableSat int64 `json:"nextMaxReceivableSat"`
NextMaxReceivableMsat int64 `json:"nextMaxReceivableMsat"`
NextMaxSpendableMPP int64 `json:"nextMaxSpendableMPP"` // deprecated
NextMaxSpendableMPPSat int64 `json:"nextMaxSpendableMPPSat"`
NextMaxSpendableMPPMsat int64 `json:"nextMaxSpendableMPPMsat"`
NextMaxReceivableMPP int64 `json:"nextMaxReceivableMPP"` // deprecated
NextMaxReceivableMPPSat int64 `json:"nextMaxReceivableMPPSat"`
NextMaxReceivableMPPMsat int64 `json:"nextMaxReceivableMPPMsat"`
}
type BalancesResponse struct {
Onchain OnchainBalanceResponse `json:"onchain"`
Lightning LightningBalanceResponse `json:"lightning"`
}
type SendPaymentResponse = Transaction
type MakeInvoiceResponse = Transaction
type LookupInvoiceResponse = Transaction
type SetTransactionUserLabelsRequest struct {
Labels map[string]string `json:"labels"`
}
type ListTransactionsFilters struct {
Type *string
MinAmountMsat *uint64
HideFailed bool
SearchTerm string
}
type ListTransactionsResponse struct {
TotalCount uint64 `json:"totalCount"`
Transactions []Transaction `json:"transactions"`
@ -363,6 +514,7 @@ type ListTransactionsResponse struct {
// TODO: camelCase
type Transaction struct {
ID uint `json:"id"`
Type string `json:"type"`
State string `json:"state"`
Invoice string `json:"invoice"`
@ -370,8 +522,12 @@ type Transaction struct {
DescriptionHash string `json:"descriptionHash"`
Preimage *string `json:"preimage"`
PaymentHash string `json:"paymentHash"`
Amount uint64 `json:"amount"`
FeesPaid uint64 `json:"feesPaid"`
Amount uint64 `json:"amount"` // deprecated
AmountSat uint64 `json:"amountSat"`
AmountMsat uint64 `json:"amountMsat"`
FeesPaid uint64 `json:"feesPaid"` // deprecated
FeesPaidSat uint64 `json:"feesPaidSat"`
FeesPaidMsat uint64 `json:"feesPaidMsat"`
UpdatedAt string `json:"updatedAt"`
CreatedAt string `json:"createdAt"`
SettledAt *string `json:"settledAt"`
@ -397,27 +553,10 @@ type Boostagram struct {
SenderName string `json:"senderName"`
Time string `json:"time"`
Action string `json:"action"`
ValueSatTotal int64 `json:"valueSatTotal"`
ValueMsatTotal int64 `json:"valueMsatTotal"`
}
// debug api
type SendPaymentProbesRequest struct {
Invoice string `json:"invoice"`
}
type SendPaymentProbesResponse struct {
Error string `json:"error"`
}
type SendSpontaneousPaymentProbesRequest struct {
Amount uint64 `json:"amount"`
NodeId string `json:"nodeId"`
}
type SendSpontaneousPaymentProbesResponse struct {
Error string `json:"error"`
}
const (
LogTypeNode = "node"
LogTypeApp = "app"
@ -441,8 +580,11 @@ type SignMessageResponse struct {
}
type PayInvoiceRequest struct {
Amount *uint64 `json:"amount"`
Metadata Metadata `json:"metadata"`
Amount *uint64 `json:"amount"` // deprecated
AmountSat *uint64 `json:"amountSat"`
AmountMsat *uint64 `json:"amountMsat"`
Metadata Metadata `json:"metadata"`
FromAppID *uint `json:"fromAppId"`
}
type MakeOfferRequest struct {
@ -450,8 +592,11 @@ type MakeOfferRequest struct {
}
type MakeInvoiceRequest struct {
Amount uint64 `json:"amount"`
Description string `json:"description"`
Amount *uint64 `json:"amount"` // deprecated
AmountSat *uint64 `json:"amountSat"`
AmountMsat *uint64 `json:"amountMsat"`
Description string `json:"description"`
ToAppID *uint `json:"toAppId"`
}
type ResetRouterRequest struct {
@ -466,21 +611,26 @@ type BasicRestoreWailsRequest struct {
UnlockPassword string `json:"unlockPassword"`
}
type NetworkGraphResponse = lnclient.NetworkGraphResponse
type NetworkGraphResponse = interface{}
type LSPOrderRequest struct {
Amount uint64 `json:"amount"`
LSPType string `json:"lspType"`
LSPIdentifier string `json:"lspIdentifier"`
Public bool `json:"public"`
Amount *uint64 `json:"amount"` // deprecated
AmountSat *uint64 `json:"amountSat"`
LSPType string `json:"lspType"`
LSPIdentifier string `json:"lspIdentifier"`
Public bool `json:"public"`
}
type LSPOrderResponse struct {
Invoice string `json:"invoice"`
Fee uint64 `json:"fee"`
InvoiceAmount uint64 `json:"invoiceAmount"`
IncomingLiquidity uint64 `json:"incomingLiquidity"`
OutgoingLiquidity uint64 `json:"outgoingLiquidity"`
Invoice string `json:"invoice"`
Fee uint64 `json:"fee"` // deprecated
FeeSat uint64 `json:"feeSat"`
InvoiceAmount uint64 `json:"invoiceAmount"` // deprecated
InvoiceAmountSat uint64 `json:"invoiceAmountSat"`
IncomingLiquidity uint64 `json:"incomingLiquidity"` // deprecated
IncomingLiquiditySat uint64 `json:"incomingLiquiditySat"`
OutgoingLiquidity uint64 `json:"outgoingLiquidity"` // deprecated
OutgoingLiquiditySat uint64 `json:"outgoingLiquiditySat"`
}
type WalletCapabilitiesResponse struct {
@ -490,25 +640,33 @@ type WalletCapabilitiesResponse struct {
}
type Channel struct {
LocalBalance int64 `json:"localBalance"`
LocalSpendableBalance int64 `json:"localSpendableBalance"`
RemoteBalance int64 `json:"remoteBalance"`
Id string `json:"id"`
RemotePubkey string `json:"remotePubkey"`
FundingTxId string `json:"fundingTxId"`
FundingTxVout uint32 `json:"fundingTxVout"`
Active bool `json:"active"`
Public bool `json:"public"`
InternalChannel interface{} `json:"internalChannel"`
Confirmations *uint32 `json:"confirmations"`
ConfirmationsRequired *uint32 `json:"confirmationsRequired"`
ForwardingFeeBaseMsat uint32 `json:"forwardingFeeBaseMsat"`
ForwardingFeeProportionalMillionths uint32 `json:"forwardingFeeProportionalMillionths"`
UnspendablePunishmentReserve uint64 `json:"unspendablePunishmentReserve"`
CounterpartyUnspendablePunishmentReserve uint64 `json:"counterpartyUnspendablePunishmentReserve"`
Error *string `json:"error"`
Status string `json:"status"`
IsOutbound bool `json:"isOutbound"`
LocalBalance int64 `json:"localBalance"` // deprecated
LocalBalanceSat int64 `json:"localBalanceSat"`
LocalBalanceMsat int64 `json:"localBalanceMsat"`
LocalSpendableBalance int64 `json:"localSpendableBalance"` // deprecated
LocalSpendableBalanceSat int64 `json:"localSpendableBalanceSat"`
LocalSpendableBalanceMsat int64 `json:"localSpendableBalanceMsat"`
RemoteBalance int64 `json:"remoteBalance"` // deprecated
RemoteBalanceSat int64 `json:"remoteBalanceSat"`
RemoteBalanceMsat int64 `json:"remoteBalanceMsat"`
Id string `json:"id"`
RemotePubkey string `json:"remotePubkey"`
FundingTxId string `json:"fundingTxId"`
FundingTxVout uint32 `json:"fundingTxVout"`
Active bool `json:"active"`
Public bool `json:"public"`
InternalChannel interface{} `json:"internalChannel"`
Confirmations *uint32 `json:"confirmations"`
ConfirmationsRequired *uint32 `json:"confirmationsRequired"`
ForwardingFeeBaseMsat uint32 `json:"forwardingFeeBaseMsat"` // expressed only in msat as per Lightning spec
ForwardingFeeProportionalMillionths uint32 `json:"forwardingFeeProportionalMillionths"`
UnspendablePunishmentReserve uint64 `json:"unspendablePunishmentReserve"` // deprecated
UnspendablePunishmentReserveSat uint64 `json:"unspendablePunishmentReserveSat"`
CounterpartyUnspendablePunishmentReserve uint64 `json:"counterpartyUnspendablePunishmentReserve"` // deprecated
CounterpartyUnspendablePunishmentReserveSat uint64 `json:"counterpartyUnspendablePunishmentReserveSat"`
Error *string `json:"error"`
Status string `json:"status"`
IsOutbound bool `json:"isOutbound"`
}
type MigrateNodeStorageRequest struct {
@ -561,7 +719,53 @@ type ExecuteCustomNodeCommandRequest struct {
}
type GetForwardsResponse struct {
OutboundAmountForwardedSat uint64 `json:"outboundAmountForwardedSat"`
OutboundAmountForwardedMsat uint64 `json:"outboundAmountForwardedMsat"`
TotalFeeEarnedSat uint64 `json:"totalFeeEarnedSat"`
TotalFeeEarnedMsat uint64 `json:"totalFeeEarnedMsat"`
NumForwards uint64 `json:"numForwards"`
}
func ResolveToSat(satValue *uint64, msatValue *uint64, legacyValueSat *uint64, legacyValueMsat *uint64) (resolvedSatValue *uint64) {
if legacyValueSat != nil {
resolvedSatValue = legacyValueSat
}
if legacyValueMsat != nil {
tmpSat := *legacyValueMsat / 1000
resolvedSatValue = &tmpSat
}
if satValue != nil {
resolvedSatValue = satValue
}
if msatValue != nil {
tmpSat := *msatValue / 1000
resolvedSatValue = &tmpSat
}
return resolvedSatValue
}
func ResolveToMsat(satValue *uint64, msatValue *uint64, legacyValueSat *uint64, legacyValueMsat *uint64) (resolvedMsatValue *uint64) {
if legacyValueSat != nil {
tmpMsat := *legacyValueSat * 1000
resolvedMsatValue = &tmpMsat
}
if legacyValueMsat != nil {
resolvedMsatValue = legacyValueMsat
}
if satValue != nil {
tmpMsat := *satValue * 1000
resolvedMsatValue = &tmpMsat
}
if msatValue != nil {
resolvedMsatValue = msatValue
}
return resolvedMsatValue
}

View file

@ -18,15 +18,22 @@ import (
)
func (api *api) RebalanceChannel(ctx context.Context, rebalanceChannelRequest *RebalanceChannelRequest) (*RebalanceChannelResponse, error) {
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")
lnClient := api.svc.GetLNClient()
if lnClient == nil {
return nil, ErrLNClientNotStarted
}
amountSat := uint64(0)
resolvedAmountSat := ResolveToSat(rebalanceChannelRequest.AmountSat, rebalanceChannelRequest.AmountMsat, nil, nil)
if resolvedAmountSat != nil {
amountSat = *resolvedAmountSat
}
receiveMetadata := map[string]interface{}{
"receive_through": rebalanceChannelRequest.ReceiveThroughNodePubkey,
}
receiveInvoice, err := api.svc.GetTransactionsService().MakeInvoice(ctx, rebalanceChannelRequest.AmountSat*1000, "Alby Hub Rebalance through "+rebalanceChannelRequest.ReceiveThroughNodePubkey, "", 0, receiveMetadata, api.svc.GetLNClient(), nil, nil, &rebalanceChannelRequest.ReceiveThroughNodePubkey)
receiveInvoice, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amountSat*1000, "Alby Hub Rebalance through "+rebalanceChannelRequest.ReceiveThroughNodePubkey, "", 0, receiveMetadata, lnClient, nil, nil, &rebalanceChannelRequest.ReceiveThroughNodePubkey)
if err != nil {
logger.Logger.WithError(err).Error("failed to generate rebalance receive invoice")
return nil, err
@ -83,7 +90,7 @@ func (api *api) RebalanceChannel(ctx context.Context, rebalanceChannelRequest *R
return nil, errors.New("failed to read response body")
}
if res.StatusCode >= 300 {
if res.StatusCode != http.StatusOK {
logger.Logger.WithFields(logrus.Fields{
"request": newRspCreateOrderRequest,
"body": string(body),
@ -115,17 +122,17 @@ func (api *api) RebalanceChannel(ctx context.Context, rebalanceChannelRequest *R
return nil, err
}
if paymentRequest.MSatoshi > int64(float64(rebalanceChannelRequest.AmountSat)*float64(1000)*float64(1.003)+1 /*0.3% fees*/) {
if paymentRequest.MSatoshi > int64(float64(amountSat)*float64(1000)*float64(1.005)+1 /*0.5% fees*/) {
return nil, errors.New("rebalance payment is more expensive than expected")
}
payMetadata := map[string]interface{}{
"receive_through": rebalanceChannelRequest.ReceiveThroughNodePubkey,
"amount_sat": rebalanceChannelRequest.AmountSat,
"amount_sat": amountSat,
"order_id": rebalanceCreateOrderResponse.OrderId,
}
payRebalanceInvoiceResponse, err := api.svc.GetTransactionsService().SendPaymentSync(rebalanceCreateOrderResponse.PayRequest, nil, payMetadata, api.svc.GetLNClient(), nil, nil)
payRebalanceInvoiceResponse, err := api.svc.GetTransactionsService().SendPaymentSync(rebalanceCreateOrderResponse.PayRequest, nil, payMetadata, lnClient, nil, nil)
if err != nil {
logger.Logger.WithError(err).Error("failed to pay rebalance invoice")
@ -137,7 +144,10 @@ func (api *api) RebalanceChannel(ctx context.Context, rebalanceChannelRequest *R
Properties: map[string]interface{}{},
})
totalFeeMsat := uint64(paymentRequest.MSatoshi) + payRebalanceInvoiceResponse.FeeMsat - amountSat*1000
return &RebalanceChannelResponse{
TotalFeeSat: uint64(paymentRequest.MSatoshi)/1000 + payRebalanceInvoiceResponse.FeeMsat/1000 - rebalanceChannelRequest.AmountSat,
TotalFeeSat: totalFeeMsat / 1000,
TotalFeeMsat: totalFeeMsat,
}, nil
}

161
api/setup_test.go Normal file
View file

@ -0,0 +1,161 @@
package api
import (
"crypto/ecdsa"
"crypto/elliptic"
crand "crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/pem"
"math/big"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
"gopkg.in/macaroon.v2"
)
// generateTestCert returns a self-signed certificate PEM block and its
// matching EC private key PEM block.
func generateTestCert(t *testing.T) (certPEM []byte, keyPEM []byte) {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), crand.Reader)
require.NoError(t, err)
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "test"},
NotBefore: time.Unix(0, 0),
NotAfter: time.Unix(1<<31, 0),
}
der, err := x509.CreateCertificate(crand.Reader, &template, &template, &key.PublicKey, key)
require.NoError(t, err)
certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
keyDER, err := x509.MarshalPKCS8PrivateKey(key)
require.NoError(t, err)
keyPEM = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})
return certPEM, keyPEM
}
func TestReadAndCanonicalizeLNDCert(t *testing.T) {
certPEM, keyPEM := generateTestCert(t)
dir := t.TempDir()
t.Run("valid certificate", func(t *testing.T) {
path := filepath.Join(dir, "tls.cert")
require.NoError(t, os.WriteFile(path, certPEM, 0600))
got, err := readAndCanonicalizeLNDCert(path)
require.NoError(t, err)
raw, err := hex.DecodeString(got)
require.NoError(t, err)
require.True(t, x509.NewCertPool().AppendCertsFromPEM(raw))
})
t.Run("bundled private key is stripped", func(t *testing.T) {
path := filepath.Join(dir, "bundle.pem")
require.NoError(t, os.WriteFile(path, append(append([]byte{}, certPEM...), keyPEM...), 0600))
got, err := readAndCanonicalizeLNDCert(path)
require.NoError(t, err)
raw, err := hex.DecodeString(got)
require.NoError(t, err)
// Only the CERTIFICATE block must survive - the private key must not
// be persisted.
require.NotContains(t, string(raw), "PRIVATE KEY")
require.Contains(t, string(raw), "CERTIFICATE")
})
t.Run("arbitrary non-cert file is rejected", func(t *testing.T) {
path := filepath.Join(dir, "secret.txt")
require.NoError(t, os.WriteFile(path, []byte("root:x:0:0:root:/root:/bin/bash\n"), 0600))
_, err := readAndCanonicalizeLNDCert(path)
require.Error(t, err)
})
t.Run("missing file is rejected", func(t *testing.T) {
_, err := readAndCanonicalizeLNDCert(filepath.Join(dir, "does-not-exist"))
require.Error(t, err)
})
}
func TestReadAndCanonicalizeLNDMacaroon(t *testing.T) {
dir := t.TempDir()
t.Run("valid macaroon", func(t *testing.T) {
mac, err := macaroon.New([]byte("root-key"), []byte("id"), "location", macaroon.LatestVersion)
require.NoError(t, err)
raw, err := mac.MarshalBinary()
require.NoError(t, err)
path := filepath.Join(dir, "admin.macaroon")
require.NoError(t, os.WriteFile(path, raw, 0600))
got, err := readAndCanonicalizeLNDMacaroon(path)
require.NoError(t, err)
gotRaw, err := hex.DecodeString(got)
require.NoError(t, err)
roundTrip := &macaroon.Macaroon{}
require.NoError(t, roundTrip.UnmarshalBinary(gotRaw))
})
t.Run("arbitrary non-macaroon file is rejected", func(t *testing.T) {
path := filepath.Join(dir, "id_rsa")
require.NoError(t, os.WriteFile(path, []byte("-----BEGIN OPENSSH PRIVATE KEY-----\nsecret\n"), 0600))
_, err := readAndCanonicalizeLNDMacaroon(path)
require.Error(t, err)
})
t.Run("missing file is rejected", func(t *testing.T) {
_, err := readAndCanonicalizeLNDMacaroon(filepath.Join(dir, "does-not-exist"))
require.Error(t, err)
})
}
func TestValidateCLNLightningDir(t *testing.T) {
certPEM, keyPEM := generateTestCert(t)
writeCLNDir := func(t *testing.T, dir string) {
t.Helper()
require.NoError(t, os.WriteFile(filepath.Join(dir, "ca.pem"), certPEM, 0600))
require.NoError(t, os.WriteFile(filepath.Join(dir, "client.pem"), certPEM, 0600))
require.NoError(t, os.WriteFile(filepath.Join(dir, "client-key.pem"), keyPEM, 0600))
}
t.Run("valid directory", func(t *testing.T) {
dir := t.TempDir()
writeCLNDir(t, dir)
require.NoError(t, validateCLNLightningDir(dir, false))
})
t.Run("valid directory with hold subdirectory", func(t *testing.T) {
dir := t.TempDir()
writeCLNDir(t, dir)
holdDir := filepath.Join(dir, "hold")
require.NoError(t, os.Mkdir(holdDir, 0700))
writeCLNDir(t, holdDir)
require.NoError(t, validateCLNLightningDir(dir, true))
})
t.Run("hold requested but subdirectory missing", func(t *testing.T) {
dir := t.TempDir()
writeCLNDir(t, dir)
require.Error(t, validateCLNLightningDir(dir, true))
})
t.Run("arbitrary directory is rejected", func(t *testing.T) {
require.Error(t, validateCLNLightningDir(t.TempDir(), false))
})
}

View file

@ -4,19 +4,29 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"strconv"
"strings"
"time"
"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/transactions"
"github.com/sirupsen/logrus"
)
func (api *api) CreateInvoice(ctx context.Context, amount uint64, description string) (*MakeInvoiceResponse, error) {
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")
func (api *api) CreateInvoice(ctx context.Context, amountMsat uint64, description string, toAppId *uint) (*MakeInvoiceResponse, error) {
lnClient := api.svc.GetLNClient()
if lnClient == nil {
return nil, ErrLNClientNotStarted
}
transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amount, description, "", 0, nil, api.svc.GetLNClient(), nil, nil, nil)
if toAppId != nil && api.appsSvc.GetAppById(*toAppId) == nil {
return nil, errors.New("app does not exist")
}
transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amountMsat, description, "", 0, nil, lnClient, toAppId, nil, nil)
if err != nil {
return nil, err
}
@ -24,19 +34,65 @@ func (api *api) CreateInvoice(ctx context.Context, amount uint64, description st
}
func (api *api) LookupInvoice(ctx context.Context, paymentHash string) (*LookupInvoiceResponse, error) {
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")
lnClient := api.svc.GetLNClient()
if lnClient == nil {
return nil, ErrLNClientNotStarted
}
transaction, err := api.svc.GetTransactionsService().LookupTransaction(ctx, paymentHash, nil, api.svc.GetLNClient(), nil)
transaction, err := api.svc.GetTransactionsService().LookupTransaction(ctx, paymentHash, nil, lnClient, nil)
if err != nil {
return nil, err
}
return toApiTransaction(transaction), nil
}
func (api *api) ListTransactions(ctx context.Context, appId *uint, limit uint64, offset uint64) (*ListTransactionsResponse, error) {
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")
func (api *api) SetTransactionUserLabels(ctx context.Context, id uint, labels map[string]string) error {
return api.svc.GetTransactionsService().SetTransactionUserLabels(ctx, id, labels)
}
// ParseListTransactionsFilters parses transaction filter query parameters
// shared by the HTTP and Wails transports. Invalid values return an error.
func ParseListTransactionsFilters(query url.Values) (ListTransactionsFilters, error) {
filters := ListTransactionsFilters{}
if transactionType := query.Get("type"); transactionType != "" {
if transactionType != constants.TRANSACTION_TYPE_INCOMING && transactionType != constants.TRANSACTION_TYPE_OUTGOING {
return filters, fmt.Errorf("invalid type: %s", transactionType)
}
filters.Type = &transactionType
}
if minAmountSatParam := query.Get("minAmountSat"); minAmountSatParam != "" {
minAmountSat, err := strconv.ParseUint(minAmountSatParam, 10, 64)
if err != nil || minAmountSat == 0 {
return filters, fmt.Errorf("invalid minAmountSat: %s", minAmountSatParam)
}
const msatPerSat = uint64(1000)
if minAmountSat > ^uint64(0)/msatPerSat {
return filters, fmt.Errorf("minAmountSat is too large")
}
minAmountMsat := minAmountSat * msatPerSat
filters.MinAmountMsat = &minAmountMsat
}
if hideFailedParam := query.Get("hideFailed"); hideFailedParam != "" {
hideFailed, err := strconv.ParseBool(hideFailedParam)
if err != nil {
return filters, fmt.Errorf("invalid hideFailed: %s", hideFailedParam)
}
filters.HideFailed = hideFailed
}
filters.SearchTerm = strings.TrimSpace(query.Get("search"))
return filters, nil
}
func (api *api) ListTransactions(ctx context.Context, appId *uint, limit uint64, offset uint64, filters ListTransactionsFilters) (*ListTransactionsResponse, error) {
lnClient := api.svc.GetLNClient()
if lnClient == nil {
return nil, ErrLNClientNotStarted
}
forceFilterByAppId := false
@ -44,13 +100,18 @@ func (api *api) ListTransactions(ctx context.Context, appId *uint, limit uint64,
forceFilterByAppId = true
}
transactions, totalCount, err := api.svc.GetTransactionsService().ListTransactions(ctx, 0, 0, limit, offset, true, false, nil, api.svc.GetLNClient(), appId, forceFilterByAppId)
dbTransactions, totalCount, err := api.svc.GetTransactionsService().ListTransactions(ctx, 0, 0, limit, offset, true, false, lnClient, appId, forceFilterByAppId, &transactions.ListTransactionsFilters{
Type: filters.Type,
MinAmountMsat: filters.MinAmountMsat,
HideFailed: filters.HideFailed,
SearchTerm: filters.SearchTerm,
})
if err != nil {
return nil, err
}
apiTransactions := []Transaction{}
for _, transaction := range transactions {
for _, transaction := range dbTransactions {
apiTransactions = append(apiTransactions, *toApiTransaction(&transaction))
}
@ -60,11 +121,13 @@ func (api *api) ListTransactions(ctx context.Context, appId *uint, limit uint64,
}, nil
}
func (api *api) SendPayment(ctx context.Context, invoice string, amountMsat *uint64, metadata map[string]interface{}) (*SendPaymentResponse, error) {
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")
func (api *api) SendPayment(ctx context.Context, invoice string, amountMsat *uint64, metadata map[string]interface{}, appId *uint) (*SendPaymentResponse, error) {
lnClient := api.svc.GetLNClient()
if lnClient == nil {
return nil, ErrLNClientNotStarted
}
transaction, err := api.svc.GetTransactionsService().SendPaymentSync(invoice, amountMsat, metadata, api.svc.GetLNClient(), nil, nil)
transaction, err := api.svc.GetTransactionsService().SendPaymentSync(invoice, amountMsat, metadata, lnClient, appId, nil)
if err != nil {
return nil, err
}
@ -108,6 +171,7 @@ func toApiTransaction(transaction *transactions.Transaction) *Transaction {
}
return &Transaction{
ID: transaction.ID,
Type: transaction.Type,
State: strings.ToLower(transaction.State),
Invoice: transaction.PaymentRequest,
@ -116,8 +180,12 @@ func toApiTransaction(transaction *transactions.Transaction) *Transaction {
Preimage: preimage,
PaymentHash: transaction.PaymentHash,
Amount: transaction.AmountMsat,
AmountSat: transaction.AmountMsat / 1000,
AmountMsat: transaction.AmountMsat,
AppId: transaction.AppId,
FeesPaid: transaction.FeeMsat,
FeesPaidSat: transaction.FeeMsat / 1000,
FeesPaidMsat: transaction.FeeMsat,
UpdatedAt: updatedAt,
CreatedAt: createdAt,
SettledAt: settledAt,
@ -127,9 +195,10 @@ func toApiTransaction(transaction *transactions.Transaction) *Transaction {
}
}
func (api *api) Transfer(ctx context.Context, fromAppId *uint, toAppId *uint, amountMsat uint64) error {
if api.svc.GetLNClient() == nil {
return errors.New("LNClient not started")
func (api *api) Transfer(ctx context.Context, fromAppId *uint, toAppId *uint, amountMsat uint64, description string) error {
lnClient := api.svc.GetLNClient()
if lnClient == nil {
return ErrLNClientNotStarted
}
for _, appId := range []*uint{fromAppId, toAppId} {
@ -144,13 +213,18 @@ func (api *api) Transfer(ctx context.Context, fromAppId *uint, toAppId *uint, am
}
}
transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amountMsat, "transfer", "", 0, nil, api.svc.GetLNClient(), toAppId, nil, nil)
// default to "transfer"
if description == "" {
description = "transfer"
}
transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amountMsat, description, "", 0, nil, lnClient, toAppId, nil, nil)
if err != nil {
return err
}
_, err = api.svc.GetTransactionsService().SendPaymentSync(transaction.PaymentRequest, nil, nil, api.svc.GetLNClient(), fromAppId, nil)
_, err = api.svc.GetTransactionsService().SendPaymentSync(transaction.PaymentRequest, nil, nil, lnClient, fromAppId, nil)
return err
}
@ -169,6 +243,7 @@ func toApiBoostagram(boostagram *transactions.Boostagram) *Boostagram {
SenderName: boostagram.SenderName,
Time: boostagram.Time,
Action: boostagram.Action,
ValueSatTotal: boostagram.ValueMsatTotal / 1000,
ValueMsatTotal: boostagram.ValueMsatTotal,
}
}

98
api/transactions_test.go Normal file
View file

@ -0,0 +1,98 @@
package api
import (
"context"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/getAlby/hub/tests"
"github.com/getAlby/hub/tests/mocks"
"github.com/getAlby/hub/transactions"
)
func TestCreateInvoice_ToApp(t *testing.T) {
ctx := context.TODO()
testSvc, err := tests.CreateTestService(t)
require.NoError(t, err)
defer testSvc.Remove()
app, _, err := tests.CreateApp(testSvc)
require.NoError(t, err)
svc := mocks.NewMockService(t)
svc.On("GetLNClient").Return(testSvc.LNClient)
svc.On("GetTransactionsService").Return(transactions.NewTransactionsService(testSvc.DB, testSvc.EventPublisher))
theAPI := &api{
appsSvc: testSvc.AppsService,
svc: svc,
}
transaction, err := theAPI.CreateInvoice(ctx, 1000, "Hello world", &app.ID)
require.NoError(t, err)
require.NotNil(t, transaction.AppId)
assert.Equal(t, app.ID, *transaction.AppId)
}
func TestCreateInvoice_ToAppNotFound(t *testing.T) {
ctx := context.TODO()
testSvc, err := tests.CreateTestService(t)
require.NoError(t, err)
defer testSvc.Remove()
svc := mocks.NewMockService(t)
svc.On("GetLNClient").Return(testSvc.LNClient)
theAPI := &api{
appsSvc: testSvc.AppsService,
svc: svc,
}
missingAppId := uint(999)
transaction, err := theAPI.CreateInvoice(ctx, 1000, "Hello world", &missingAppId)
assert.Nil(t, transaction)
require.Error(t, err)
assert.Equal(t, "app does not exist", err.Error())
}
func TestParseListTransactionsFilters(t *testing.T) {
minAmountMsat := uint64(1000_000)
outgoing := "outgoing"
filters, err := ParseListTransactionsFilters(url.Values{
"type": {"outgoing"},
"minAmountSat": {"1000"},
"hideFailed": {"true"},
"search": {" coffee "},
})
require.NoError(t, err)
assert.Equal(t, ListTransactionsFilters{
Type: &outgoing,
MinAmountMsat: &minAmountMsat,
HideFailed: true,
SearchTerm: "coffee",
}, filters)
filters, err = ParseListTransactionsFilters(url.Values{})
require.NoError(t, err)
assert.Equal(t, ListTransactionsFilters{}, filters)
for _, invalidQuery := range []url.Values{
{"type": {"sideways"}},
{"minAmountSat": {"abc"}},
{"minAmountSat": {"-1"}},
{"minAmountSat": {"0"}},
{"minAmountSat": {"18446744073709551615"}},
{"hideFailed": {"maybe"}},
} {
_, err = ParseListTransactionsFilters(invalidQuery)
assert.Error(t, err, "query: %v", invalidQuery)
}
}

View file

@ -9,13 +9,13 @@ import (
"strings"
"time"
"github.com/getAlby/go-nostr"
"github.com/getAlby/hub/config"
"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/service/keys"
"github.com/nbd-wtf/go-nostr"
"gorm.io/datatypes"
"gorm.io/gorm"
)
@ -58,9 +58,11 @@ func (svc *appsService) CreateApp(name string, pubkey string, maxAmountSat uint6
backendType, _ := svc.cfg.Get("LNBackendType", "")
if backendType != config.LDKBackendType &&
backendType != config.LNDBackendType &&
backendType != config.PhoenixBackendType {
backendType != config.PhoenixBackendType &&
backendType != config.BarkBackendType &&
backendType != config.CLNBackendType {
return nil, "", fmt.Errorf(
"sub-wallets are currently not supported on your node backend. Try LDK or LND")
"sub-wallets are currently not supported on your node backend. Try LDK, LND, PHOENIX, BARK, or CLN")
}
}
@ -184,11 +186,17 @@ func (svc *appsService) DeleteApp(app *db.App) error {
if err != nil {
return err
}
walletPubkey := ""
if app.WalletPubkey != nil {
// only exists for non-legacy apps
walletPubkey = *app.WalletPubkey
}
svc.eventPublisher.Publish(&events.Event{
Event: "nwc_app_deleted",
Properties: map[string]interface{}{
"name": app.Name,
"id": app.ID,
"name": app.Name,
"id": app.ID,
"walletPubkey": walletPubkey,
},
})
return nil

View file

@ -54,5 +54,5 @@ func TestHandleCreateApp_IsolatedUnsupportedBackendType(t *testing.T) {
assert.Nil(t, app)
assert.Equal(t, "", secretKey)
require.Error(t, err)
assert.Equal(t, "sub-wallets are currently not supported on your node backend. Try LDK or LND", err.Error())
assert.Equal(t, "sub-wallets are currently not supported on your node backend. Try LDK, LND, PHOENIX, BARK, or CLN", err.Error())
}

View file

@ -2,9 +2,7 @@ package main
import (
"flag"
"fmt"
"os"
"slices"
"strconv"
"github.com/sirupsen/logrus"
@ -14,18 +12,6 @@ import (
"github.com/getAlby/hub/logger"
)
var expectedTables = []string{
"apps",
"app_permissions",
"request_events",
"response_events",
"transactions",
"swaps",
"user_configs",
"migrations",
"forwards",
}
func main() {
var fromDSN, toDSN string
@ -64,54 +50,29 @@ func main() {
}
defer stopDB(toDB)
// Migrations are applied to both the source and the target DB, so
// schemas should be equal at this point.
err = checkSchema(fromDB)
if err != nil {
logger.Logger.WithError(err).Error("database schema check failed; the migration tool may be outdated")
os.Exit(1)
}
// Check if VSS is enabled in the source database
var vssConfig db.UserConfig
result := fromDB.Where("key = ?", "LdkVssEnabled").First(&vssConfig)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
logger.Logger.Error("LdkVssEnabled config not found in source DB. Migration will not proceed.")
} else {
logger.Logger.WithError(result.Error).Error("failed to query LdkVssEnabled config from source DB")
// When migrating to Postgres (e.g. a cloud deployment) the node data must
// be stored in VSS, since only the database is migrated by this tool.
if toDB.Dialector.Name() == "postgres" {
var vssConfig db.UserConfig
result := fromDB.Where("key = ?", "LdkVssEnabled").First(&vssConfig)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
logger.Logger.Error("LdkVssEnabled config not found in source DB. Migration will not proceed.")
} else {
logger.Logger.WithError(result.Error).Error("failed to query LdkVssEnabled config from source DB")
}
os.Exit(1)
}
os.Exit(1)
}
if vssConfig.Value != "true" {
logger.Logger.Error("VSS is not enabled in the source DB (LdkVssEnabled is not 'true'). Migration will not proceed.")
os.Exit(1)
}
logger.Logger.Info("LdkVssEnabled check passed.")
// NOTE: we assume that excess request events have already been cleaned up due to the background task
// and only a maximum of ~1000 remain.
logger.Logger.Info("Deleting orphaned request events.")
err = fromDB.Exec("DELETE FROM request_events WHERE app_id NOT IN (SELECT id FROM apps);").Error
if err != nil {
logger.Logger.WithError(err).Error("failed to delete orphaned request events")
os.Exit(1)
}
// NOTE: we assume that excess response events have already been cleaned up due to the background task
// and only a maximum of ~1000 remain.
logger.Logger.Info("Deleting orphaned response events.")
err = fromDB.Exec("DELETE FROM response_events WHERE request_id NOT IN (SELECT id FROM request_events);").Error
if err != nil {
logger.Logger.WithError(err).Error("failed to delete orphaned response events")
os.Exit(1)
if vssConfig.Value != "true" {
logger.Logger.Error("VSS is not enabled in the source DB (LdkVssEnabled is not 'true'). Migration will not proceed.")
os.Exit(1)
}
logger.Logger.Info("LdkVssEnabled check passed.")
}
logger.Logger.Info("migrating...")
err = migrateDB(fromDB, toDB)
err = db.MigrateDB(fromDB, toDB)
if err != nil {
logger.Logger.WithError(err).Error("failed to migrate database")
os.Exit(1)
@ -119,175 +80,3 @@ func main() {
logger.Logger.Info("migration complete")
}
func migrateDB(from, to *gorm.DB) error {
tx := to.Begin()
defer tx.Rollback()
if err := tx.Error; err != nil {
return fmt.Errorf("failed to start transaction: %w", err)
}
// Table migration order matters: referenced tables must be migrated
// before referencing tables.
logger.Logger.Info("migrating apps...")
if err := migrateTable[db.App](from, tx); err != nil {
return fmt.Errorf("failed to migrate apps: %w", err)
}
logger.Logger.Info("migrating app_permissions...")
if err := migrateTable[db.AppPermission](from, tx); err != nil {
return fmt.Errorf("failed to migrate app_permissions: %w", err)
}
logger.Logger.Info("migrating request_events...")
if err := migrateTable[db.RequestEvent](from, tx); err != nil {
return fmt.Errorf("failed to migrate request_events: %w", err)
}
logger.Logger.Info("migrating response_events...")
if err := migrateTable[db.ResponseEvent](from, tx); err != nil {
return fmt.Errorf("failed to migrate response_events: %w", err)
}
logger.Logger.Info("migrating transactions...")
if err := migrateTable[db.Transaction](from, tx); err != nil {
return fmt.Errorf("failed to migrate transactions: %w", err)
}
logger.Logger.Info("migrating user_configs...")
if err := migrateTable[db.UserConfig](from, tx); err != nil {
return fmt.Errorf("failed to migrate user_configs: %w", err)
}
if to.Dialector.Name() == "postgres" {
logger.Logger.Info("resetting sequences...")
if err := resetSequences(tx); err != nil {
return fmt.Errorf("failed to reset sequences: %w", err)
}
}
tx.Commit()
if err := tx.Error; err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
return nil
}
func migrateTable[T any](from, to *gorm.DB) error {
var data []T
if err := from.Find(&data).Error; err != nil {
return fmt.Errorf("failed to fetch data: %w", err)
}
if len(data) == 0 {
return nil
}
// to avoid "failed to migrate transactions: failed to insert data: extended protocol limited to 65535 parameters"
// see https://stackoverflow.com/questions/77372430/extended-protocol-limited-to-65535-parameters-golang-gorm
// max statements is 65535
// but it's the number of records * columns
// to be safe, using a lower value of 1000.
// this will fail if any table has more than 65 columns, which I doubt we will have
max := 1000
for i := 0; i < len(data); i += max {
j := min(i+max, len(data))
if err := to.Create(data[i:j]).Error; err != nil {
return fmt.Errorf("failed to insert data: %w", err)
}
}
return nil
}
func checkSchema(db *gorm.DB) error {
tables, err := listTables(db)
if err != nil {
return fmt.Errorf("failed to list database tables: %w", err)
}
for _, table := range expectedTables {
if !slices.Contains(tables, table) {
return fmt.Errorf("table missing from the database: %q", table)
}
}
for _, table := range tables {
if !slices.Contains(expectedTables, table) {
return fmt.Errorf("unexpected table found in the database: %q", table)
}
}
return nil
}
func listTables(db *gorm.DB) ([]string, error) {
var query string
switch db.Dialector.Name() {
case "sqlite":
query = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';"
case "postgres":
query = "SELECT tablename FROM pg_tables WHERE schemaname = 'public';"
default:
return nil, fmt.Errorf("unsupported database: %q", db.Dialector.Name())
}
rows, err := db.Raw(query).Rows()
if err != nil {
return nil, fmt.Errorf("failed to query table names: %w", err)
}
defer func() {
if err := rows.Close(); err != nil {
logger.Logger.WithError(err).Error("failed to close rows")
}
}()
var tables []string
for rows.Next() {
var table string
if err := rows.Scan(&table); err != nil {
return nil, fmt.Errorf("failed to scan table name: %w", err)
}
tables = append(tables, table)
}
return tables, nil
}
func resetSequences(db *gorm.DB) error {
type resetReq struct {
table string
seq string
}
resetReqs := []resetReq{
{"apps", "apps_2_id_seq"},
{"app_permissions", "app_permissions_2_id_seq"},
{"request_events", "request_events_id_seq"},
{"response_events", "response_events_id_seq"},
{"transactions", "transactions_id_seq"},
{"user_configs", "user_configs_id_seq"},
}
for _, req := range resetReqs {
if err := resetPostgresSequence(db, req.table, req.seq); err != nil {
return fmt.Errorf("failed to reset sequence %q for %q: %w", req.seq, req.table, err)
}
}
return nil
}
func resetPostgresSequence(db *gorm.DB, table string, seq string) error {
query := fmt.Sprintf("SELECT setval('%s', (SELECT MAX(id) FROM %s));", seq, table)
if err := db.Exec(query).Error; err != nil {
return fmt.Errorf("failed to execute setval(): %w", err)
}
return nil
}

View file

@ -30,40 +30,6 @@ func (e *testEnvironment) cleanup(t *testing.T) {
require.NoError(t, err)
}
func TestSchemaCheck(t *testing.T) {
type testCase struct {
name string
uri string
}
tc := []testCase{
{
name: "schema check sqlite",
uri: getTestSqliteURI(0),
},
}
if pgUri := getTestPostgresURI(); pgUri != "" {
tc = append(tc, testCase{
name: "schema check postgres",
uri: pgUri,
})
}
logger.Init(strconv.Itoa(int(logrus.DebugLevel)))
for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
dbConn, err := test_db.NewDBWithURI(t, tt.uri)
require.NoError(t, err)
defer db.Stop(dbConn)
err = checkSchema(dbConn)
require.NoError(t, err)
})
}
}
func TestMigrate(t *testing.T) {
type testCase struct {
name string
@ -104,8 +70,17 @@ func TestMigrate(t *testing.T) {
require.NoError(t, err)
defer env.cleanup(t)
err = migrateDB(env.source, env.dest)
err = db.MigrateDB(env.source, env.dest)
require.NoError(t, err)
requireCount[db.App](t, env.dest, 2)
requireCount[db.AppPermission](t, env.dest, 2)
requireCount[db.RequestEvent](t, env.dest, 1)
requireCount[db.ResponseEvent](t, env.dest, 1)
requireCount[db.Transaction](t, env.dest, 1)
requireCount[db.Swap](t, env.dest, 1)
requireCount[db.Forward](t, env.dest, 1)
requireCount[db.UserConfig](t, env.dest, 1)
})
}
}
@ -148,7 +123,7 @@ func insertMockData(t *testing.T, tx *gorm.DB) {
userCfg1 := &db.UserConfig{
Key: "Relay",
Value: "wss://relay.getalby.com/v1",
Value: "wss://relay.getalby.com",
Encrypted: false,
CreatedAt: baseTime,
UpdatedAt: baseTime,
@ -200,6 +175,83 @@ func insertMockData(t *testing.T, tx *gorm.DB) {
UpdatedAt: baseTime,
}
create(t, tx, app2Perm)
requestEvent1 := &db.RequestEvent{
AppId: &app1.ID,
NostrId: "a35a1ca6d1a06e08a509f2c8fe3edb2ba10811d030e2f6f3239e9f21203ac954",
ContentData: "{}",
Method: "pay_invoice",
State: "executed",
CreatedAt: baseTime,
UpdatedAt: baseTime,
}
create(t, tx, requestEvent1)
responseEvent1 := &db.ResponseEvent{
NostrId: "e30d55d0e4f0d5391a1a1379f1d8b7d38ad02b3554b06ca993aa8790a3153f61",
RequestId: requestEvent1.ID,
State: "confirmed",
RepliedAt: baseTime,
CreatedAt: baseTime,
UpdatedAt: baseTime,
}
create(t, tx, responseEvent1)
transaction1 := &db.Transaction{
AppId: &app1.ID,
RequestEventId: &requestEvent1.ID,
Type: "outgoing",
State: "settled",
AmountMsat: 21000,
FeeMsat: 1000,
PaymentRequest: "lnbc210n1invoice",
PaymentHash: "13d9764a54269fa4d5f4e7c410f4ffdbc839bbeaa2fcbb96343ca502f0c86e34",
Description: "test transaction",
Preimage: ptr("2c1ee1b464b1a1a147debe0ac0c8ce4b615f9bfa64d12a25c1c4d10ea45a5b02"),
CreatedAt: baseTime,
UpdatedAt: baseTime,
SettledAt: &baseTime,
Metadata: datatypes.JSON("{}"),
Boostagram: datatypes.JSON("{}"),
}
create(t, tx, transaction1)
swap1 := &db.Swap{
SwapId: "swap1",
Type: "out",
State: "success",
Invoice: "lnbc210n1swapinvoice",
SendAmountSat: 21000,
ReceiveAmountSat: 20000,
Preimage: "35a3f1a7a06a41b9ba3a1b1a8ff852e5085b3b593f8ba4677a35a1ca6d1a06e0",
PaymentHash: "e6b1a1379f1d8b7d38ad02b3554b06ca993aa8790a3153f61e30d55d0e4f0d53",
DestinationAddress: "bc1qtest",
LockupAddress: "bc1qlockup",
LockupTxId: "lockuptx",
ClaimTxId: "claimtx",
AutoSwap: false,
TimeoutBlockHeight: 900000,
BoltzPubkey: "02d1a06e08a509f2c8fe3edb2ba10811d030e2f6f3239e9f21203ac954a35a1c",
SwapTree: datatypes.JSON("{}"),
CreatedAt: baseTime,
UpdatedAt: baseTime,
}
create(t, tx, swap1)
forward1 := &db.Forward{
OutboundAmountForwardedMsat: 1000000,
TotalFeeEarnedMsat: 1000,
CreatedAt: baseTime,
UpdatedAt: baseTime,
}
create(t, tx, forward1)
}
func requireCount[T any](t *testing.T, tx *gorm.DB, expected int64) {
var count int64
var model T
require.NoError(t, tx.Model(&model).Count(&count).Error)
require.Equal(t, expected, count)
}
func create[T any](t *testing.T, tx *gorm.DB, v T) *gorm.DB {

View file

@ -7,7 +7,6 @@ import (
"errors"
"fmt"
"os"
"path"
"strings"
"sync"
@ -20,11 +19,12 @@ import (
)
type config struct {
Env *AppConfig
db *gorm.DB
cache map[string]map[string]string // key -> encryptionKeyHash -> value
cacheMutex sync.Mutex
jwtSecret string
Env *AppConfig
db *gorm.DB
cache map[string]map[string]string // key -> encryptionKeyHash -> value
cacheMutex sync.Mutex
jwtSecret string
jwtSecretMutex sync.Mutex
}
const (
@ -112,36 +112,66 @@ func (cfg *config) init(env *AppConfig) error {
}
}
// CLN specific to support env variables
if cfg.Env.CLNAddress != "" {
err := cfg.SetUpdate("CLNAddress", cfg.Env.CLNAddress, "")
if err != nil {
return err
}
}
if cfg.Env.CLNLightningDir != "" {
err := cfg.SetUpdate("CLNLightningDir", cfg.Env.CLNLightningDir, "")
if err != nil {
return err
}
}
if cfg.Env.CLNAddressHold != "" {
err := cfg.SetUpdate("CLNAddressHold", cfg.Env.CLNAddressHold, "")
if err != nil {
return err
}
}
return nil
}
func (cfg *config) SetupCompleted() bool {
// TODO: remove hasLdkDir check after 2025/01/01
// to give time for users to update to 1.6.0+
nodeLastStartTime, _ := cfg.Get("NodeLastStartTime", "")
ldkDir, err := os.Stat(path.Join(cfg.GetEnv().Workdir, "ldk"))
hasLdkDir := err == nil && ldkDir != nil && ldkDir.IsDir()
func (cfg *config) SetupCompleted() (bool, error) {
nodeLastStartTime, err := cfg.Get("NodeLastStartTime", "")
if err != nil {
return false, err
}
logger.Logger.WithFields(logrus.Fields{
"has_ldk_dir": hasLdkDir,
"has_node_last_start_time": nodeLastStartTime != "",
}).Debug("Checking if setup is completed")
return nodeLastStartTime != "" || hasLdkDir
return nodeLastStartTime != "", nil
}
func (cfg *config) GetJWTSecret() (string, error) {
if cfg.jwtSecret == "" {
cfg.jwtSecretMutex.Lock()
jwtSecret := cfg.jwtSecret
cfg.jwtSecretMutex.Unlock()
if jwtSecret == "" {
return "", errors.New("config not unlocked")
}
return cfg.jwtSecret, nil
return jwtSecret, nil
}
func (cfg *config) Unlock(encryptionKey string) error {
// Decrypt and store the JWT secret in memory
func (cfg *config) LoadJWTSecret(encryptionKey string) error {
if !cfg.CheckUnlockPassword(encryptionKey) {
return errors.New("incorrect password")
}
cfg.jwtSecretMutex.Lock()
if cfg.jwtSecret != "" {
cfg.jwtSecretMutex.Unlock()
return nil
}
cfg.jwtSecretMutex.Unlock()
// TODO: remove encryptedJwtSecret check after 2027-01-01
// - all hubs should have updated to use an encrypted JWT secret by then
encryptedJwtSecret, err := cfg.Get("JWTSecret", "")
@ -168,7 +198,9 @@ func (cfg *config) Unlock(encryptionKey string) error {
return err
}
}
cfg.jwtSecretMutex.Lock()
cfg.jwtSecret = jwtSecret
cfg.jwtSecretMutex.Unlock()
return nil
}
@ -353,7 +385,9 @@ func (cfg *config) ChangeUnlockPassword(currentUnlockPassword string, newUnlockP
}
// JWT secret will be set on config unlock (required after password change)
cfg.jwtSecretMutex.Lock()
cfg.jwtSecret = ""
cfg.jwtSecretMutex.Unlock()
return nil
}
@ -374,7 +408,18 @@ func (cfg *config) SetAutoUnlockPassword(unlockPassword string) error {
func (cfg *config) CheckUnlockPassword(encryptionKey string) bool {
decryptedValue, err := cfg.Get("UnlockPasswordCheck", encryptionKey)
return err == nil && (decryptedValue == "" || decryptedValue == unlockPasswordCheck)
// require a non-empty match so an absent or empty canary always fails
return err == nil && decryptedValue != "" && decryptedValue == unlockPasswordCheck
}
func (cfg *config) IsUnlockPasswordCheckSet() (bool, error) {
// Read the raw value with an empty encryption key so we can detect the
// presence of the canary row without needing the (possibly wrong) password.
value, err := cfg.Get("UnlockPasswordCheck", "")
if err != nil {
return false, fmt.Errorf("read unlock password check: %w", err)
}
return value != "", nil
}
func (cfg *config) SaveUnlockPasswordCheck(encryptionKey string) error {

View file

@ -56,8 +56,6 @@ func TestCheckUnlockPasswordCache(t *testing.T) {
Workdir: ".test",
}, db)
require.NoError(t, err)
err = cfg.ChangeUnlockPassword("", unlockPassword)
require.NoError(t, err)
err = cfg.SaveUnlockPasswordCheck(unlockPassword)
require.NoError(t, err)

View file

@ -5,6 +5,8 @@ const (
LDKBackendType = "LDK"
PhoenixBackendType = "PHOENIX"
CashuBackendType = "CASHU"
CLNBackendType = "CLN"
BarkBackendType = "BARK"
)
const (
@ -16,7 +18,7 @@ const (
)
type AppConfig struct {
Relay string `envconfig:"RELAY" default:"wss://relay.getalby.com/v1"`
Relay string `envconfig:"RELAY" default:"wss://relay.getalby.com,wss://relay2.getalby.com"`
LNBackendType string `envconfig:"LN_BACKEND_TYPE"`
LNDAddress string `envconfig:"LND_ADDRESS"`
LNDCertFile string `envconfig:"LND_CERT_FILE"`
@ -34,7 +36,9 @@ type AppConfig struct {
LDKLogLevel string `envconfig:"LDK_LOG_LEVEL" default:"3"`
LDKMaxChannelSaturationPowerOfHalf uint8 `envconfig:"LDK_MAX_CHANNEL_SATURATION" default:"2"`
LDKMaxPathCount uint8 `envconfig:"LDK_MAX_PATH_COUNT" default:"5"`
LDKChannelMonitorWarningSizeBytes uint64 `envconfig:"LDK_CHANNEL_MONITOR_WARNING_SIZE_BYTES" default:"5000000"`
LDKVssUrl string `envconfig:"LDK_VSS_URL" default:"https://vss.getalbypro.com/vss"`
LDKLiquiditySourceLsps2 string `envconfig:"LDK_LSPS2_ADDRESSES"`
LDKListeningAddresses string `envconfig:"LDK_LISTENING_ADDRESSES" default:"[::]:9735"`
LDKAnnouncementAddresses string `envconfig:"LDK_ANNOUNCEMENT_ADDRESSES"`
LDKTransientNetworkGraph bool `envconfig:"LDK_TRANSIENT_NETWORK_GRAPH" default:"false"`
@ -57,6 +61,14 @@ type AppConfig struct {
AutoUnlockPassword string `envconfig:"AUTO_UNLOCK_PASSWORD"`
LogDBQueries bool `envconfig:"LOG_DB_QUERIES" default:"false"`
BoltzApi string `envconfig:"BOLTZ_API" default:"https://api.boltz.exchange"`
HideUpdateBanner bool `envconfig:"HIDE_UPDATE_BANNER" default:"false"`
CLNAddress string `envconfig:"CLN_ADDRESS"`
CLNLightningDir string `envconfig:"CLN_LIGHTNING_DIR"`
CLNAddressHold string `envconfig:"CLN_ADDRESS_HOLD"`
BarkServer string `envconfig:"BARK_SERVER" default:"https://ark.second.tech"`
BarkEsploraServer string `envconfig:"BARK_ESPLORA_SERVER" default:"https://mempool.second.tech/api"`
BarkServerAccessToken string `envconfig:"BARK_SERVER_ACCESS_TOKEN"`
BarkLogLevel string `envconfig:"BARK_LOG_LEVEL" default:"3"`
}
func (c *AppConfig) IsDefaultClientId() bool {
@ -72,20 +84,21 @@ func (c *AppConfig) GetBaseFrontendUrl() string {
}
type Config interface {
Unlock(encryptionKey string) error
Get(key string, encryptionKey string) (string, error)
SetIgnore(key string, value string, encryptionKey string) error
SetUpdate(key string, value string, encryptionKey string) error
LoadJWTSecret(encryptionKey string) error
GetJWTSecret() (string, error)
GetRelayUrls() []string
GetNetwork() string
GetMempoolUrl() string
GetEnv() *AppConfig
CheckUnlockPassword(password string) bool
IsUnlockPasswordCheckSet() (bool, error)
ChangeUnlockPassword(currentUnlockPassword string, newUnlockPassword string) error
SetAutoUnlockPassword(unlockPassword string) error
SaveUnlockPasswordCheck(encryptionKey string) error
SetupCompleted() bool
SetupCompleted() (bool, error)
GetCurrency() string
SetCurrency(value string) error
GetBitcoinDisplayFormat() string

View file

@ -16,8 +16,6 @@ func TestCheckUnlockPasswordCache_InvalidSecond(t *testing.T) {
require.NoError(t, err)
defer svc.Remove()
err = svc.Cfg.ChangeUnlockPassword("", unlockPassword)
require.NoError(t, err)
err = svc.Cfg.SaveUnlockPasswordCheck(unlockPassword)
require.NoError(t, err)
@ -35,8 +33,6 @@ func TestCheckUnlockPasswordCache_InvalidFirst(t *testing.T) {
require.NoError(t, err)
defer svc.Remove()
err = svc.Cfg.ChangeUnlockPassword("", unlockPassword)
require.NoError(t, err)
err = svc.Cfg.SaveUnlockPasswordCheck(unlockPassword)
require.NoError(t, err)
@ -58,8 +54,6 @@ func TestCheckUnlockPassword_ChangePassword(t *testing.T) {
require.NoError(t, err)
defer svc.Remove()
err = svc.Cfg.ChangeUnlockPassword("", unlockPassword)
require.NoError(t, err)
err = svc.Cfg.SaveUnlockPasswordCheck(unlockPassword)
require.NoError(t, err)
@ -81,6 +75,50 @@ func TestCheckUnlockPassword_ChangePassword(t *testing.T) {
assert.True(t, svc.Cfg.CheckUnlockPassword(newUnlockPassword))
}
func TestCheckUnlockPassword_MissingCanaryFailsClosed(t *testing.T) {
svc, err := tests.CreateTestService(t)
require.NoError(t, err)
defer svc.Remove()
// A fresh hub has not saved the unlock-password canary yet.
set, err := svc.Cfg.IsUnlockPasswordCheckSet()
require.NoError(t, err)
assert.False(t, set)
// Without the canary, no password may validate - including an empty one.
assert.False(t, svc.Cfg.CheckUnlockPassword(""))
assert.False(t, svc.Cfg.CheckUnlockPassword("any-password"))
// After the canary is saved, only the correct password validates.
err = svc.Cfg.SaveUnlockPasswordCheck("correct")
require.NoError(t, err)
set, err = svc.Cfg.IsUnlockPasswordCheckSet()
require.NoError(t, err)
assert.True(t, set)
assert.True(t, svc.Cfg.CheckUnlockPassword("correct"))
assert.False(t, svc.Cfg.CheckUnlockPassword("wrong"))
assert.False(t, svc.Cfg.CheckUnlockPassword(""))
}
func TestCheckUnlockPassword_NoPasswordHub(t *testing.T) {
svc, err := tests.CreateTestService(t)
require.NoError(t, err)
defer svc.Remove()
// A hub configured without an unlock password stores the canary unencrypted;
// the empty password must still validate after the fail-closed change.
err = svc.Cfg.SaveUnlockPasswordCheck("")
require.NoError(t, err)
set, err := svc.Cfg.IsUnlockPasswordCheckSet()
require.NoError(t, err)
assert.True(t, set)
assert.True(t, svc.Cfg.CheckUnlockPassword(""))
}
func TestSetIgnore_NoEncryptionKey(t *testing.T) {
svc, err := tests.CreateTestService(t)
require.NoError(t, err)
@ -211,7 +249,7 @@ func TestSetUpdate_EncryptionKeyToNoEncryptionKey(t *testing.T) {
assert.Equal(t, "value2", updatedValue)
}
func TestJWTSecret_GeneratedOnUnlock(t *testing.T) {
func TestJWTSecret_GeneratedOnLoad(t *testing.T) {
svc, err := tests.CreateTestService(t)
require.NoError(t, err)
defer svc.Remove()
@ -219,10 +257,10 @@ func TestJWTSecret_GeneratedOnUnlock(t *testing.T) {
cfg, err := config.NewConfig(&config.AppConfig{}, svc.DB)
require.NoError(t, err)
err = cfg.ChangeUnlockPassword("", "123")
err = cfg.SaveUnlockPasswordCheck("123")
require.NoError(t, err)
err = cfg.Unlock("123")
err = cfg.LoadJWTSecret("123")
require.NoError(t, err)
jwtSecret, err := cfg.GetJWTSecret()
@ -235,14 +273,32 @@ func TestJWTSecret_GeneratedOnUnlock(t *testing.T) {
require.NoError(t, err)
assert.NotEqual(t, encryptedSecret, decryptedSecret)
// unlock again without doing anything, ensure the same JWT secret is returned
err = cfg.Unlock("123")
// load again without doing anything, ensure the same JWT secret is returned
err = cfg.LoadJWTSecret("123")
require.NoError(t, err)
jwtSecret2, err := cfg.GetJWTSecret()
require.NoError(t, err)
assert.Equal(t, jwtSecret, jwtSecret2)
}
func TestJWTSecret_WrongPassword(t *testing.T) {
svc, err := tests.CreateTestService(t)
require.NoError(t, err)
defer svc.Remove()
cfg, err := config.NewConfig(&config.AppConfig{}, svc.DB)
require.NoError(t, err)
err = cfg.SaveUnlockPasswordCheck("123")
require.NoError(t, err)
err = cfg.LoadJWTSecret("123")
require.NoError(t, err)
err = cfg.LoadJWTSecret("wrong")
require.ErrorContains(t, err, "incorrect password")
}
func TestJWTSecret_ChangePassword(t *testing.T) {
svc, err := tests.CreateTestService(t)
require.NoError(t, err)
@ -251,23 +307,23 @@ func TestJWTSecret_ChangePassword(t *testing.T) {
cfg, err := config.NewConfig(&config.AppConfig{}, svc.DB)
require.NoError(t, err)
err = cfg.ChangeUnlockPassword("", "123")
err = cfg.SaveUnlockPasswordCheck("123")
require.NoError(t, err)
err = cfg.Unlock("123")
err = cfg.LoadJWTSecret("123")
require.NoError(t, err)
jwtSecret, err := cfg.GetJWTSecret()
require.NoError(t, err)
assert.NotEmpty(t, jwtSecret)
err = cfg.ChangeUnlockPassword("", "1234")
err = cfg.ChangeUnlockPassword("123", "1234")
require.NoError(t, err)
newJwtSecret, err := cfg.GetJWTSecret()
require.ErrorContains(t, err, "unlock")
err = cfg.Unlock("1234")
err = cfg.LoadJWTSecret("1234")
require.NoError(t, err)
// a new JWT secret must be generated after password change
@ -277,7 +333,7 @@ func TestJWTSecret_ChangePassword(t *testing.T) {
assert.NotEqual(t, newJwtSecret, jwtSecret)
}
func TestJWTSecret_ReplaceUnencryptedSecretOnUnlock(t *testing.T) {
func TestJWTSecret_ReplaceUnencryptedSecretOnLoad(t *testing.T) {
svc, err := tests.CreateTestService(t)
require.NoError(t, err)
defer svc.Remove()
@ -285,7 +341,7 @@ func TestJWTSecret_ReplaceUnencryptedSecretOnUnlock(t *testing.T) {
cfg, err := config.NewConfig(&config.AppConfig{}, svc.DB)
require.NoError(t, err)
err = cfg.ChangeUnlockPassword("", "123")
err = cfg.SaveUnlockPasswordCheck("123")
require.NoError(t, err)
// simulate a hub that had an unencrypted JWT secret
@ -293,7 +349,7 @@ func TestJWTSecret_ReplaceUnencryptedSecretOnUnlock(t *testing.T) {
err = svc.Cfg.SetUpdate("JWTSecret", oldJwtSecret, "")
require.NoError(t, err)
err = cfg.Unlock("123")
err = cfg.LoadJWTSecret("123")
require.NoError(t, err)
jwtSecret, err := cfg.GetJWTSecret()

View file

@ -76,6 +76,8 @@ const (
ENCRYPTION_TYPE_NIP44_V2 = "nip44_v2"
)
const METADATA_APPSTORE_APP_ID_KEY = "app_store_app_id"
const SUBWALLET_APPSTORE_APP_ID = "uncle-jim"
const (

235
db/db_migrate.go Normal file
View file

@ -0,0 +1,235 @@
package db
import (
"fmt"
"slices"
"gorm.io/gorm"
"github.com/getAlby/hub/logger"
)
var expectedTables = []string{
"apps",
"app_permissions",
"request_events",
"response_events",
"transactions",
"swaps",
"user_configs",
"migrations",
"forwards",
}
// MigrateDB copies all rows from one database to another. Both databases
// must have an up-to-date schema (they are checked against expectedTables).
// Orphaned request and response events are deleted from the source database
// before copying, as they would violate foreign key constraints in the
// destination database.
func MigrateDB(from, to *gorm.DB) error {
if err := checkSchema(from); err != nil {
return fmt.Errorf("source database schema check failed: %w", err)
}
if err := checkSchema(to); err != nil {
return fmt.Errorf("destination database schema check failed: %w", err)
}
// NOTE: we assume that excess request events have already been cleaned up due to the background task
// and only a maximum of ~1000 remain.
logger.Logger.Info("Deleting orphaned request events.")
err := from.Exec("DELETE FROM request_events WHERE app_id NOT IN (SELECT id FROM apps);").Error
if err != nil {
return fmt.Errorf("failed to delete orphaned request events: %w", err)
}
// NOTE: we assume that excess response events have already been cleaned up due to the background task
// and only a maximum of ~1000 remain.
logger.Logger.Info("Deleting orphaned response events.")
err = from.Exec("DELETE FROM response_events WHERE request_id NOT IN (SELECT id FROM request_events);").Error
if err != nil {
return fmt.Errorf("failed to delete orphaned response events: %w", err)
}
tx := to.Begin()
defer tx.Rollback()
if err := tx.Error; err != nil {
return fmt.Errorf("failed to start transaction: %w", err)
}
// Table migration order matters: referenced tables must be migrated
// before referencing tables.
logger.Logger.Info("migrating apps...")
if err := migrateTable[App](from, tx); err != nil {
return fmt.Errorf("failed to migrate apps: %w", err)
}
logger.Logger.Info("migrating app_permissions...")
if err := migrateTable[AppPermission](from, tx); err != nil {
return fmt.Errorf("failed to migrate app_permissions: %w", err)
}
logger.Logger.Info("migrating request_events...")
if err := migrateTable[RequestEvent](from, tx); err != nil {
return fmt.Errorf("failed to migrate request_events: %w", err)
}
logger.Logger.Info("migrating response_events...")
if err := migrateTable[ResponseEvent](from, tx); err != nil {
return fmt.Errorf("failed to migrate response_events: %w", err)
}
logger.Logger.Info("migrating transactions...")
if err := migrateTable[Transaction](from, tx); err != nil {
return fmt.Errorf("failed to migrate transactions: %w", err)
}
logger.Logger.Info("migrating swaps...")
if err := migrateTable[Swap](from, tx); err != nil {
return fmt.Errorf("failed to migrate swaps: %w", err)
}
logger.Logger.Info("migrating forwards...")
if err := migrateTable[Forward](from, tx); err != nil {
return fmt.Errorf("failed to migrate forwards: %w", err)
}
logger.Logger.Info("migrating user_configs...")
if err := migrateTable[UserConfig](from, tx); err != nil {
return fmt.Errorf("failed to migrate user_configs: %w", err)
}
if to.Dialector.Name() == "postgres" {
logger.Logger.Info("resetting sequences...")
if err := resetSequences(tx); err != nil {
return fmt.Errorf("failed to reset sequences: %w", err)
}
}
tx.Commit()
if err := tx.Error; err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
return nil
}
func migrateTable[T any](from, to *gorm.DB) error {
var data []T
if err := from.Find(&data).Error; err != nil {
return fmt.Errorf("failed to fetch data: %w", err)
}
if len(data) == 0 {
return nil
}
// to avoid "failed to migrate transactions: failed to insert data: extended protocol limited to 65535 parameters"
// see https://stackoverflow.com/questions/77372430/extended-protocol-limited-to-65535-parameters-golang-gorm
// max statements is 65535
// but it's the number of records * columns
// to be safe, using a lower value of 1000.
// this will fail if any table has more than 65 columns, which I doubt we will have
max := 1000
for i := 0; i < len(data); i += max {
j := min(i+max, len(data))
if err := to.Create(data[i:j]).Error; err != nil {
return fmt.Errorf("failed to insert data: %w", err)
}
}
return nil
}
func checkSchema(db *gorm.DB) error {
tables, err := listTables(db)
if err != nil {
return fmt.Errorf("failed to list database tables: %w", err)
}
for _, table := range expectedTables {
if !slices.Contains(tables, table) {
return fmt.Errorf("table missing from the database: %q", table)
}
}
for _, table := range tables {
if !slices.Contains(expectedTables, table) {
return fmt.Errorf("unexpected table found in the database: %q", table)
}
}
return nil
}
func listTables(db *gorm.DB) ([]string, error) {
var query string
switch db.Dialector.Name() {
case "sqlite":
query = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';"
case "postgres":
query = "SELECT tablename FROM pg_tables WHERE schemaname = 'public';"
default:
return nil, fmt.Errorf("unsupported database: %q", db.Dialector.Name())
}
rows, err := db.Raw(query).Rows()
if err != nil {
return nil, fmt.Errorf("failed to query table names: %w", err)
}
defer func() {
if err := rows.Close(); err != nil {
logger.Logger.WithError(err).Error("failed to close rows")
}
}()
var tables []string
for rows.Next() {
var table string
if err := rows.Scan(&table); err != nil {
return nil, fmt.Errorf("failed to scan table name: %w", err)
}
tables = append(tables, table)
}
return tables, nil
}
func resetSequences(db *gorm.DB) error {
type resetReq struct {
table string
seq string
}
resetReqs := []resetReq{
{"apps", "apps_2_id_seq"},
{"app_permissions", "app_permissions_2_id_seq"},
{"request_events", "request_events_id_seq"},
{"response_events", "response_events_id_seq"},
{"transactions", "transactions_id_seq"},
{"swaps", "swaps_id_seq"},
{"forwards", "forwards_id_seq"},
{"user_configs", "user_configs_id_seq"},
}
for _, req := range resetReqs {
if err := resetPostgresSequence(db, req.table, req.seq); err != nil {
return fmt.Errorf("failed to reset sequence %q for %q: %w", req.seq, req.table, err)
}
}
return nil
}
func resetPostgresSequence(db *gorm.DB, table string, seq string) error {
query := fmt.Sprintf("SELECT setval('%s', (SELECT MAX(id) FROM %s));", seq, table)
if err := db.Exec(query).Error; err != nil {
return fmt.Errorf("failed to execute setval(): %w", err)
}
return nil
}

View file

@ -0,0 +1,29 @@
package migrations
import (
_ "embed"
"text/template"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
const appLastSettledTransactionMigration = `ALTER TABLE apps ADD COLUMN last_settled_transaction_at {{ .Timestamp }};`
var appLastSettledTransactionMigrationTmpl = template.Must(template.New("appLastSettledTransactionMigration").Parse(appLastSettledTransactionMigration))
var _202604081200_app_last_settled_transaction = &gormigrate.Migration{
ID: "202604081200_app_last_settled_transaction",
Migrate: func(tx *gorm.DB) error {
err := exec(tx, appLastSettledTransactionMigrationTmpl)
if err != nil {
return err
}
return nil
},
Rollback: func(tx *gorm.DB) error {
return nil
},
}

View file

@ -38,6 +38,7 @@ func Migrate(gormDB *gorm.DB) error {
_202508151405_swap_xpub,
_202508192137_forwards,
_202509031250_transactions_updated_at_index,
_202604081200_app_last_settled_transaction,
})
return m.Migrate()

View file

@ -16,16 +16,17 @@ type UserConfig struct {
}
type App struct {
ID uint
Name string `validate:"required"`
Description string
AppPubkey string `validate:"required"`
WalletPubkey *string
CreatedAt time.Time
UpdatedAt time.Time
LastUsedAt *time.Time
Isolated bool
Metadata datatypes.JSON
ID uint
Name string `validate:"required"`
Description string
AppPubkey string `validate:"required"`
WalletPubkey *string
CreatedAt time.Time
UpdatedAt time.Time
LastUsedAt *time.Time
LastSettledTransactionAt *time.Time
Isolated bool
Metadata datatypes.JSON
}
type AppPermission struct {
@ -96,8 +97,8 @@ type Swap struct {
Type string
State string
Invoice string
SendAmount uint64
ReceiveAmount uint64
SendAmountSat uint64 `gorm:"column:send_amount"`
ReceiveAmountSat uint64 `gorm:"column:receive_amount"`
Preimage string
PaymentHash string
DestinationAddress string

View file

@ -8,15 +8,18 @@ import (
"gorm.io/gorm"
)
func GetBudgetUsageSat(tx *gorm.DB, appPermission *db.AppPermission) uint64 {
func GetBudgetUsageMsat(tx *gorm.DB, appPermission *db.AppPermission) (uint64, error) {
var result struct {
Sum uint64
}
tx.
err := tx.
Table("transactions").
Select("SUM(amount_msat + fee_msat + fee_reserve_msat) as sum").
Where("app_id = ? AND type = ? AND (state = ? OR state = ?) AND created_at > ?", appPermission.AppId, constants.TRANSACTION_TYPE_OUTGOING, constants.TRANSACTION_STATE_SETTLED, constants.TRANSACTION_STATE_PENDING, getStartOfBudget(appPermission.BudgetRenewal)).Scan(&result)
return result.Sum / 1000
Where("app_id = ? AND type = ? AND (state = ? OR state = ?) AND created_at > ?", appPermission.AppId, constants.TRANSACTION_TYPE_OUTGOING, constants.TRANSACTION_STATE_SETTLED, constants.TRANSACTION_STATE_PENDING, getStartOfBudget(appPermission.BudgetRenewal)).Scan(&result).Error
if err != nil {
return 0, err
}
return result.Sum, nil
}
func getStartOfBudget(budget_type string) time.Time {

View file

@ -0,0 +1,234 @@
package queries
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/tests"
)
func TestGetBudgetUsage_IncludesPendingAndSettledOutgoing(t *testing.T) {
svc, err := tests.CreateTestService(t)
require.NoError(t, err)
defer svc.Remove()
app, _, err := tests.CreateApp(svc)
require.NoError(t, err)
appPermission := &db.AppPermission{
AppId: app.ID,
App: *app,
Scope: constants.PAY_INVOICE_SCOPE,
BudgetRenewal: constants.BUDGET_RENEWAL_NEVER,
}
require.NoError(t, svc.DB.Create(&db.Transaction{
AppId: &app.ID,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_PENDING,
AmountMsat: 50000,
FeeMsat: 1000,
FeeReserveMsat: 2000,
}).Error)
require.NoError(t, svc.DB.Create(&db.Transaction{
AppId: &app.ID,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_SETTLED,
AmountMsat: 25000,
FeeMsat: 500,
FeeReserveMsat: 500,
}).Error)
budgetUsageMsat, err := GetBudgetUsageMsat(svc.DB, appPermission)
require.NoError(t, err)
assert.Equal(t, uint64(79000), budgetUsageMsat)
}
func TestGetBudgetUsage_ExcludesWrongStateTypeAndApp(t *testing.T) {
svc, err := tests.CreateTestService(t)
require.NoError(t, err)
defer svc.Remove()
app, _, err := tests.CreateApp(svc)
require.NoError(t, err)
otherApp, _, err := tests.CreateApp(svc)
require.NoError(t, err)
appPermission := &db.AppPermission{
AppId: app.ID,
App: *app,
Scope: constants.PAY_INVOICE_SCOPE,
BudgetRenewal: constants.BUDGET_RENEWAL_NEVER,
}
require.NoError(t, svc.DB.Create(&db.Transaction{
AppId: &app.ID,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_SETTLED,
AmountMsat: 20000,
FeeMsat: 1000,
FeeReserveMsat: 0,
}).Error)
require.NoError(t, svc.DB.Create(&db.Transaction{
AppId: &app.ID,
Type: constants.TRANSACTION_TYPE_INCOMING,
State: constants.TRANSACTION_STATE_SETTLED,
AmountMsat: 90000,
FeeMsat: 0,
FeeReserveMsat: 0,
}).Error)
require.NoError(t, svc.DB.Create(&db.Transaction{
AppId: &app.ID,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_FAILED,
AmountMsat: 90000,
FeeMsat: 0,
FeeReserveMsat: 0,
}).Error)
require.NoError(t, svc.DB.Create(&db.Transaction{
AppId: &otherApp.ID,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_SETTLED,
AmountMsat: 90000,
FeeMsat: 0,
FeeReserveMsat: 0,
}).Error)
budgetUsageMsat, err := GetBudgetUsageMsat(svc.DB, appPermission)
require.NoError(t, err)
assert.Equal(t, uint64(21000), budgetUsageMsat)
}
func TestGetBudgetUsage_BudgetWindowDaily(t *testing.T) {
svc, err := tests.CreateTestService(t)
require.NoError(t, err)
defer svc.Remove()
app, _, err := tests.CreateApp(svc)
require.NoError(t, err)
appPermissionDaily := &db.AppPermission{
AppId: app.ID,
App: *app,
Scope: constants.PAY_INVOICE_SCOPE,
BudgetRenewal: constants.BUDGET_RENEWAL_DAILY,
}
dailyStart := getStartOfBudget(constants.BUDGET_RENEWAL_DAILY)
require.NoError(t, svc.DB.Create(&db.Transaction{
AppId: &app.ID,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_SETTLED,
AmountMsat: 20000,
FeeMsat: 1000,
FeeReserveMsat: 0,
CreatedAt: dailyStart.Add(1 * time.Minute),
}).Error)
require.NoError(t, svc.DB.Create(&db.Transaction{
AppId: &app.ID,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_PENDING,
AmountMsat: 20000,
FeeMsat: 0,
FeeReserveMsat: 1000,
CreatedAt: dailyStart.Add(2 * time.Hour),
}).Error)
budgetUsageMsat, err := GetBudgetUsageMsat(svc.DB, appPermissionDaily)
require.NoError(t, err)
assert.Equal(t, uint64(42000), budgetUsageMsat)
}
func TestGetBudgetUsage_BudgetWindowWeekly(t *testing.T) {
svc, err := tests.CreateTestService(t)
require.NoError(t, err)
defer svc.Remove()
app, _, err := tests.CreateApp(svc)
require.NoError(t, err)
appPermissionWeekly := &db.AppPermission{
AppId: app.ID,
App: *app,
Scope: constants.PAY_INVOICE_SCOPE,
BudgetRenewal: constants.BUDGET_RENEWAL_WEEKLY,
}
weeklyStart := getStartOfBudget(constants.BUDGET_RENEWAL_WEEKLY)
require.NoError(t, svc.DB.Create(&db.Transaction{
AppId: &app.ID,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_SETTLED,
AmountMsat: 10000,
FeeMsat: 1000,
FeeReserveMsat: 0,
CreatedAt: weeklyStart.Add(30 * time.Minute),
}).Error)
require.NoError(t, svc.DB.Create(&db.Transaction{
AppId: &app.ID,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_PENDING,
AmountMsat: 7000,
FeeMsat: 0,
FeeReserveMsat: 0,
CreatedAt: weeklyStart.Add(-1 * time.Hour),
}).Error)
budgetUsageMsat, err := GetBudgetUsageMsat(svc.DB, appPermissionWeekly)
require.NoError(t, err)
assert.Equal(t, uint64(11000), budgetUsageMsat)
}
func TestGetBudgetUsage_BudgetWindowNever(t *testing.T) {
svc, err := tests.CreateTestService(t)
require.NoError(t, err)
defer svc.Remove()
app, _, err := tests.CreateApp(svc)
require.NoError(t, err)
appPermission := &db.AppPermission{
AppId: app.ID,
App: *app,
Scope: constants.PAY_INVOICE_SCOPE,
BudgetRenewal: constants.BUDGET_RENEWAL_NEVER,
}
require.NoError(t, svc.DB.Create(&db.Transaction{
AppId: &app.ID,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_SETTLED,
AmountMsat: 5000,
FeeMsat: 1000,
FeeReserveMsat: 0,
CreatedAt: time.Now().AddDate(-2, 0, 0),
}).Error)
require.NoError(t, svc.DB.Create(&db.Transaction{
AppId: &app.ID,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_PENDING,
AmountMsat: 4000,
FeeMsat: 0,
FeeReserveMsat: 1000,
CreatedAt: time.Now().AddDate(-1, 0, 0),
}).Error)
budgetUsageMsat, err := GetBudgetUsageMsat(svc.DB, appPermission)
require.NoError(t, err)
assert.Equal(t, uint64(11000), budgetUsageMsat)
}

View file

@ -5,23 +5,29 @@ import (
"gorm.io/gorm"
)
func GetIsolatedBalance(tx *gorm.DB, appId uint) int64 {
func GetIsolatedBalanceMsat(tx *gorm.DB, appId uint) (int64, error) {
var received struct {
Sum int64
}
tx.
err := tx.
Table("transactions").
Select("SUM(amount_msat) as sum").
Where("app_id = ? AND type = ? AND state = ?", appId, constants.TRANSACTION_TYPE_INCOMING, constants.TRANSACTION_STATE_SETTLED).Scan(&received)
Where("app_id = ? AND type = ? AND state = ?", appId, constants.TRANSACTION_TYPE_INCOMING, constants.TRANSACTION_STATE_SETTLED).Scan(&received).Error
if err != nil {
return 0, err
}
var spent struct {
Sum int64
}
tx.
err = tx.
Table("transactions").
Select("SUM(amount_msat + fee_msat + fee_reserve_msat) as sum").
Where("app_id = ? AND type = ? AND (state = ? OR state = ?)", appId, constants.TRANSACTION_TYPE_OUTGOING, constants.TRANSACTION_STATE_SETTLED, constants.TRANSACTION_STATE_PENDING).Scan(&spent)
Where("app_id = ? AND type = ? AND (state = ? OR state = ?)", appId, constants.TRANSACTION_TYPE_OUTGOING, constants.TRANSACTION_STATE_SETTLED, constants.TRANSACTION_STATE_PENDING).Scan(&spent).Error
if err != nil {
return 0, err
}
return received.Sum - spent.Sum
return received.Sum - spent.Sum, nil
}

View file

@ -36,8 +36,9 @@ func TestGetIsolatedBalance_PendingNoOverflow(t *testing.T) {
}
svc.DB.Save(&tx)
balance := GetIsolatedBalance(svc.DB, app.ID)
assert.Equal(t, int64(-11000), balance)
balanceMsat, err := GetIsolatedBalanceMsat(svc.DB, app.ID)
require.NoError(t, err)
assert.Equal(t, int64(-11000), balanceMsat)
}
func TestGetIsolatedBalance_SettledNoOverflow(t *testing.T) {
@ -65,6 +66,7 @@ func TestGetIsolatedBalance_SettledNoOverflow(t *testing.T) {
}
svc.DB.Save(&tx)
balance := GetIsolatedBalance(svc.DB, app.ID)
assert.Equal(t, int64(-1000), balance)
balanceMsat, err := GetIsolatedBalanceMsat(svc.DB, app.ID)
require.NoError(t, err)
assert.Equal(t, int64(-1000), balanceMsat)
}

View file

@ -0,0 +1,40 @@
package queries
import (
"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"gorm.io/datatypes"
"gorm.io/gorm"
)
func GetTotalSubwalletBalanceMsat(tx *gorm.DB) (int64, error) {
subwalletAppIDsQuery := tx.Model(&db.App{}).
Select("id").
Where(datatypes.JSONQuery("metadata").Equals(constants.SUBWALLET_APPSTORE_APP_ID, constants.METADATA_APPSTORE_APP_ID_KEY))
var received struct {
Sum int64
}
res := tx.
Table("transactions").
Select("SUM(amount_msat) as sum").
Where("app_id IN (?) AND type = ? AND state = ?", subwalletAppIDsQuery, constants.TRANSACTION_TYPE_INCOMING, constants.TRANSACTION_STATE_SETTLED).
Scan(&received)
if res.Error != nil {
return 0, res.Error
}
var spent struct {
Sum int64
}
res = tx.
Table("transactions").
Select("SUM(amount_msat + fee_msat + fee_reserve_msat) as sum").
Where("app_id IN (?) AND type = ? AND (state = ? OR state = ?)", subwalletAppIDsQuery, constants.TRANSACTION_TYPE_OUTGOING, constants.TRANSACTION_STATE_SETTLED, constants.TRANSACTION_STATE_PENDING).
Scan(&spent)
if res.Error != nil {
return 0, res.Error
}
return received.Sum - spent.Sum, nil
}

View file

@ -0,0 +1,64 @@
package queries
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/datatypes"
"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/tests"
)
func TestGetTotalSubwalletBalance(t *testing.T) {
svc, err := tests.CreateTestService(t)
require.NoError(t, err)
defer svc.Remove()
subwalletA, _, err := tests.CreateApp(svc)
require.NoError(t, err)
subwalletA.Isolated = true
subwalletA.Metadata = datatypes.JSON([]byte(fmt.Sprintf(`{"%s":"%s"}`, constants.METADATA_APPSTORE_APP_ID_KEY, constants.SUBWALLET_APPSTORE_APP_ID)))
svc.DB.Save(&subwalletA)
subwalletB, _, err := tests.CreateApp(svc)
require.NoError(t, err)
subwalletB.Isolated = true
subwalletB.Metadata = datatypes.JSON([]byte(fmt.Sprintf(`{"%s":"%s"}`, constants.METADATA_APPSTORE_APP_ID_KEY, constants.SUBWALLET_APPSTORE_APP_ID)))
svc.DB.Save(&subwalletB)
incomingSubwalletTx := db.Transaction{
AppId: &subwalletA.ID,
Type: constants.TRANSACTION_TYPE_INCOMING,
State: constants.TRANSACTION_STATE_SETTLED,
AmountMsat: 5000,
}
svc.DB.Save(&incomingSubwalletTx)
outgoingSettledSubwalletTx := db.Transaction{
AppId: &subwalletA.ID,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_SETTLED,
AmountMsat: 1000,
FeeMsat: 100,
FeeReserveMsat: 0,
}
svc.DB.Save(&outgoingSettledSubwalletTx)
outgoingPendingSubwalletTx := db.Transaction{
AppId: &subwalletB.ID,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_PENDING,
AmountMsat: 2000,
FeeMsat: 0,
FeeReserveMsat: 300,
}
svc.DB.Save(&outgoingPendingSubwalletTx)
totalBalanceMsat, err := GetTotalSubwalletBalanceMsat(svc.DB)
require.NoError(t, err)
assert.Equal(t, int64(1600), totalBalanceMsat)
}

View file

@ -1,2 +1,2 @@
# set a custom messageboard wallet (should be a sub-wallet with only make invoice and list transactions permissions)
#VITE_LIGHTNING_MESSAGEBOARD_NWC_URL="nostr+walletconnect://5f8e7c098137ccca853327be44a9b2e956cf79a8e2336e27a4f27b3fb55325b6?relay=wss://relay.getalby.com/v1&secret=ace5c4b9e08138a2ef91b4ccf1379952c77c651866b29f5872b5165134417894"
#VITE_LIGHTNING_MESSAGEBOARD_NWC_URL="nostr+walletconnect://5f8e7c098137ccca853327be44a9b2e956cf79a8e2336e27a4f27b3fb55325b6?relay=wss://relay.getalby.com&relay=wss://relay2.getalby.com&secret=ace5c4b9e08138a2ef91b4ccf1379952c77c651866b29f5872b5165134417894"

View file

@ -4,7 +4,7 @@
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"config": "",
"css": "src/index.css",
"baseColor": "zinc",
"cssVariables": true,

View file

@ -1,20 +1,10 @@
import { fixupConfigRules, fixupPluginRules } from "@eslint/compat";
import { FlatCompat } from "@eslint/eslintrc";
import js from "@eslint/js";
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import eslint from "@eslint/js";
import tsParser from "@typescript-eslint/parser";
import eslintConfigPrettier from "eslint-config-prettier/flat";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import globals from "globals";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
import tseslint from "typescript-eslint";
export default [
{
@ -25,20 +15,12 @@ export default [
"src/components/ui/navigation-menu.tsx",
],
},
...fixupConfigRules(
compat.extends(
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
"prettier"
)
),
eslint.configs.recommended,
...tseslint.configs.recommended,
reactRefresh.configs.vite,
reactHooks.configs.flat["recommended-latest"],
eslintConfigPrettier,
{
plugins: {
"react-refresh": reactRefresh,
"@typescript-eslint": fixupPluginRules(typescriptEslint),
},
languageOptions: {
globals: {
...globals.browser,
@ -48,20 +30,7 @@ export default [
},
files: ["**/*.ts", "**/*.tsx"],
rules: {
"react-refresh/only-export-components": [
"warn",
{
allowConstantExport: true,
},
],
"@typescript-eslint/ban-ts-comment": [
"error",
{
"ts-ignore": "allow-with-description",
},
],
"react-hooks/set-state-in-effect": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{

View file

@ -20,97 +20,63 @@
"prepare": "cd .. && husky frontend/.husky"
},
"dependencies": {
"@getalby/lightning-tools": "^6.0.0",
"@getalby/sdk": "^6.0.1",
"@hookform/resolvers": "^5.1.1",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-aspect-ratio": "^1.1.7",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-navigation-menu": "^1.2.13",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.5",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-toast": "^1.2.14",
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7",
"@scure/bip39": "^2.0.1",
"@stepperize/react": "^5.1.8",
"argon2-wasm-esm": "^1.0.3",
"@base-ui/react": "^1.5.0",
"@fontsource-variable/figtree": "^5.3.0",
"@fontsource-variable/inter": "^5.3.0",
"@getalby/lightning-tools": "^8.1.0",
"@getalby/sdk": "^8.0.3",
"@scure/bip39": "^2.2.0",
"@stepperize/react": "^6.1.0",
"bitcoin-address-validation": "^3.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"compare-versions": "^6.1.1",
"date-fns": "^4.1.0",
"dayjs": "^1.11.10",
"dayjs": "^1.11.20",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.544.0",
"next-themes": "^0.4.6",
"react": "18.3.1",
"react-day-picker": "^9.11.0",
"react-dom": "18.3.1",
"react-hook-form": "^7.60.0",
"react-lottie": "^1.2.4",
"react-qr-code": "^2.0.12",
"react-resizable-panels": "^3.0.6",
"react-router-dom": "^6.21.0",
"recharts": "2.15.4",
"lottie-react": "^2.4.1",
"lucide-react": "^1.28.0",
"qr-code-styling": "^1.9.2",
"radix-ui": "^1.4.3",
"react": "^19.2.6",
"react-day-picker": "^9.14.0",
"react-dom": "^19.2.6",
"react-router": "^7.18.2",
"sonner": "^2.0.7",
"swr": "^2.3.6",
"tailwind-merge": "^3.3.1",
"tw-animate-css": "^1.3.5",
"swr": "^2.4.1",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0",
"vaul": "^1.1.2",
"zod": "^4.0.2",
"zustand": "^4.5.0"
"zustand": "^5.0.12"
},
"devDependencies": {
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^20.0.0",
"@eslint/compat": "^1.0.3",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.4.0",
"@commitlint/cli": "^20.5.3",
"@commitlint/config-conventional": "^21.2.0",
"@eslint/eslintrc": "^3.3.5",
"@eslint/js": "^10.0.1",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.11",
"@types/node": "^24.7.2",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@types/react-lottie": "^1.2.10",
"@typescript-eslint/eslint-plugin": "^7.11.0",
"@typescript-eslint/parser": "^7.11.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"eslint": "^9.4.0",
"@tailwindcss/vite": "^4.2.4",
"@types/node": "^25.9.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react-swc": "^4.3.1",
"eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.3",
"globals": "^15.4.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"husky": "^9.0.11",
"lint-staged": "^15.2.5",
"prettier": "3.6.2",
"lint-staged": "^16.4.0",
"prettier": "3.8.3",
"shx": "^0.4.0",
"tailwindcss": "^4.1.16",
"tailwindcss": "^4.3.0",
"typescript": "^5.9.3",
"vite": "^5.4.0",
"vite-plugin-pwa": "^0.20.1",
"vite-tsconfig-paths": "^5.1.4"
}
"typescript-eslint": "^8.61.0",
"vite": "^8.2.1",
"vite-plugin-pwa": "^1.3.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View file

@ -10,7 +10,7 @@ export const request = async <T>(
args[1]?.body?.toString() || ""
);
console.info("Wails request", ...args, res);
console.info("Wails request", args[0].toString(), args[1]?.method || "GET");
if (res.error) {
throw new Error(res.error);
}

View file

@ -1,31 +1,32 @@
import {
RouterProvider,
createBrowserRouter,
createHashRouter,
} from "react-router-dom";
import { createBrowserRouter, createHashRouter } from "react-router";
import { RouterProvider } from "react-router/dom";
import { Toaster } from "src/components/ui/sonner";
import { ThemeProvider } from "src/components/ui/theme-provider";
import { TouchProvider } from "src/components/ui/tooltip";
import { useInfo } from "src/hooks/useInfo";
import { useRegisterProtocolHandler } from "src/hooks/useRegisterProtocolHandler";
import routes from "src/routes.tsx";
import { isHttpMode } from "src/utils/isHttpMode";
const createRouterFunc = isHttpMode() ? createBrowserRouter : createHashRouter;
const basePath =
import.meta.env.BASE_URL !== "/" ? import.meta.env.BASE_URL : "";
const router = createRouterFunc(routes, {
// if running on a subpath, use the subpath as the router basename
// BASE_URL is set via process.env.BASE_PATH, see https://vite.dev/guide/build#public-base-path
basename:
import.meta.env.BASE_URL !== "/" ? import.meta.env.BASE_URL : undefined,
basename: basePath || undefined,
});
function App() {
const { data: info } = useInfo();
useRegisterProtocolHandler(basePath);
return (
<>
<TouchProvider>
<ThemeProvider
defaultTheme="default"
defaultTheme="alby"
defaultDarkMode="system"
storageKey="vite-ui-theme"
>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,12 @@
<svg width="672" height="660" viewBox="0 0 672 660" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_531_1058)">
<path d="M672 0H0V660H672V0Z" fill="#070707"/>
<path d="M214.167 255.106L137.061 267.979C124.805 270.011 117.322 282.607 121.387 294.339C138.046 342.411 159.232 374.006 183.343 370.188C203.543 366.985 224.822 325.566 236.923 280.081C240.68 265.977 228.609 252.704 214.198 255.106H214.167Z" fill="white"/>
<path d="M446.871 660L443.391 634.471C442.191 625.664 437.787 617.595 431.074 611.806C366.47 556.251 324.961 499.065 300.142 457.03C296.569 450.963 304.299 444.589 309.564 449.3C375.338 508.057 475.663 569.586 525.794 535.064C584.086 494.938 561.175 374.037 505.994 270.535C503.561 266.008 487.703 239.216 484.039 233.427C450.813 180.736 368.841 51.4279 330.103 72.1529C301.496 87.4581 336.939 158.533 351.782 207.066C354.245 215.073 350.673 223.695 343.283 227.637C337.678 230.624 332.413 234.104 323.052 240.232C239.109 298.035 208.778 329.723 256.785 393.53C261.373 399.628 261.342 408.004 256.6 414.009C228.055 450.101 127.268 554.373 112.057 660M515.94 462.758C525.271 476.307 523.885 504.947 512.769 512.584C501.683 520.221 476.155 510.12 466.825 496.571C457.495 483.021 466.764 474.798 480.343 465.468C493.892 456.137 506.61 449.177 515.94 462.758ZM442.375 317.282C455.154 306.042 473.723 304.933 490.844 314.387C493.738 315.988 495.801 318.606 496.725 321.747C497.649 324.919 497.249 328.245 495.678 331.109C494.077 334.003 491.46 336.098 488.288 336.991C485.116 337.914 481.791 337.514 478.927 335.944C471.167 331.663 463.407 331.601 458.634 335.79C454.354 339.547 453.461 345.952 456.232 352.942C457.433 355.991 457.372 359.348 456.078 362.366C454.785 365.384 452.352 367.724 449.304 368.925C447.856 369.51 446.348 369.787 444.777 369.787C439.696 369.787 435.17 366.739 433.322 361.996C426.671 345.182 430.15 328.06 442.375 317.282ZM348.579 385.092C361.358 373.852 379.927 372.744 397.048 382.198C399.942 383.799 402.005 386.417 402.929 389.558C403.853 392.73 403.453 396.055 401.882 398.919C400.281 401.814 397.664 403.908 394.492 404.801C391.32 405.725 387.995 405.325 385.131 403.754C377.371 399.474 369.611 399.412 364.838 403.6C360.558 407.357 359.665 413.763 362.436 420.753C364.93 427.066 361.82 434.241 355.508 436.736C354.06 437.321 352.521 437.598 350.981 437.598C345.9 437.598 341.404 434.549 339.526 429.807C332.875 412.993 336.354 395.84 348.579 385.092Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_531_1058">
<rect width="672" height="660" rx="330" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,12 @@
<svg width="672" height="660" viewBox="0 0 672 660" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_531_1063)">
<path d="M672 0H0V660H672V0Z" fill="white"/>
<path d="M214.167 255.106L137.061 267.979C124.805 270.011 117.322 282.607 121.387 294.339C138.046 342.411 159.232 374.006 183.343 370.188C203.543 366.985 224.822 325.566 236.923 280.081C240.68 265.977 228.609 252.704 214.198 255.106H214.167Z" fill="#060606"/>
<path d="M446.871 660L443.391 634.471C442.191 625.664 437.787 617.595 431.074 611.806C366.47 556.251 324.961 499.065 300.142 457.03C296.569 450.963 304.299 444.589 309.564 449.3C375.338 508.057 475.663 569.586 525.794 535.064C584.086 494.938 561.175 374.037 505.994 270.535C503.561 266.008 487.703 239.216 484.039 233.427C450.813 180.736 368.841 51.4279 330.103 72.1529C301.496 87.4581 336.939 158.533 351.782 207.066C354.245 215.073 350.673 223.695 343.283 227.637C337.678 230.624 332.413 234.104 323.052 240.232C239.109 298.035 208.778 329.723 256.785 393.53C261.373 399.628 261.342 408.004 256.6 414.009C228.055 450.101 127.268 554.373 112.057 660M515.94 462.758C525.271 476.307 523.885 504.947 512.769 512.584C501.683 520.221 476.155 510.12 466.825 496.571C457.495 483.021 466.764 474.798 480.343 465.468C493.892 456.137 506.61 449.177 515.94 462.758ZM442.375 317.282C455.154 306.042 473.723 304.933 490.844 314.387C493.738 315.988 495.801 318.606 496.725 321.747C497.649 324.919 497.249 328.245 495.678 331.109C494.077 334.003 491.46 336.098 488.288 336.991C485.116 337.914 481.791 337.514 478.927 335.944C471.167 331.663 463.407 331.601 458.634 335.79C454.354 339.547 453.461 345.952 456.232 352.942C457.433 355.991 457.372 359.348 456.078 362.366C454.785 365.384 452.352 367.724 449.304 368.925C447.856 369.51 446.348 369.787 444.777 369.787C439.696 369.787 435.17 366.739 433.322 361.996C426.671 345.182 430.15 328.06 442.375 317.282ZM348.579 385.092C361.358 373.852 379.927 372.744 397.048 382.198C399.942 383.799 402.005 386.417 402.929 389.558C403.853 392.73 403.453 396.055 401.882 398.919C400.281 401.814 397.664 403.908 394.492 404.801C391.32 405.725 387.995 405.325 385.131 403.754C377.371 399.474 369.611 399.412 364.838 403.6C360.558 407.357 359.665 413.763 362.436 420.753C364.93 427.066 361.82 434.241 355.508 436.736C354.06 437.321 352.521 437.598 350.981 437.598C345.9 437.598 341.404 434.549 339.526 429.807C332.875 412.993 336.354 395.84 348.579 385.092Z" fill="#060606"/>
</g>
<defs>
<clipPath id="clip0_531_1063">
<rect width="672" height="660" rx="330" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -5,10 +5,10 @@ import { useBalances } from "src/hooks/useBalances";
import { useChannels } from "src/hooks/useChannels";
export function AnchorReserveAlert({
amount,
amountSat,
className,
}: {
amount: number;
amountSat: number;
className?: string;
}) {
const { data: balances } = useBalances();
@ -19,9 +19,9 @@ export function AnchorReserveAlert({
}
const showAlert =
amount &&
amountSat &&
!!channels.length &&
+amount > balances.onchain.spendable - channels.length * 25000;
+amountSat > balances.onchain.spendableSat - channels.length * 25000;
if (!showAlert) {
return null;
@ -32,12 +32,14 @@ export function AnchorReserveAlert({
<AlertTriangleIcon className="h-4 w-4" />
<AlertTitle>Channel Anchor Reserves will be depleted</AlertTitle>
<AlertDescription>
You have channels open and by spending your entire on-chain balance
including your anchor reserves may put your node at risk of unable to
reclaim funds in your channel after a force-closure. To prevent this,
set aside at least{" "}
<FormattedBitcoinAmount amount={channels.length * 25000 * 1000} />{" "}
on-chain.
<p>
You have channels open and by spending your entire on-chain balance
including your anchor reserves may put your node at risk of unable to
reclaim funds in your channel after a force-closure. To prevent this,
set aside at least{" "}
<FormattedBitcoinAmount amountMsat={channels.length * 25000 * 1000} />{" "}
on-chain.
</p>
</AlertDescription>
</Alert>
);

View file

@ -1,9 +1,32 @@
import claudeLogo from "src/assets/suggested-apps/claude.png";
import clineLogo from "src/assets/suggested-apps/cline.png";
import codexLogo from "src/assets/suggested-apps/codex.png";
import cursorLogo from "src/assets/suggested-apps/cursor.png";
import geminiLogo from "src/assets/suggested-apps/gemini.png";
import gooseLogo from "src/assets/suggested-apps/goose.png";
import hermesLogo from "src/assets/suggested-apps/hermes.png";
import openclawLogo from "src/assets/suggested-apps/openclaw.png";
import opencodeLogo from "src/assets/suggested-apps/opencode.png";
import { appStoreApps } from "src/components/connections/SuggestedAppData";
import UserAvatar from "src/components/UserAvatar";
import { ALBY_ACCOUNT_APP_NAME } from "src/constants";
import { cn } from "src/lib/utils";
import { App } from "src/types";
// Lightweight logo map for agents that don't have full app store entries.
// Matches on app_store_app_id metadata set when the connection was created.
const agentLogos: Record<string, string> = {
claude: claudeLogo,
goose: gooseLogo,
hermes: hermesLogo,
openclaw: openclawLogo,
cursor: cursorLogo,
codex: codexLogo,
cline: clineLogo,
gemini: geminiLogo,
opencode: opencodeLogo,
};
type Props = {
app: App;
className?: string;
@ -13,13 +36,20 @@ export default function AppAvatar({ app, className }: Props) {
if (app.name === ALBY_ACCOUNT_APP_NAME) {
return <UserAvatar className={className} />;
}
const agentLogo =
(app?.metadata?.app_store_app_id
? agentLogos[app.metadata.app_store_app_id]
: undefined) ||
Object.entries(agentLogos).find(([key]) =>
app.name.toLowerCase().includes(key)
)?.[1];
const appStoreApp = appStoreApps.find(
(suggestedApp) =>
(app?.metadata?.app_store_app_id &&
suggestedApp.id === app.metadata?.app_store_app_id) ||
app.name.includes(suggestedApp.title)
);
const image = appStoreApp?.logo;
const image = agentLogo || appStoreApp?.logo;
const gradient =
app.name

View file

@ -9,6 +9,7 @@ type Props = {
contentRight?: React.ReactNode;
breadcrumb?: boolean;
addSidebarTrigger?: boolean;
pageTitle?: string;
};
function AppHeader({
@ -17,9 +18,11 @@ function AppHeader({
description = "",
contentRight,
addSidebarTrigger = true,
pageTitle,
}: Props) {
return (
<>
{pageTitle && <title>{`${pageTitle} · Alby Hub`}</title>}
<header className="flex flex-row flex-wrap items-center border-b border-border pb-4 gap-2">
{addSidebarTrigger && <SidebarTrigger className="-ml-1 md:hidden" />}
<Separator orientation="vertical" className="mr-2 h-4 md:hidden" />

View file

@ -1,21 +1,23 @@
import {
BotIcon,
BoxIcon,
ChevronsUpDown,
CircleHelp,
ChevronsUpDownIcon,
CreditCardIcon,
CircleHelpIcon,
HandCoinsIcon,
HomeIcon,
LogOut,
LogOutIcon,
LucideIcon,
Plug2Icon,
PlugZapIcon,
Settings,
Sparkles,
SquareStack,
StarIcon,
SettingsIcon,
SparklesIcon,
SquareStackIcon,
WalletIcon,
} from "lucide-react";
import React from "react";
import { Link, NavLink, useLocation, useNavigate } from "react-router-dom";
import { Link, NavLink, useLocation, useNavigate } from "react-router";
import ExternalLink from "src/components/ExternalLink";
import { AlbyIcon } from "src/components/icons/Alby";
@ -23,6 +25,7 @@ import { AlbyHubIcon } from "src/components/icons/AlbyHubIcon";
import { AlbyHubLogo } from "src/components/icons/AlbyHubLogo";
import { ProBadge } from "src/components/ProBadge";
import SidebarHint from "src/components/SidebarHint";
import { Badge } from "src/components/ui/badge";
import {
DropdownMenu,
DropdownMenuContent,
@ -51,6 +54,10 @@ import { useInfo } from "src/hooks/useInfo";
import { deleteAuthToken } from "src/lib/auth";
import { isHttpMode } from "src/utils/isHttpMode";
function isPathActive(pathname: string, url: string) {
return pathname === url || pathname.startsWith(`${url}/`);
}
export function AppSidebar() {
const { data: albyMe } = useAlbyMe();
@ -88,13 +95,24 @@ export function AppSidebar() {
{
title: "Sub-wallets",
url: "/sub-wallets",
icon: SquareStack,
icon: SquareStackIcon,
},
{
title: "Connections",
url: "/apps",
icon: Plug2Icon,
},
{
title: "AI & Agents",
url: "/ai",
icon: BotIcon,
},
{
title: "Cards",
url: "/cards",
icon: CreditCardIcon,
badge: "NEW",
},
],
navSecondary: [
...(hasChannelManagement
@ -109,12 +127,12 @@ export function AppSidebar() {
{
title: "Settings",
url: "/settings",
icon: Settings,
icon: SettingsIcon,
},
{
title: "Review & Earn",
url: "/review-earn",
icon: StarIcon,
title: "Earn",
url: "/earn",
icon: HandCoinsIcon,
},
],
};
@ -127,7 +145,7 @@ export function AppSidebar() {
<SidebarHeader>
<div className="p-2 flex flex-row items-center justify-between">
<Link to="/home" onClick={() => setOpenMobile(false)}>
<AlbyHubLogo className="w-32" />
<AlbyHubLogo className="h-7" />
</Link>
<div className="flex gap-3 items-center">
<HealthIndicator />
@ -142,7 +160,7 @@ export function AppSidebar() {
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
asChild
isActive={location.pathname === item.url}
isActive={isPathActive(location.pathname, item.url)}
>
<Link
to={item.url}
@ -152,6 +170,11 @@ export function AppSidebar() {
>
<item.icon />
<span>{item.title}</span>
{item.badge && (
<Badge className="ml-auto text-[10px] px-1.5 py-0">
{item.badge}
</Badge>
)}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
@ -200,7 +223,7 @@ export function AppSidebar() {
</div>
</>
)}
<ChevronsUpDown className="ml-auto size-4" />
<ChevronsUpDownIcon className="ml-auto size-4" />
</DropdownMenuTrigger>
</SidebarMenuButton>
<DropdownMenuContent
@ -255,7 +278,7 @@ export function AppSidebar() {
<>
<UpgradeDialog>
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<Sparkles />
<SparklesIcon />
Upgrade to Pro
</DropdownMenuItem>
</UpgradeDialog>
@ -265,7 +288,7 @@ export function AppSidebar() {
<>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={logout}>
<LogOut className="size-4" />
<LogOutIcon className="size-4" />
Log out
</DropdownMenuItem>
</>
@ -300,7 +323,7 @@ export function NavSecondary({
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
asChild
isActive={location.pathname === item.url}
isActive={isPathActive(location.pathname, item.url)}
>
<NavLink
to={item.url}
@ -318,7 +341,7 @@ export function NavSecondary({
<SidebarMenuItem>
<SidebarMenuButton asChild>
<ExternalLink to="https://support.getalby.com">
<CircleHelp className="h-4 w-4" />
<CircleHelpIcon className="h-4 w-4" />
Help
</ExternalLink>
</SidebarMenuButton>

View file

@ -1,6 +1,6 @@
import { RefreshCwIcon } from "lucide-react";
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import Container from "src/components/Container";
import PasswordInput from "src/components/password/PasswordInput";
import TwoColumnLayoutHeader from "src/components/TwoColumnLayoutHeader";
@ -62,6 +62,7 @@ function AuthCodeForm({ url }: AuthCodeFormProps) {
<div className="grid gap-5">
<TwoColumnLayoutHeader
title="Connect your Alby Account"
pageTitle="Connect your Alby Account"
description="A new window will open. Sign in with your Alby Account, copy the Authorization Code, and paste it here."
/>
{!hasRequestedCode && (

View file

@ -1,5 +1,5 @@
import { XIcon } from "lucide-react";
import { Link } from "react-router-dom";
import { Link } from "react-router";
import ExternalLink from "src/components/ExternalLink";
import { useAlbyInfo } from "src/hooks/useAlbyInfo";

View file

@ -1,5 +1,5 @@
import { Fragment } from "react";
import { Link, useMatches } from "react-router-dom";
import { Link, useMatches } from "react-router";
import {
Breadcrumb,
BreadcrumbItem,

View file

@ -2,97 +2,77 @@ import React from "react";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import { Input } from "src/components/ui/input";
import { Label } from "src/components/ui/label";
import { cn } from "src/lib/utils";
import { budgetOptions as defaultBudgetOptions } from "src/types";
import { budgetOptionsSat as defaultBudgetOptionsSat } from "src/types";
function BudgetAmountSelect({
value,
valueSat,
onChange,
minAmount,
budgetOptions = defaultBudgetOptions,
minAmountSat,
budgetOptionsSat = defaultBudgetOptionsSat,
}: {
value: number;
valueSat: number;
onChange: (value: number) => void;
minAmount?: number;
budgetOptions?: typeof defaultBudgetOptions;
minAmountSat?: number;
budgetOptionsSat?: typeof defaultBudgetOptionsSat;
}) {
const [customBudget, setCustomBudget] = React.useState(
value ? !Object.values(budgetOptions).includes(value) : false
const [inputValue, setInputValue] = React.useState(
valueSat ? String(valueSat) : ""
);
React.useEffect(() => {
setInputValue(valueSat ? String(valueSat) : "");
}, [valueSat]);
return (
<>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 text-xs mb-4">
{Object.keys(budgetOptions)
<div className="grid grid-cols-3 gap-3 text-xs mb-3">
{Object.keys(budgetOptionsSat)
.filter(
(budget) =>
!minAmount ||
!budgetOptions[budget] ||
budgetOptions[budget] > minAmount
!minAmountSat || budgetOptionsSat[budget] >= minAmountSat
)
.map((budget) => {
return (
<button
type="button"
key={budget}
onClick={() => {
setCustomBudget(false);
onChange(budgetOptions[budget]);
}}
className={cn(
"cursor-pointer rounded text-nowrap border-2 text-center p-2 py-4 slashed-zero",
!customBudget && value == budgetOptions[budget]
? "border-primary"
: "border-muted"
)}
>
{budgetOptions[budget] ? (
<>
<FormattedBitcoinAmount
amount={budgetOptions[budget] * 1000}
/>
<FormattedFiatAmount
className="text-xs"
showApprox
amount={budgetOptions[budget]}
/>
</>
) : (
budget
)}
</button>
);
})}
<button
onClick={() => {
setCustomBudget(true);
onChange(0);
}}
className={cn(
"cursor-pointer rounded border-2 text-center p-4 dark:text-white",
customBudget ? "border-primary" : "border-muted"
)}
>
Custom
</button>
.map((budget) => (
<button
type="button"
key={budget}
onClick={() => {
onChange(budgetOptionsSat[budget]);
}}
className={cn(
"cursor-pointer rounded text-nowrap border-2 text-center p-3 py-4 slashed-zero",
valueSat === budgetOptionsSat[budget]
? "border-primary"
: "border-muted"
)}
>
<FormattedBitcoinAmount
amountMsat={budgetOptionsSat[budget] * 1000}
/>
<FormattedFiatAmount
className="text-xs"
showApprox
amountSat={budgetOptionsSat[budget]}
/>
</button>
))}
</div>
<div className="mb-3">
<Input
id="budget"
name="budget"
type="number"
min={1}
required
placeholder="Custom amount in sats"
value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
const n = parseInt(e.target.value);
onChange(!isNaN(n) && n > 0 ? n : 0);
}}
/>
</div>
{customBudget && (
<div className="grid gap-2 mb-5">
<Label htmlFor="budget">Custom budget amount (sats)</Label>
<Input
id="budget"
name="budget"
type="number"
required
autoFocus
min={minAmount || 1}
value={value || ""}
onChange={(e) => {
onChange(parseInt(e.target.value));
}}
/>
</div>
)}
</>
);
}

View file

@ -1,4 +1,3 @@
import { XIcon } from "lucide-react";
import React from "react";
import { Label } from "src/components/ui/label";
import {
@ -13,40 +12,28 @@ import { BudgetRenewalType, validBudgetRenewals } from "src/types";
interface BudgetRenewalProps {
value: BudgetRenewalType;
onChange: (value: BudgetRenewalType) => void;
onClose?: () => void;
}
const BudgetRenewalSelect: React.FC<BudgetRenewalProps> = ({
value,
onChange,
onClose,
}) => {
return (
<>
<Label htmlFor="budget-renewal" className="block mb-2">
Budget Renewal
</Label>
<div className="flex gap-2 items-center text-muted-foreground mb-4 text-sm">
<Select value={value} onValueChange={onChange}>
<SelectTrigger id="budget-renewal" className="w-[150px] capitalize">
<SelectValue placeholder={value} />
</SelectTrigger>
<SelectContent className="capitalize">
{validBudgetRenewals.map((renewalOption) => (
<SelectItem key={renewalOption} value={renewalOption}>
{renewalOption}
</SelectItem>
))}
</SelectContent>
{onClose && (
<XIcon
className="cursor-pointer w-4 text-muted-foreground"
onClick={onClose}
/>
)}
</Select>
</div>
</>
<div className="flex gap-3 items-center mb-3">
<Label htmlFor="budget-renewal">Renewal</Label>
<Select value={value} onValueChange={onChange}>
<SelectTrigger id="budget-renewal" className="w-[150px] capitalize">
<SelectValue placeholder={value} />
</SelectTrigger>
<SelectContent className="capitalize">
{validBudgetRenewals.map((renewalOption) => (
<SelectItem key={renewalOption} value={renewalOption}>
{renewalOption}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
};

View file

@ -1,32 +0,0 @@
import { ChevronRightIcon } from "lucide-react";
import { ReactElement } from "react";
import { Link } from "react-router-dom";
import { Card } from "src/components/ui/card";
type Props = {
title: string | ReactElement;
description: string | ReactElement;
to: string;
};
function CardButton({ title, description, to }: Props) {
return (
<Link to={to}>
<Card className="p-4 shadow-none hover:bg-accent">
<div className="flex flex-row justify-between items-center">
<div>
<div className="font-medium flex flex-row items-center gap-2">
{title}
</div>
<div className="text-muted-foreground text-sm">{description}</div>
</div>
<div>
<ChevronRightIcon />
</div>
</div>
</Card>
</Link>
);
}
export default CardButton;

View file

@ -118,19 +118,24 @@ export function CloseChannelDialogContent({ alias, channel }: Props) {
<AlertDescription>
<div>
Closing this channel will move{" "}
<FormattedBitcoinAmount amount={channel.localBalance} /> in
this channel to your on-chain balance and reduce your
<FormattedBitcoinAmount
amountMsat={channel.localBalanceMsat}
/>{" "}
in this channel to your on-chain balance and reduce your
receive limit by{" "}
<FormattedBitcoinAmount amount={channel.remoteBalance} />.
<FormattedBitcoinAmount
amountMsat={channel.remoteBalanceMsat}
/>
.
</div>
</AlertDescription>
</Alert>
<div>
<p className="text-primary font-medium">Node ID</p>
<p className="font-medium text-foreground">Node ID</p>
<p className="break-all">{channel.remotePubkey}</p>
</div>
<div className="mt-4">
<p className="text-primary font-medium">Channel ID</p>
<p className="font-medium text-foreground">Channel ID</p>
<p className="break-all">{channel.id}</p>
</div>
</AlertDialogDescription>
@ -177,7 +182,7 @@ export function CloseChannelDialogContent({ alias, channel }: Props) {
<div className="grid gap-1.5">
<Label
htmlFor="normal"
className="text-primary font-medium cursor-pointer"
className="text-foreground cursor-pointer"
>
Normal Close (Recommended)
</Label>
@ -198,7 +203,7 @@ export function CloseChannelDialogContent({ alias, channel }: Props) {
<div className="grid gap-1.5">
<Label
htmlFor="force"
className="text-primary font-medium cursor-pointer"
className="text-foreground cursor-pointer"
>
Force Close
</Label>
@ -231,7 +236,9 @@ export function CloseChannelDialogContent({ alias, channel }: Props) {
<AlertDialogHeader>
<AlertDialogTitle>Channel closed successfully</AlertDialogTitle>
<AlertDialogDescription className="text-left">
<p className="text-primary font-medium">Funding Transaction Id</p>
<p className="font-medium text-foreground">
Funding Transaction Id
</p>
<div className="flex items-center justify-between gap-4">
<p className="break-all">{fundingTxId}</p>
<CopyIcon

View file

@ -1,28 +1,28 @@
import {
ArrowUpDown,
Code2,
CreditCard,
FileSignature,
FileText,
Home,
Info,
LayoutGrid,
Link,
Network,
Plug,
QrCode,
RefreshCw,
Send,
Settings,
Shield,
Shuffle,
SquareStack,
User,
UserPlus2,
Wallet,
ArrowUpDownIcon,
Code2Icon,
CreditCardIcon,
FileSignatureIcon,
FileTextIcon,
HomeIcon,
InfoIcon,
LayoutGridIcon,
LinkIcon,
NetworkIcon,
PlugIcon,
QrCodeIcon,
RefreshCwIcon,
SendIcon,
SettingsIcon,
ShieldIcon,
ShuffleIcon,
SquareStackIcon,
UserIcon,
UserPlus2Icon,
WalletIcon,
} from "lucide-react";
import * as React from "react";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import AppAvatar from "src/components/AppAvatar";
import {
@ -79,11 +79,11 @@ export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) {
onSelect={() => runCommand(() => navigate("/home"))}
keywords={["dashboard"]}
>
<Home />
<HomeIcon />
<span>Home</span>
</CommandItem>
<CommandItem onSelect={() => runCommand(() => navigate("/wallet"))}>
<Wallet />
<WalletIcon />
<span>Wallet</span>
</CommandItem>
<CommandItem
@ -91,30 +91,30 @@ export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) {
runCommand(() => navigate("/apps?tab=connected-apps"))
}
>
<Plug />
<PlugIcon />
<span>Connected Apps</span>
</CommandItem>
<CommandItem
onSelect={() => runCommand(() => navigate("/sub-wallets"))}
>
<CreditCard />
<CreditCardIcon />
<span>Sub-wallets</span>
</CommandItem>
<CommandItem
onSelect={() => runCommand(() => navigate("/channels"))}
keywords={["node", "liquidity", "channels"]}
>
<Network />
<NetworkIcon />
<span>Node</span>
</CommandItem>
<CommandItem onSelect={() => runCommand(() => navigate("/peers"))}>
<Network />
<NetworkIcon />
<span>Peers</span>
</CommandItem>
<CommandItem
onSelect={() => runCommand(() => navigate("/apps?tab=app-store"))}
>
<LayoutGrid />
<LayoutGridIcon />
<span>App Store</span>
</CommandItem>
</CommandGroup>
@ -123,25 +123,25 @@ export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) {
<CommandItem
onSelect={() => runCommand(() => navigate("/wallet/send"))}
>
<Send />
<SendIcon />
<span>Send Payment</span>
</CommandItem>
<CommandItem
onSelect={() => runCommand(() => navigate("/wallet/receive"))}
>
<QrCode />
<QrCodeIcon />
<span>Receive Payment</span>
</CommandItem>
<CommandItem
onSelect={() => runCommand(() => navigate("/wallet/swap"))}
>
<Shuffle />
<ShuffleIcon />
<span>Swap</span>
</CommandItem>
<CommandItem
onSelect={() => runCommand(() => navigate("/wallet/swap/auto"))}
>
<RefreshCw />
<RefreshCwIcon />
<span>Auto Swap</span>
</CommandItem>
<CommandItem
@ -149,21 +149,21 @@ export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) {
runCommand(() => navigate("/wallet/receive/invoice"))
}
>
<FileText />
<FileTextIcon />
<span>Create Invoice</span>
</CommandItem>
<CommandItem
onSelect={() =>
runCommand(() => navigate("/wallet/receive/onchain"))
runCommand(() => navigate("/wallet/receive?type=onchain"))
}
>
<Link />
<LinkIcon />
<span>Receive On-chain</span>
</CommandItem>
<CommandItem
onSelect={() => runCommand(() => navigate("/wallet/sign-message"))}
>
<FileSignature />
<FileSignatureIcon />
<span>Sign Message</span>
</CommandItem>
</CommandGroup>
@ -173,13 +173,13 @@ export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) {
onSelect={() => runCommand(() => navigate("/settings"))}
keywords={["theme", "fiat", "currency", "dark"]}
>
<Settings />
<SettingsIcon />
<span>Settings</span>
</CommandItem>
<CommandItem
onSelect={() => runCommand(() => navigate("/settings/backup"))}
>
<Shield />
<ShieldIcon />
<span>Backup</span>
</CommandItem>
<CommandItem
@ -187,47 +187,47 @@ export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) {
runCommand(() => navigate("/settings/alby-account"))
}
>
<User />
<UserIcon />
<span>Alby Account</span>
</CommandItem>
<CommandItem
keywords={["info"]}
onSelect={() => runCommand(() => navigate("/settings/about"))}
>
<Info />
<InfoIcon />
<span>About</span>
</CommandItem>
<CommandItem
onSelect={() => runCommand(() => navigate("/settings/developer"))}
>
<Code2 />
<Code2Icon />
<span>Developer Settings</span>
</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Quick Actions">
<CommandItem onSelect={() => runCommand(() => navigate("/apps/new"))}>
<Plug />
<PlugIcon />
<span>Connect New App</span>
</CommandItem>
<CommandItem
keywords={["New Sub-Wallet"]}
onSelect={() => runCommand(() => navigate("/sub-wallets/new"))}
>
<SquareStack />
<SquareStackIcon />
<span>Create Sub-wallet</span>
</CommandItem>
<CommandItem
keywords={["New Channel"]}
onSelect={() => runCommand(() => navigate("/channels/incoming"))}
>
<ArrowUpDown />
<ArrowUpDownIcon />
<span>Open Channel</span>
</CommandItem>
<CommandItem
onSelect={() => runCommand(() => navigate("/peers/new"))}
>
<UserPlus2 />
<UserPlus2Icon />
<span>Connect Peer</span>
</CommandItem>
</CommandGroup>

View file

@ -0,0 +1,32 @@
import { CoinsIcon, ExternalLinkIcon } from "lucide-react";
import { FixedFloatButton } from "src/components/FixedFloatButton";
import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert";
type CryptoSwapAlertProps = {
className?: string;
};
export function CryptoSwapAlert({ className }: CryptoSwapAlertProps) {
return (
<Alert className={className}>
<AlertTitle className="flex items-center gap-2">
<CoinsIcon className="h-4 w-4" />
Looking to pay to other Cryptocurrency?
</AlertTitle>
<AlertDescription className="text-xs gap-2 mt-1">
<p>
If you are trying to pay a non-Bitcoin payment destination, use
FixedFloat to swap and complete the payment across 70+ supported
cryptocurrencies.
</p>
<FixedFloatButton
from="BTCLN"
variant="outline"
className="text-foreground"
>
Pay with FixedFloat
<ExternalLinkIcon className="size-4" />
</FixedFloatButton>
</AlertDescription>
</Alert>
);
}

View file

@ -0,0 +1,528 @@
import * as React from "react";
import { toast } from "sonner";
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from "src/components/ui/field";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "src/components/ui/input-group";
import { Skeleton } from "src/components/ui/skeleton";
import { BITCOIN_DISPLAY_FORMAT_BIP177 } from "src/constants";
import { useBitcoinRate } from "src/hooks/useBitcoinRate";
import { useInfo } from "src/hooks/useInfo";
import { cn } from "src/lib/utils";
type CurrencyInputMode = "bitcoin" | "fiat";
type BitcoinDenomination = "sats" | "btc";
export type CurrencyInputContextRow = {
label: string;
amountSat?: number | null;
value?: React.ReactNode;
};
type CurrencyInputFieldProps = Omit<
React.ComponentProps<typeof InputGroupInput>,
"max" | "min" | "onChange" | "step" | "type" | "value"
> & {
contextRows?: CurrencyInputContextRow[];
description?: React.ReactNode;
error?: React.ReactNode;
label?: React.ReactNode;
maxSat?: number;
minSat?: number;
onValueSatChange: (valueSat: string) => void;
valueSat: string;
};
const SATS_PER_BTC = 100_000_000;
function getNumericValue(value: string | number | null | undefined) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : 0;
}
function getCurrencyFractionDigits(currency: string) {
try {
return new Intl.NumberFormat("en-US", {
currency,
style: "currency",
}).resolvedOptions().maximumFractionDigits;
} catch {
return 2;
}
}
function getCurrencySymbol(currency: string) {
try {
return (
new Intl.NumberFormat("en-US", {
currency,
style: "currency",
})
.formatToParts(0)
.find((part) => part.type === "currency")?.value || currency
);
} catch {
return currency;
}
}
function formatFiatValue(
amountSat: string | number | undefined,
rate: number | undefined,
currency: string | undefined
) {
if (!rate || !currency) {
return null;
}
return new Intl.NumberFormat("en-US", {
currency,
style: "currency",
}).format((getNumericValue(amountSat) / SATS_PER_BTC) * rate);
}
function formatFiatInput(amountSat: string, rate: number, currency: string) {
const fractionDigits = getCurrencyFractionDigits(currency);
const amountFiat = (getNumericValue(amountSat) / SATS_PER_BTC) * rate;
if (!amountFiat) {
return "";
}
return amountFiat.toFixed(fractionDigits);
}
function formatBitcoinValue(
amountSat: string | number | null | undefined,
displayFormat: string | undefined,
denomination: BitcoinDenomination = "sats"
) {
const { amount, unit } = formatBitcoinValueParts(
amountSat,
displayFormat,
denomination
);
if (unit === "₿") {
return `${unit}${amount}`;
}
return `${amount} ${unit}`;
}
function formatBitcoinValueParts(
amountSat: string | number | null | undefined,
displayFormat: string | undefined,
denomination: BitcoinDenomination = "sats"
) {
if (denomination === "btc") {
return {
amount: formatBtcDisplay(amountSat),
unit: "BTC",
};
}
const formattedAmount = new Intl.NumberFormat().format(
Math.floor(getNumericValue(amountSat))
);
if (displayFormat === BITCOIN_DISPLAY_FORMAT_BIP177) {
return {
amount: formattedAmount,
unit: "₿",
};
}
return {
amount: formattedAmount,
unit: "sats",
};
}
function BitcoinValueText({
amountSat,
denomination,
displayFormat,
}: {
amountSat: string | number | null | undefined;
denomination: BitcoinDenomination;
displayFormat: string | undefined;
}) {
const { amount, unit } = formatBitcoinValueParts(
amountSat,
displayFormat,
denomination
);
return (
<span className="inline-flex min-w-0 items-center justify-end gap-1">
{unit === "₿" && <span>{unit}</span>}
<span className="min-w-0 truncate">{amount}</span>
{unit !== "₿" && <span>{unit}</span>}
</span>
);
}
function formatBtcDisplay(amountSat: string | number | null | undefined) {
return (getNumericValue(amountSat) / SATS_PER_BTC).toFixed(8);
}
function formatBtcInput(amountSat: string | number | null | undefined) {
const amount = getNumericValue(amountSat);
if (!amount) {
return "";
}
return (amount / SATS_PER_BTC).toFixed(8);
}
export function CurrencyInputField({
className,
contextRows,
description,
disabled,
error,
id,
label = "Amount",
maxSat,
minSat,
onValueSatChange,
required,
valueSat,
...props
}: CurrencyInputFieldProps) {
const generatedId = React.useId();
const { data: info } = useInfo();
const { data: bitcoinRate, error: bitcoinRateError } = useBitcoinRate(
info?.currency
);
const [mode, setMode] = React.useState<CurrencyInputMode>("bitcoin");
const [fiatValue, setFiatValue] = React.useState("");
const [bitcoinDenomination, setBitcoinDenomination] =
React.useState<BitcoinDenomination>("sats");
const [btcValue, setBtcValue] = React.useState("");
const currency = info?.currency || "USD";
const rate = bitcoinRate?.rate_float;
const canUseFiat = currency !== "SATS" && !!rate && !bitcoinRateError;
const bitcoinUnit =
info?.bitcoinDisplayFormat === BITCOIN_DISPLAY_FORMAT_BIP177 ? "₿" : "sats";
const invalid =
props["aria-invalid"] === true ||
props["aria-invalid"] === "true" ||
!!error;
const inputId = id || generatedId;
const isFiatMode = mode === "fiat";
const isBtcDenominated = bitcoinDenomination === "btc";
const inputValue = isFiatMode
? fiatValue
: isBtcDenominated
? btcValue
: valueSat;
const alternateBitcoinValue = formatBitcoinValueParts(
valueSat,
info?.bitcoinDisplayFormat,
bitcoinDenomination
);
const alternateValue = isFiatMode
? formatBitcoinValue(
valueSat,
info?.bitcoinDisplayFormat,
bitcoinDenomination
)
: formatFiatValue(valueSat, rate, currency);
React.useEffect(() => {
if (mode === "fiat" && !valueSat) {
setFiatValue("");
}
}, [mode, valueSat]);
React.useEffect(() => {
if (mode === "bitcoin" && isBtcDenominated && !valueSat) {
setBtcValue("");
}
}, [isBtcDenominated, mode, valueSat]);
function handleToggleMode() {
if (disabled) {
return;
}
if (mode === "bitcoin") {
if (!canUseFiat) {
return;
}
setFiatValue(formatFiatInput(valueSat, rate, currency));
setMode("fiat");
return;
}
if (isBtcDenominated) {
setBtcValue(formatBtcInput(valueSat));
}
setMode("bitcoin");
}
function handleAlternateValueClick() {
if (disabled || isFiatMode || !canUseFiat) {
return;
}
handleToggleMode();
}
function handleToggleBitcoinDenomination() {
if (disabled) {
return;
}
if (isBtcDenominated) {
setBitcoinDenomination("sats");
return;
}
setBtcValue(formatBtcInput(valueSat));
setBitcoinDenomination("btc");
}
function handleChangeMode(event: React.ChangeEvent<HTMLInputElement>) {
// clear any custom validity set via onInvalid so the field re-validates
// on the next submit
event.currentTarget.setCustomValidity("");
const nextValue = event.target.value.trim();
if (mode === "bitcoin") {
if (!isBtcDenominated && nextValue.includes(".")) {
setBitcoinDenomination("btc");
setBtcValue(nextValue);
toast("Switched to BTC for decimal amount");
if (!nextValue) {
onValueSatChange("");
return;
}
const amountBtc = Number(nextValue);
if (!Number.isFinite(amountBtc)) {
onValueSatChange("");
return;
}
onValueSatChange(
Math.max(0, Math.round(amountBtc * SATS_PER_BTC)).toString()
);
return;
}
if (isBtcDenominated) {
setBtcValue(nextValue);
if (!nextValue) {
onValueSatChange("");
return;
}
const amountBtc = Number(nextValue);
if (!Number.isFinite(amountBtc)) {
onValueSatChange("");
return;
}
onValueSatChange(
Math.max(0, Math.round(amountBtc * SATS_PER_BTC)).toString()
);
return;
}
onValueSatChange(nextValue);
return;
}
setFiatValue(nextValue);
if (!nextValue || !rate) {
onValueSatChange("");
return;
}
const amountFiat = Number(nextValue);
if (!Number.isFinite(amountFiat)) {
onValueSatChange("");
return;
}
onValueSatChange(
Math.max(0, Math.round((amountFiat / rate) * SATS_PER_BTC)).toString()
);
}
function getModeBound(amountSat: number | undefined) {
if (amountSat === undefined) {
return undefined;
}
if (!isFiatMode) {
if (isBtcDenominated) {
return amountSat / SATS_PER_BTC;
}
return amountSat;
}
if (!rate) {
return amountSat;
}
return ((amountSat / SATS_PER_BTC) * rate).toFixed(
getCurrencyFractionDigits(currency)
);
}
return (
<Field
className={cn("w-full min-w-0", className)}
data-disabled={disabled || undefined}
data-invalid={invalid || undefined}
>
{label && <FieldLabel htmlFor={inputId}>{label}</FieldLabel>}
<InputGroup className="h-9 min-w-0 overflow-hidden has-[>[data-align=inline-start]]:[&>input]:pl-1">
<InputGroupInput
{...props}
id={inputId}
aria-invalid={invalid || undefined}
autoComplete="off"
className={cn(
"sensitive slashed-zero min-w-0 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
)}
disabled={disabled}
inputMode="decimal"
max={getModeBound(maxSat)}
min={getModeBound(minSat)}
onChange={handleChangeMode}
placeholder={
isFiatMode ? "0.00" : isBtcDenominated ? "0.00000000" : "0"
}
required={required}
step={isFiatMode ? "any" : isBtcDenominated ? 0.00000001 : 1}
type="number"
value={inputValue}
/>
<InputGroupAddon align="inline-start">
{isFiatMode ? (
<InputGroupButton
aria-label="Enter amount in bitcoin"
disabled={disabled}
onClick={handleToggleMode}
size="xs"
className="h-full rounded-none bg-transparent pl-2 pr-0 text-muted-foreground hover:bg-transparent hover:text-foreground focus-visible:text-foreground focus-visible:ring-0"
title="Enter amount in bitcoin"
>
{getCurrencySymbol(currency)}
</InputGroupButton>
) : (
<InputGroupButton
aria-label={
isBtcDenominated
? "Display bitcoin amounts in satoshis"
: "Display bitcoin amounts in BTC"
}
aria-pressed={isBtcDenominated}
disabled={disabled}
onClick={handleToggleBitcoinDenomination}
size="xs"
className="h-full rounded-none bg-transparent pl-2 pr-0 text-muted-foreground hover:bg-transparent hover:text-foreground focus-visible:text-foreground focus-visible:ring-0"
title={
isBtcDenominated
? "Display bitcoin amounts in satoshis"
: "Display bitcoin amounts in BTC"
}
>
{isBtcDenominated ? "BTC" : bitcoinUnit}
</InputGroupButton>
)}
</InputGroupAddon>
<InputGroupAddon
align="inline-end"
className="mr-0 min-w-0 self-stretch py-0 pr-4"
>
{isFiatMode ? (
<InputGroupButton
aria-label={
isBtcDenominated
? "Display bitcoin amounts in satoshis"
: "Display bitcoin amounts in BTC"
}
aria-pressed={isBtcDenominated}
disabled={disabled}
onClick={handleToggleBitcoinDenomination}
size="xs"
className="sensitive slashed-zero h-full min-w-0 justify-end truncate rounded-none bg-transparent px-0.5 text-muted-foreground tabular-nums hover:bg-transparent hover:text-foreground focus-visible:text-foreground focus-visible:ring-0"
title={
isBtcDenominated
? "Display bitcoin amounts in satoshis"
: "Display bitcoin amounts in BTC"
}
>
{alternateBitcoinValue.unit === "₿" && (
<span>{alternateBitcoinValue.unit}</span>
)}
<span className="min-w-0 truncate">
{alternateBitcoinValue.amount}
</span>
{alternateBitcoinValue.unit !== "₿" && (
<span>{alternateBitcoinValue.unit}</span>
)}
</InputGroupButton>
) : (
<InputGroupButton
aria-label="Enter amount in fiat"
disabled={disabled || !canUseFiat}
onClick={handleAlternateValueClick}
size="xs"
className="sensitive slashed-zero h-full min-w-0 max-w-28 justify-end truncate rounded-none bg-transparent px-1 text-muted-foreground tabular-nums hover:bg-transparent hover:text-foreground focus-visible:text-foreground focus-visible:ring-0 sm:max-w-none"
title="Enter amount in fiat"
>
{alternateValue ?? <Skeleton className="h-4 w-16" />}
</InputGroupButton>
)}
</InputGroupAddon>
</InputGroup>
{!!contextRows?.length && (
<div className="flex min-w-0 cursor-default flex-col gap-1 text-sm text-muted-foreground">
{contextRows.map((row) => (
<div
className="flex min-w-0 items-center justify-between gap-3"
key={row.label}
>
<span className="truncate">{row.label}:</span>
<span className="sensitive slashed-zero min-w-0 max-w-[55%] truncate text-right tabular-nums">
{row.value ?? (
<BitcoinValueText
amountSat={row.amountSat}
displayFormat={info?.bitcoinDisplayFormat}
denomination={bitcoinDenomination}
/>
)}
</span>
</div>
))}
</div>
)}
{description && <FieldDescription>{description}</FieldDescription>}
{error && <FieldError>{error}</FieldError>}
</Field>
);
}

View file

@ -53,7 +53,7 @@ export function DisconnectPeerDialogContent({ peer }: Props) {
Are you sure you wish to disconnect from{" "}
{peerDetails?.alias || "this peer"}?
</p>
<p className="text-primary font-medium mt-4">Peer Pubkey</p>
<p className="font-medium text-foreground mt-4">Peer Pubkey</p>
<p className="break-all">{peer.nodeId}</p>
</div>
</AlertDialogDescription>

View file

@ -3,37 +3,44 @@ import React from "react";
import { LinkButton } from "src/components/ui/custom/link-button";
import { cn } from "src/lib/utils";
interface Props {
type Variant = "dashed" | "muted" | "none";
type Props = {
icon: LucideIcon;
title: string;
description: string;
buttonText: string;
buttonLink: string;
showButton?: boolean;
showBorder?: boolean;
}
variant?: Variant;
} & (
| { buttonText: string; buttonLink: string }
| { buttonText?: never; buttonLink?: never }
);
const variantClasses: Record<Variant, string> = {
dashed: "shadow-xs border border-dashed",
muted: "bg-muted",
none: "",
};
const EmptyState: React.FC<Props> = ({
icon: Icon,
title: message,
description: subMessage,
variant = "muted",
buttonText,
buttonLink,
showButton = true,
showBorder = true,
}) => {
return (
<div
className={cn(
"flex flex-1 items-center justify-center rounded-lg p-8",
showBorder && "shadow-xs border border-dashed"
variantClasses[variant]
)}
>
<div className="flex flex-col items-center gap-1 text-center max-w-sm">
<Icon className="w-10 h-10 text-muted-foreground" />
<h3 className="mt-4 text-lg font-semibold">{message}</h3>
<p className="text-sm text-muted-foreground">{subMessage}</p>
{showButton && (
{buttonText && buttonLink && (
<LinkButton to={buttonLink} className="mt-4">
{buttonText}
</LinkButton>

View file

@ -32,7 +32,7 @@ export function ExecuteCustomNodeCommandDialogContent({
null,
2
);
} catch (error) {
} catch {
// ignore unexpected json
}
@ -60,17 +60,24 @@ export function ExecuteCustomNodeCommandDialogContent({
}
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
executeCommand();
};
return (
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Execute Custom Node Command</AlertDialogTitle>
<AlertDialogDescription className="text-left">
<Textarea
className="h-36 font-mono"
value={command}
onChange={(e) => setCommand(e.target.value)}
placeholder="commandname --arg1=value1"
/>
<form id="execute-command-form" onSubmit={handleSubmit}>
<Textarea
className="h-36 font-mono"
value={command}
onChange={(e) => setCommand(e.target.value)}
placeholder="commandname --arg1=value1"
/>
</form>
<p className="mt-2">Available commands</p>
<Textarea
readOnly
@ -80,11 +87,13 @@ export function ExecuteCustomNodeCommandDialogContent({
/>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="mt-4">
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setCommand("")}>
Cancel
</AlertDialogCancel>
<AlertDialogAction onClick={executeCommand}>Execute</AlertDialogAction>
<AlertDialogAction type="submit" form="execute-command-form">
Execute
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
);

Some files were not shown because too many files have changed in this diff Show more