From 162965300e6c93e900002a384f990f0c76526fb2 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Mon, 11 May 2026 21:27:50 +0700 Subject: [PATCH 001/136] docs: add README for keys (#2333) --- scripts/keys/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 scripts/keys/README.md diff --git a/scripts/keys/README.md b/scripts/keys/README.md new file mode 100644 index 00000000..e2a3c191 --- /dev/null +++ b/scripts/keys/README.md @@ -0,0 +1,12 @@ +# Release verification keys + +This directory contains all keys that are currently signing Alby Hub releases. + +The name of the file must match exactly the suffix that user is going to use +when signing a release. +For example, if the key is called `im-adithya.asc` then that user should upload a +signature file called `manifest-im-adithya.txt.asc`. + +In addition to adding the key file here there is a main `mainfest.txt.asc` file +that is used by the systemd install/update scripts. This must be provided by a +single verified signing user per release. From 55d665db7424c6efef052db2b995916cc12cb1e4 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Thu, 14 May 2026 18:37:37 +0700 Subject: [PATCH 002/136] fix: check lnclient is nil in GetPermittedMethods (#2343) this caused a possible panic on shutdown --- nip47/permissions/permissions.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nip47/permissions/permissions.go b/nip47/permissions/permissions.go index 0c0f0c56..3dc19c03 100644 --- a/nip47/permissions/permissions.go +++ b/nip47/permissions/permissions.go @@ -61,6 +61,10 @@ func (svc *permissionsService) HasPermission(app *db.App, scope string) (result } func (svc *permissionsService) GetPermittedMethods(app *db.App, lnClient lnclient.LNClient) []string { + if lnClient == nil { + return []string{} + } + appPermissions := []db.AppPermission{} svc.db.Where("app_id = ?", app.ID).Find(&appPermissions) scopes := make([]string, 0, len(appPermissions)) From a0d3da28036ea7ea0ac7bd96b62cfca9a7542f8e Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Thu, 14 May 2026 20:34:59 +0700 Subject: [PATCH 003/136] 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 --------- Co-authored-by: Adithya Vardhan --- apps/apps_service.go | 10 ++++++-- go.mod | 2 +- go.sum | 4 ++-- service/delete_app_consumer.go | 43 +++++++++++++++++----------------- 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/apps/apps_service.go b/apps/apps_service.go index c0bbda7c..3a20ac76 100644 --- a/apps/apps_service.go +++ b/apps/apps_service.go @@ -184,11 +184,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 diff --git a/go.mod b/go.mod index 0801c5e2..41e688f5 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6 github.com/btcsuite/btcd/btcutil v1.1.6 github.com/elnosh/gonuts v0.4.2 - github.com/getAlby/go-nostr v0.0.0-20260509070347-31e205cac904 + github.com/getAlby/go-nostr v0.0.0-20260513161014-22fb7840c7a4 github.com/getAlby/ldk-node-go v0.0.0-20260424111754-3690cdb3031c github.com/go-gormigrate/gormigrate/v2 v2.1.5 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index cb3e0891..0af4021e 100644 --- a/go.sum +++ b/go.sum @@ -189,8 +189,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/getAlby/go-nostr v0.0.0-20260509070347-31e205cac904 h1:UvdEf2rj3EXGR/HkMz3eZI9x1hBdSSlTLutyIHLYy8o= -github.com/getAlby/go-nostr v0.0.0-20260509070347-31e205cac904/go.mod h1:BtlkV9evCTjpY0YeFhoNgycp7XNFbnfVXJPoykp+NtM= +github.com/getAlby/go-nostr v0.0.0-20260513161014-22fb7840c7a4 h1:Z93wPXKIMY4Emr+zDz0R0NrSg1FEVGrzcqqomuSrmko= +github.com/getAlby/go-nostr v0.0.0-20260513161014-22fb7840c7a4/go.mod h1:BtlkV9evCTjpY0YeFhoNgycp7XNFbnfVXJPoykp+NtM= github.com/getAlby/ldk-node-go v0.0.0-20260424111754-3690cdb3031c h1:ikai5+taiPSgbaocdVMdxPkmOhnXs5V/gCX+J/P8iRw= github.com/getAlby/ldk-node-go v0.0.0-20260424111754-3690cdb3031c/go.mod h1:8BRjtKcz8E0RyYTPEbMS8VIdgredcGSLne8vHDtcRLg= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/service/delete_app_consumer.go b/service/delete_app_consumer.go index 8bb55dd0..1a2b7ada 100644 --- a/service/delete_app_consumer.go +++ b/service/delete_app_consumer.go @@ -27,43 +27,42 @@ func (s *deleteAppConsumer) ConsumeEvent(ctx context.Context, event *events.Even logger.Logger.WithField("event", event).Error("Failed to cast event.Properties to map") return } + // Note: for legacy apps the deleted app's WalletPubkey is empty and will + // not match the master key used for the legacy app subscription, so the + // subscription is preserved for any remaining legacy apps. + walletPubKey, _ := properties["walletPubkey"].(string) + if walletPubKey == "" || walletPubKey != s.walletPubkey { + return + } id, ok := properties["id"].(uint) if !ok { logger.Logger.WithField("event", event).Error("missing id in properties event") return } + // no longer need to listen to events for this wallet + s.cancelSubscription() + + // remove this consumer as subscriber in eventPublisher + s.svc.eventPublisher.RemoveSubscriber(s) + walletPrivKey, err := s.svc.keys.GetAppWalletKey(id) if err != nil { logger.Logger.WithError(err).WithField("id", id).Error("Failed to calculate app wallet priv key") return } - walletPubKey, err := nostr.GetPublicKey(walletPrivKey) + + // try to delete info event from relays (non-critical if it fails) + // get nip47 event info for this app wallet key + nip47InfoEvent, err := s.svc.GetNip47Service().GetNip47Info(ctx, s.pool, s.walletPubkey) if err != nil { - logger.Logger.WithError(err).WithField("id", id).Error("Failed to calculate app wallet pub key") + logger.Logger.WithError(err).Error("Could not get nip47 info event") return } - // Note: for legacy apps this check will always return false as the wallet pubkey - // generated by the id will not match the master key which is used for all legacy apps - if s.walletPubkey == walletPubKey { - // no longer need to listen to events for this wallet - s.cancelSubscription() - - // remove this consumer as subscriber in eventPublisher - s.svc.eventPublisher.RemoveSubscriber(s) - - // try to delete info event from relays (non-critical if it fails) - // get nip47 event info for this app wallet key - nip47InfoEvent, err := s.svc.GetNip47Service().GetNip47Info(ctx, s.pool, s.walletPubkey) + if nip47InfoEvent != nil { + err = s.svc.nip47Service.PublishNip47InfoDeletion(ctx, s.pool, walletPubKey, walletPrivKey, nip47InfoEvent.ID) if err != nil { - logger.Logger.WithError(err).Error("Could not get nip47 info event") - return - } - if nip47InfoEvent != nil { - err = s.svc.nip47Service.PublishNip47InfoDeletion(ctx, s.pool, walletPubKey, walletPrivKey, nip47InfoEvent.ID) - if err != nil { - logger.Logger.WithError(err).WithField("event", event).Error("Failed to publish nip47 info deletion") - } + logger.Logger.WithError(err).WithField("event", event).Error("Failed to publish nip47 info deletion") } } } From 3a2f935a75d3abbe9fe3175f1307892b54efab86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= <100827540+reneaaron@users.noreply.github.com> Date: Thu, 21 May 2026 13:04:35 +0200 Subject: [PATCH 004/136] feat: add Hermes agent to AI page (#2358) Closes #2357 Co-authored-by: Claude Opus 4.7 (1M context) --- frontend/src/assets/suggested-apps/hermes.png | Bin 0 -> 9421 bytes frontend/src/components/AppAvatar.tsx | 2 ++ frontend/src/screens/ai/AI.tsx | 8 ++++++++ 3 files changed, 10 insertions(+) create mode 100644 frontend/src/assets/suggested-apps/hermes.png diff --git a/frontend/src/assets/suggested-apps/hermes.png b/frontend/src/assets/suggested-apps/hermes.png new file mode 100644 index 0000000000000000000000000000000000000000..dc8a6ec689f1ce0b92792d4b977532bfc1799c63 GIT binary patch literal 9421 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 T^grUj00000NkvXXu0mjfxN?SF literal 0 HcmV?d00001 diff --git a/frontend/src/components/AppAvatar.tsx b/frontend/src/components/AppAvatar.tsx index 526c1be4..cd658007 100644 --- a/frontend/src/components/AppAvatar.tsx +++ b/frontend/src/components/AppAvatar.tsx @@ -4,6 +4,7 @@ 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"; @@ -17,6 +18,7 @@ import { App } from "src/types"; const agentLogos: Record = { claude: claudeLogo, goose: gooseLogo, + hermes: hermesLogo, openclaw: openclawLogo, cursor: cursorLogo, codex: codexLogo, diff --git a/frontend/src/screens/ai/AI.tsx b/frontend/src/screens/ai/AI.tsx index dcc41662..c2970ab6 100644 --- a/frontend/src/screens/ai/AI.tsx +++ b/frontend/src/screens/ai/AI.tsx @@ -29,6 +29,7 @@ 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 payperqLogo from "src/assets/suggested-apps/payperq.png"; @@ -82,6 +83,13 @@ const agents: Agent[] = [ description: "Open-source personal AI assistant", setupUrl: "", }, + { + id: "hermes", + name: "Hermes", + logo: hermesLogo, + description: "Self-improving open-source AI agent by Nous Research", + setupUrl: "", + }, { id: "cursor", name: "Cursor", From 12a3b114ff4e767db537a553fefbcb95d04f347b Mon Sep 17 00:00:00 2001 From: daywalker90 <8257956+daywalker90@users.noreply.github.com> Date: Sat, 23 May 2026 08:55:44 +0200 Subject: [PATCH 005/136] 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 --- lnclient/cln/cln.go | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/lnclient/cln/cln.go b/lnclient/cln/cln.go index f7dd1730..acd11a1e 100644 --- a/lnclient/cln/cln.go +++ b/lnclient/cln/cln.go @@ -994,25 +994,25 @@ func (c *CLNService) GetBalances(ctx context.Context, includeInactiveChannels bo if ch.SpendableMsat != nil { spendable := int64(ch.SpendableMsat.Msat) - lightning.TotalSpendable += spendable + lightning.TotalSpendableMsat += spendable - if spendable > lightning.NextMaxSpendable { - lightning.NextMaxSpendable = spendable + if spendable > lightning.NextMaxSpendableMsat { + lightning.NextMaxSpendableMsat = spendable } } if ch.ReceivableMsat != nil { receivable := int64(ch.ReceivableMsat.Msat) - lightning.TotalReceivable += receivable + lightning.TotalReceivableMsat += receivable - if receivable > lightning.NextMaxReceivable { - lightning.NextMaxReceivable = receivable + if receivable > lightning.NextMaxReceivableMsat { + lightning.NextMaxReceivableMsat = receivable } } } - lightning.NextMaxSpendableMPP = lightning.TotalSpendable - lightning.NextMaxReceivableMPP = lightning.TotalReceivable + lightning.NextMaxSpendableMPPMsat = lightning.TotalSpendableMsat + lightning.NextMaxReceivableMPPMsat = lightning.TotalReceivableMsat return &lnclient.BalancesResponse{ Onchain: *onchainBalance, @@ -1274,24 +1274,24 @@ func (c *CLNService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainBa } amt := satInt64(utxo.AmountMsat) - balances.Total += amt + balances.TotalSat += amt if utxo.Reserved { - balances.Reserved += amt + balances.ReservedSat += amt reservedSats += amt } switch utxo.Status { case clngrpc.ListfundsOutputs_CONFIRMED: if !utxo.Reserved { - balances.Spendable += amt + balances.SpendableSat += amt } case clngrpc.ListfundsOutputs_UNCONFIRMED: balances.PendingSweepBalancesDetails = append( balances.PendingSweepBalancesDetails, lnclient.PendingBalanceDetails{ - Amount: uint64(amt), + AmountSat: uint64(amt), FundingTxId: hex.EncodeToString(utxo.Txid), FundingTxVout: utxo.Output, }, @@ -1305,13 +1305,13 @@ func (c *CLNService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainBa } amt := sat(ch.OurAmountMsat) - balances.PendingBalancesFromChannelClosures += amt + balances.PendingBalancesFromChannelClosuresSat += amt chanIdStr := hex.EncodeToString(ch.ChannelId) detail := lnclient.PendingBalanceDetails{ ChannelId: chanIdStr, NodeId: hex.EncodeToString(ch.PeerId), - Amount: amt, + AmountSat: amt, } if pc, ok := chByID[chanIdStr]; ok { @@ -2128,6 +2128,7 @@ func (c *CLNService) MakeOffer(ctx context.Context, description string) (string, }).Debug("Make Offer") req := &clngrpc.OfferRequest{ + Amount: "any", Description: &description, } resp, err := c.client.Offer(ctx, req) From 4a7123a4b977ff36fb9fefad7cdbb3e5ef43056d Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Sat, 23 May 2026 15:26:33 +0700 Subject: [PATCH 006/136] 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 --- .../src/assets/suggested-apps/lendaswap.png | Bin 11296 -> 0 bytes frontend/src/assets/suggested-apps/satora.png | Bin 0 -> 2368 bytes .../connections/SuggestedAppData.tsx | 22 +++++++++++------- .../components/connections/SuggestedApps.tsx | 7 +++++- 4 files changed, 19 insertions(+), 10 deletions(-) delete mode 100644 frontend/src/assets/suggested-apps/lendaswap.png create mode 100644 frontend/src/assets/suggested-apps/satora.png diff --git a/frontend/src/assets/suggested-apps/lendaswap.png b/frontend/src/assets/suggested-apps/lendaswap.png deleted file mode 100644 index 6987c6ac0c8edceb309854a63186da009e0123be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11296 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 diff --git a/frontend/src/assets/suggested-apps/satora.png b/frontend/src/assets/suggested-apps/satora.png new file mode 100644 index 0000000000000000000000000000000000000000..f94f9ab5bce1bbade7982d29057dc9b21d0ce9be GIT binary patch literal 2368 zcmV-G3BUGP@U7RTSKrsNjac72eY(t0?C^} zAbB9>1ZIGY2+U@fNn}J~cfFvRA8txbvPm|(tDFBH5X7d;VoCbpy;mPyRiHVc+E^oS{?<;g)DDBtQS9icKKAa zdLrySovj$rV4^w+ooJzV3yyR0_oW}}Y4>SbGAoM_qF>mMV&bBksvPC)Wg*&FEz4nJZJos1 z$#@ir1PdM5E{&D4%#$P`oD|sEwow|ZWbxCDc-1+Tqz@Y-jTN$(!u9vhK0YUr-L(5+ z8%*@lVQ1e4X;PQv@ZzhdM7o-%!ZmJ8A*?m+r9FB|O%|k_@ku*1r6LQW$>gLNWfGO8 zJ*jrN!$dEV{McGZ{?U@x(ip&MGgM9us98b~chqODc&EF=@vY z_Z*_ci}`uF;+?nC`s|sxEZ;Z&wnHRuQJ>6=qWSqAHon@K^#{!~4ZXf7e~0=cL6~V4Dv+bF*9e0Cf#B;fLc`Qt;akmYk!9umW*R74X zM@q5QED*&G%+LRFapCywht*i?#8_p4DE815CeAivlf|!5Ac|F(-^ZK(l&f*I7mF-r zc#LA!PK0Ue%|-dUINF+879)xl#?}Ih5uz7y%)?AI8fwoR%G+r>0g{5hsVtu!P5kaj z-`L(_;GSb^z=yEoq`OWQNF|c*hVWbNIivNr%)E$@1Hs%N;BVW)t&!yS%e7 zQT;M@8hYI*Z?RBp!!eK+~@vqRYU@ zf?8!x84_JWBMYL+d^2cd6dG9&Rpy&(twU$(!_$!9%#?XXu{;mk^KwfTe+>w;wewE# z^3I)G(dCvbN@z2&E&*#1(qG?vt*^jEVGDxXyJ~$ATwv^rEG_a-tAHLiQ&FERNI@XT z%~aG&Lr6g&j9@D2MHZwW5XQ%ah2Bm&75KxG)OrN0MOgK`_*AoCA`XNgtotL&_rH`^ zF%bs_P^>oAd^dbqJO}5hfWa(!?RwUig+0gu2BWF%M_U#}kJ9cCuo%@I{P?m6A+ms> z_%#cg;#Mm;V=~3Nm>>%njWE}g%L}!%I|A%SIjZTO&vJNC{vTGK!4R62<+Cr^Pb6aa42I)ppY{`p zn%K#CcA{T@q#Pw&(rz_OIpM`%& zu}{EKlt?e@d^&)T1*r(!Ld&PYPfwQXKUznr^$1vt=Qy_`%l1mG$wY`QAjrvf;F7Ao z%}<&HbqnYrnqCF}-Y1K9=K`H_P`ce>uJqm(U1Slj0QJ5^9RD|7g zbBsR`_u%YSNpedTlZr@WKe3(x>j+N7^}QbUqN@rhbmy3sa`wVr50%9fHYjxG6BatS zS=e?N_>h^fgxUr2&7i14!(^!}y+1*8nLBO`mxBy_d==4UzHr&u9`^O2vLL$555sDe zE+Zd|Z;h}Gj#1Q4TQaCrVKpWGq?Q z;R@*}7mSg9x{Q6MTxI#+N2E1YrL&Bt{je1RjG3b8vh#S{^wVYHqfJr2WD(w;R1})_ICj5&!R&4((87C_<1J<7yAY#;eNm*;X74#U=~2 zg)s#$E)};0;acVX>EpPXi}mb&_o}tgJ%0?OjBw4p{(K|OCS#Rld!^QdFc#1eWqai$ z)}@(P(@+r9FsTt+jdGKjT_NLl7{lsh(X>A+QE1lJ-c|0u--R&A;LoExID0Y6&zM;n z3UYYyoAc!)NNG+NKN&TwcUX(#C6OPE9dv$juZBfXE$P%Qp`)(IAzwMOM zVrQz&N^xVAEM1H=3*uGh@fjr}iLmTdmfl-;Ac2*#beX3}W3wb(td^xqR~jLHlCwhC zKkU&}g^|Zy*gz&^31UW1N@z37Dau3xo3b;L@)yZ+Nqb-LELXgnOjH+5 z@03HeYjTDj

Open{" "} - LendaSwap + Satora {" "} in your browser

@@ -2491,7 +2494,7 @@ export const appStoreApps: AppStoreApp[] = ( finalizeGuide: ( <>
-

In LendaSwap

+

In Satora

  • Click the{" "} @@ -2533,6 +2536,7 @@ export const getAppStoreApp = (app: App) => { return appStoreApps.find( (suggestedApp) => suggestedApp.id === (app.metadata?.app_store_app_id ?? "") || - app.name.includes(suggestedApp.title) + app.name.includes(suggestedApp.title) || + suggestedApp.legacyTitles?.some((title) => app.name.includes(title)) ); }; diff --git a/frontend/src/components/connections/SuggestedApps.tsx b/frontend/src/components/connections/SuggestedApps.tsx index 239c4020..a097c081 100644 --- a/frontend/src/components/connections/SuggestedApps.tsx +++ b/frontend/src/components/connections/SuggestedApps.tsx @@ -28,7 +28,12 @@ function AppCard(app: AppStoreApp) { />
    {app.title} - {app.description} + + {app.description} + {app.legacyTitles?.length + ? ` (Previously ${app.legacyTitles.join(", ")})` + : ""} +
From dd04326f762982c978a2bf48722c56075664ec66 Mon Sep 17 00:00:00 2001 From: hermes-alby Date: Sat, 23 May 2026 15:26:54 +0700 Subject: [PATCH 007/136] chore: remove shut down suggested apps (#2372) Co-authored-by: Hermes Agent --- .../suggested-apps/paper-scissors-hodl.png | Bin 21293 -> 0 bytes .../suggested-apps/satoshis-auction-house.png | Bin 20599 -> 0 bytes .../connections/SuggestedAppData.tsx | 84 ------------------ 3 files changed, 84 deletions(-) delete mode 100644 frontend/src/assets/suggested-apps/paper-scissors-hodl.png delete mode 100644 frontend/src/assets/suggested-apps/satoshis-auction-house.png diff --git a/frontend/src/assets/suggested-apps/paper-scissors-hodl.png b/frontend/src/assets/suggested-apps/paper-scissors-hodl.png deleted file mode 100644 index 4fe1fd0c90a629a534b4646b23925176d1d33bb9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21293 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 ovWwaw2@~07*qoM6N<$f-bE>t^fc4 diff --git a/frontend/src/assets/suggested-apps/satoshis-auction-house.png b/frontend/src/assets/suggested-apps/satoshis-auction-house.png deleted file mode 100644 index f132c37eaf89538ae2163a3a0ea293670199fc1a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20599 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 -

- Open{" "} - - Paper Scissors HODL - {" "} - in your browser -

- - ), - finalizeGuide: ( - <> -
-

In Paper Scissors HODL

-
    -
  • Start playing until the Bitcoin Connect screen pops up
  • -
  • - Choose{" "} - - Nostr Wallet Connect - -
  • -
  • Paste the connection secret from Alby Hub
  • -
-
- - ), - categories: ["games"], - }, { id: "pullthatupjamie-ai", title: "Pull That Up Jamie!", @@ -2320,47 +2277,6 @@ export const appStoreApps: AppStoreApp[] = ( ), categories: ["social-media"], }, - { - id: "satoshis-auction-house", - title: "Satoshi's Auction House", - description: "Bitcoin-powered auction platform", - webLink: "https://satoshisauction.house", - logo: satoshisauctionhouse, - extendedDescription: - "Buy and sell items through Bitcoin-powered auctions directly from your Hub", - installGuide: ( - <> -

- Open{" "} - - Satoshi's Auction House - {" "} - in your browser -

- - ), - finalizeGuide: ( - <> -
-

In Satoshi's Auction House

-
    -
  • - Click on the Hamburger menu on the top right and click{" "} - Settings -
  • -
  • - Paste the connection secret from Alby Hub into the receive-only - connection secret field -
  • -
-
- - ), - categories: ["shopping"], - }, { id: "takemysats", title: "Take My Sats", From ee001d16b4378999f93c792db27b69387866ebff Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Sat, 23 May 2026 15:29:35 +0700 Subject: [PATCH 008/136] fix: missing error handling in create connection controller (#2335) --- nip47/controllers/create_connection_controller.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/nip47/controllers/create_connection_controller.go b/nip47/controllers/create_connection_controller.go index 45f46f4b..9d515ac7 100644 --- a/nip47/controllers/create_connection_controller.go +++ b/nip47/controllers/create_connection_controller.go @@ -104,6 +104,17 @@ func (controller *nip47Controller) HandleCreateConnectionEvent(ctx context.Conte scopes, err := permissions.RequestMethodsToScopes(params.RequestMethods) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + }).WithError(err).Error("Failed to convert request methods to scopes") + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: mapNip47Error(err), + }, nostr.Tags{}) + return + } + supportedNotificationTypes := controller.lnClient.GetSupportedNIP47NotificationTypes() if len(params.NotificationTypes) > 0 { if slices.ContainsFunc(params.NotificationTypes, func(method string) bool { From 3bb36611aeada752804e791e4d7ced8dc7a93cf4 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Sat, 23 May 2026 15:30:13 +0700 Subject: [PATCH 009/136] fix: error log when failing to request delete lightning address endpoint (#2338) --- alby/alby_oauth_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alby/alby_oauth_service.go b/alby/alby_oauth_service.go index 7c264d9b..165f5b34 100644 --- a/alby/alby_oauth_service.go +++ b/alby/alby_oauth_service.go @@ -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 } From d58b9f3ead36a60440833660f61170ce801f4266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= <100827540+reneaaron@users.noreply.github.com> Date: Sat, 23 May 2026 15:10:02 +0200 Subject: [PATCH 010/136] 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) * 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) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- api/api.go | 1 + api/models.go | 1 + .../src/components/ReceiveToLightning.tsx | 2 +- .../screens/wallet/receive/ReceiveInvoice.tsx | 21 +++++++++---------- frontend/src/types.ts | 1 + 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/api/api.go b/api/api.go index f828a92a..e0e3402b 100644 --- a/api/api.go +++ b/api/api.go @@ -1391,6 +1391,7 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { info.HideUpdateBanner = api.cfg.GetEnv().HideUpdateBanner info.LdkVssEnabled = ldkVssEnabled == "true" info.VssSupported = backendType == config.LDKBackendType && api.cfg.GetEnv().LDKVssUrl != "" + info.SupportsBolt12 = backendType == config.LDKBackendType || backendType == config.CLNBackendType info.AutoUnlockPasswordEnabled = autoUnlockPassword != "" info.AutoUnlockPasswordSupported = api.cfg.GetEnv().IsDefaultClientId() info.Relays = []InfoResponseRelay{} diff --git a/api/models.go b/api/models.go index 375a9556..4664dfbd 100644 --- a/api/models.go +++ b/api/models.go @@ -333,6 +333,7 @@ type InfoResponse struct { ChainDataSourceType string `json:"chainDataSourceType,omitempty"` ChainDataSourceAddress string `json:"chainDataSourceAddress,omitempty"` HideUpdateBanner bool `json:"hideUpdateBanner"` + SupportsBolt12 bool `json:"supportsBolt12"` } type UpdateSettingsRequest struct { diff --git a/frontend/src/components/ReceiveToLightning.tsx b/frontend/src/components/ReceiveToLightning.tsx index 6c0b4f09..444774eb 100644 --- a/frontend/src/components/ReceiveToLightning.tsx +++ b/frontend/src/components/ReceiveToLightning.tsx @@ -40,7 +40,7 @@ export function ReceiveToLightning() { Create Invoice - {info.backendType === "LDK" && ( + {info.supportsBolt12 && ( {(!info?.albyAccountConnected || !me?.lightning_address) && (
- {!info?.albyAccountConnected && - info.backendType === "LDK" && ( - - - Lightning Offer - - )} + {!info?.albyAccountConnected && info.supportsBolt12 && ( + + + Lightning Offer + + )} Date: Sun, 24 May 2026 07:22:48 +0200 Subject: [PATCH 011/136] feat(cards): provider directory routed through the standard app-install flow (#2366) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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¤cy=USDC&nwc= 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 * 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 * 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 * 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 * 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/ 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/, 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= 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= 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=. - 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= and /appstore/. 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 Co-authored-by: Roland Bewick --- alby/alby_oauth_service.go | 2 + frontend/src/assets/cards/2fiat.png | Bin 0 -> 4695 bytes frontend/src/assets/cards/freedomia.png | Bin 0 -> 1362 bytes frontend/src/assets/cards/redotpay.png | Bin 0 -> 1224 bytes .../suggested-apps/bitcoin-card-topup.png | Bin 0 -> 8944 bytes frontend/src/components/AppSidebar.tsx | 6 + .../src/components/PaymentFailedAlert.tsx | 27 +- .../connections/AppStoreDetailHeader.tsx | 49 +- .../connections/SuggestedAppData.tsx | 57 +- frontend/src/components/icons/GooglePay.tsx | 28 + .../src/components/icons/MastercardLogo.tsx | 19 + frontend/src/components/icons/VisaLogo.tsx | 16 + frontend/src/components/ui/toggle-group.tsx | 83 ++ frontend/src/components/ui/toggle.tsx | 24 + frontend/src/components/ui/toggleVariants.tsx | 23 + frontend/src/constants.ts | 1 + frontend/src/routes.tsx | 7 + frontend/src/screens/ai/AI.tsx | 2 +- frontend/src/screens/apps/NewApp.tsx | 2 +- frontend/src/screens/cards/Cards.tsx | 759 ++++++++++++++++++ frontend/src/utils/sendEvent.ts | 16 + http/http_service.go | 2 +- 22 files changed, 1070 insertions(+), 53 deletions(-) create mode 100644 frontend/src/assets/cards/2fiat.png create mode 100644 frontend/src/assets/cards/freedomia.png create mode 100644 frontend/src/assets/cards/redotpay.png create mode 100644 frontend/src/assets/suggested-apps/bitcoin-card-topup.png create mode 100644 frontend/src/components/icons/GooglePay.tsx create mode 100644 frontend/src/components/icons/MastercardLogo.tsx create mode 100644 frontend/src/components/icons/VisaLogo.tsx create mode 100644 frontend/src/components/ui/toggle-group.tsx create mode 100644 frontend/src/components/ui/toggle.tsx create mode 100644 frontend/src/components/ui/toggleVariants.tsx create mode 100644 frontend/src/screens/cards/Cards.tsx create mode 100644 frontend/src/utils/sendEvent.ts diff --git a/alby/alby_oauth_service.go b/alby/alby_oauth_service.go index 165f5b34..445412a6 100644 --- a/alby/alby_oauth_service.go +++ b/alby/alby_oauth_service.go @@ -1413,5 +1413,7 @@ func getEventWhitelist() []string { // client-side events "payment_failed_details", + "debit_card_url_clicked", + "debit_card_connect", } } diff --git a/frontend/src/assets/cards/2fiat.png b/frontend/src/assets/cards/2fiat.png new file mode 100644 index 0000000000000000000000000000000000000000..dd6b3732ba43fe43768145204e1caedd6266f264 GIT binary patch literal 4695 zcmV-d5~%HoP)rY~p|c2O9@M5)6jW93U)B2m!MsJx$2co`fvWoF@N} zmeV8$3TfcbB&7!^B`j$TCJrG9*fC&Z8H^XnvL$P=*0$+&$N?m}l!))Pc|hG6)cWs{TX)N}+%S3mSR)>Hnm&b1{IZb^r%R0r`tA zx^Q~Smfx^=@nZV=`u=P_#Gf8=d~5G)!}H2q{Gqk{YRl#%O2rhyRj7yxr3uQ=bt4^# zL>AQ5rJLjNcvHlUG�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 GgeCx2uVbeG literal 0 HcmV?d00001 diff --git a/frontend/src/assets/cards/redotpay.png b/frontend/src/assets/cards/redotpay.png new file mode 100644 index 0000000000000000000000000000000000000000..cc72cb4c92899b8ad23ffbc1e0531d37612fe6ed GIT binary patch literal 1224 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 KMNUMnLSTZ@xURYY literal 0 HcmV?d00001 diff --git a/frontend/src/components/AppSidebar.tsx b/frontend/src/components/AppSidebar.tsx index 6d4eb8e8..3157a289 100644 --- a/frontend/src/components/AppSidebar.tsx +++ b/frontend/src/components/AppSidebar.tsx @@ -2,6 +2,7 @@ import { BotIcon, BoxIcon, ChevronsUpDownIcon, + CreditCardIcon, CircleHelpIcon, HandCoinsIcon, HomeIcon, @@ -105,6 +106,11 @@ export function AppSidebar() { title: "AI & Agents", url: "/ai", icon: BotIcon, + }, + { + title: "Cards", + url: "/cards", + icon: CreditCardIcon, badge: "NEW", }, ], diff --git a/frontend/src/components/PaymentFailedAlert.tsx b/frontend/src/components/PaymentFailedAlert.tsx index 9ccb4aab..16e57377 100644 --- a/frontend/src/components/PaymentFailedAlert.tsx +++ b/frontend/src/components/PaymentFailedAlert.tsx @@ -5,7 +5,7 @@ import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert"; import { ExternalLinkButton } from "src/components/ui/custom/external-link-button"; import { LoadingButton } from "src/components/ui/custom/loading-button"; import { useChannels } from "src/hooks/useChannels"; -import { request } from "src/utils/request"; +import { sendEvent } from "src/utils/sendEvent"; export function PaymentFailedAlert({ invoice, @@ -19,25 +19,12 @@ export function PaymentFailedAlert({ async function sendDetailsToAlby() { setSendingDetailsToAlby(true); - try { - await request(`/api/event`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - event: "payment_failed_details", - properties: { - invoice, - errorMessage, - channels, - }, - }), - }); - toast("Thanks for improving Alby Hub."); - } catch (error) { - console.error(error); - } + await sendEvent("payment_failed_details", { + invoice, + errorMessage, + channels, + }); + toast("Thanks for improving Alby Hub."); setSendingDetailsToAlby(false); } diff --git a/frontend/src/components/connections/AppStoreDetailHeader.tsx b/frontend/src/components/connections/AppStoreDetailHeader.tsx index b40c5bd2..02f4a688 100644 --- a/frontend/src/components/connections/AppStoreDetailHeader.tsx +++ b/frontend/src/components/connections/AppStoreDetailHeader.tsx @@ -24,36 +24,27 @@ export function AppStoreDetailHeader({ <> -
- -
-
- {appStoreApp.title} - {!!connectedApps.length && ( - - {" "} - {connectedApps.length > 1 - ? `${connectedApps.length} Connections` - : "Connected"} - - )} -
-
- {appStoreApp.description} -
-
-
- + icon={ + logo } - description="" + title={ +
+ {appStoreApp.title} + {!!connectedApps.length && ( + + {" "} + {connectedApps.length > 1 + ? `${connectedApps.length} Connections` + : "Connected"} + + )} +
+ } + description={appStoreApp.description} contentRight={ contentRight !== undefined ? ( contentRight diff --git a/frontend/src/components/connections/SuggestedAppData.tsx b/frontend/src/components/connections/SuggestedAppData.tsx index 35c1b73f..25f1921b 100644 --- a/frontend/src/components/connections/SuggestedAppData.tsx +++ b/frontend/src/components/connections/SuggestedAppData.tsx @@ -6,6 +6,7 @@ import albyGo from "src/assets/suggested-apps/alby-go.png"; import albySandbox from "src/assets/suggested-apps/alby-sandbox.png"; import albyCli from "src/assets/suggested-apps/alby.png"; import amethyst from "src/assets/suggested-apps/amethyst.png"; +import bitcoinCardTopup from "src/assets/suggested-apps/bitcoin-card-topup.png"; import bitrefill from "src/assets/suggested-apps/bitrefill.png"; import bitrequest from "src/assets/suggested-apps/bitrequest.png"; import bringin from "src/assets/suggested-apps/bringin.png"; @@ -34,8 +35,8 @@ import payperq from "src/assets/suggested-apps/payperq.png"; import primal from "src/assets/suggested-apps/primal.png"; import pullthatupjamie from "src/assets/suggested-apps/pullthatupjamie.png"; import runstr from "src/assets/suggested-apps/runstr.png"; -import sats4ai from "src/assets/suggested-apps/sats4ai.png"; import satsorter from "src/assets/suggested-apps/sat-sorter.png"; +import sats4ai from "src/assets/suggested-apps/sats4ai.png"; import simpleboost from "src/assets/suggested-apps/simple-boost.png"; import snort from "src/assets/suggested-apps/snort.png"; import stackernews from "src/assets/suggested-apps/stacker-news.png"; @@ -218,6 +219,60 @@ export const appStoreApps: AppStoreApp[] = ( categories: ["audio"], addedDate: "2026-03-12", }, + { + id: "bitcoin-card-topup", + title: "Bitcoin Card Topup", + description: "Top up any crypto debit card instantly with bitcoin", + logo: bitcoinCardTopup, + categories: ["payment-tools"], + extendedDescription: + "A generic top-up app that swaps Lightning sats to a stablecoin and sends them to your card's deposit address. Works with RedotPay, Freedomia, Nexo, Bybit, and any other card that accepts on-chain crypto deposits.", + webLink: "https://card.albylabs.com", + installGuide: ( + <> +
+
    +
  • + Open{" "} + + card.albylabs.com + {" "} + on the device you'll top up from. +
  • +
  • + + Add it to your home screen + {" "} + (or bookmark it) so you can reopen it later. +
  • +
  • + Enter your card's deposit address, network, and currency to set + it up. +
  • +
+
+ + ), + finalizeGuide: ( + <> +
+
    +
  • Copy the connection secret below.
  • +
  • + In the topup app, tap{" "} + + Connect Wallet + {" "} + and paste the connection secret. +
  • +
+
+ + ), + }, { id: "2fiat", title: "2fiat Top up", diff --git a/frontend/src/components/icons/GooglePay.tsx b/frontend/src/components/icons/GooglePay.tsx new file mode 100644 index 00000000..3261bb3f --- /dev/null +++ b/frontend/src/components/icons/GooglePay.tsx @@ -0,0 +1,28 @@ +import { SVGAttributes } from "react"; + +export function GooglePayIcon(props: SVGAttributes) { + return ( + + + + + ); +} diff --git a/frontend/src/components/icons/MastercardLogo.tsx b/frontend/src/components/icons/MastercardLogo.tsx new file mode 100644 index 00000000..d4053bf0 --- /dev/null +++ b/frontend/src/components/icons/MastercardLogo.tsx @@ -0,0 +1,19 @@ +import { SVGAttributes } from "react"; + +export function MastercardLogo(props: SVGAttributes) { + return ( + + + + + + ); +} diff --git a/frontend/src/components/icons/VisaLogo.tsx b/frontend/src/components/icons/VisaLogo.tsx new file mode 100644 index 00000000..e2948a1a --- /dev/null +++ b/frontend/src/components/icons/VisaLogo.tsx @@ -0,0 +1,16 @@ +import { SVGAttributes } from "react"; + +export function VisaLogo(props: SVGAttributes) { + return ( + + + + + ); +} diff --git a/frontend/src/components/ui/toggle-group.tsx b/frontend/src/components/ui/toggle-group.tsx new file mode 100644 index 00000000..6f63ca4c --- /dev/null +++ b/frontend/src/components/ui/toggle-group.tsx @@ -0,0 +1,83 @@ +"use client"; + +import * as React from "react"; +import { type VariantProps } from "class-variance-authority"; +import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"; + +import { cn } from "src/lib/utils"; +import { toggleVariants } from "src/components/ui/toggleVariants"; + +const ToggleGroupContext = React.createContext< + VariantProps & { + spacing?: number; + } +>({ + size: "default", + variant: "default", + spacing: 0, +}); + +function ToggleGroup({ + className, + variant, + size, + spacing = 0, + children, + ...props +}: React.ComponentProps & + VariantProps & { + spacing?: number; + }) { + return ( + + + {children} + + + ); +} + +function ToggleGroupItem({ + className, + children, + variant, + size, + ...props +}: React.ComponentProps & + VariantProps) { + const context = React.useContext(ToggleGroupContext); + + return ( + + {children} + + ); +} + +export { ToggleGroup, ToggleGroupItem }; diff --git a/frontend/src/components/ui/toggle.tsx b/frontend/src/components/ui/toggle.tsx new file mode 100644 index 00000000..4fe897c0 --- /dev/null +++ b/frontend/src/components/ui/toggle.tsx @@ -0,0 +1,24 @@ +import * as React from "react"; +import { type VariantProps } from "class-variance-authority"; +import { Toggle as TogglePrimitive } from "radix-ui"; + +import { cn } from "src/lib/utils"; +import { toggleVariants } from "src/components/ui/toggleVariants"; + +function Toggle({ + className, + variant, + size, + ...props +}: React.ComponentProps & + VariantProps) { + return ( + + ); +} + +export { Toggle }; diff --git a/frontend/src/components/ui/toggleVariants.tsx b/frontend/src/components/ui/toggleVariants.tsx new file mode 100644 index 00000000..d3be284b --- /dev/null +++ b/frontend/src/components/ui/toggleVariants.tsx @@ -0,0 +1,23 @@ +import { cva } from "class-variance-authority"; + +export const toggleVariants = cva( + "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-transparent", + outline: + "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground", + }, + size: { + default: "h-9 min-w-9 px-2", + sm: "h-8 min-w-8 px-1.5", + lg: "h-10 min-w-10 px-2.5", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); diff --git a/frontend/src/constants.ts b/frontend/src/constants.ts index 9156a45f..dd64c3a5 100644 --- a/frontend/src/constants.ts +++ b/frontend/src/constants.ts @@ -5,6 +5,7 @@ export const localStorageKeys = { authToken: "authToken", supportAlbySidebarHintHiddenUntil: "supportAlbySidebarHintHiddenUntil", aiHeroDismissed: "aiHeroDismissed", + cardsHeroDismissed: "cardsHeroDismissed", }; export const ONCHAIN_DUST_SATS = 1000; diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 41f25ce0..78f645f6 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -18,6 +18,7 @@ import Unlock from "src/screens/Unlock"; import { Welcome } from "src/screens/Welcome"; import AlbyAuthRedirect from "src/screens/alby/AlbyAuthRedirect"; import { AI } from "src/screens/ai/AI"; +import { Cards } from "src/screens/cards/Cards"; import { AlbyEarn } from "src/screens/alby/AlbyEarn"; import SupportAlby from "src/screens/alby/SupportAlby"; import AppDetails from "src/screens/apps/AppDetails"; @@ -474,6 +475,12 @@ const routes: RouteObject[] = [ element: , handle: { crumb: () => "AI & Agents" }, }, + { + path: "cards", + element: , + handle: { crumb: () => "Cards" }, + children: [{ index: true, element: }], + }, ], }, { diff --git a/frontend/src/screens/ai/AI.tsx b/frontend/src/screens/ai/AI.tsx index c2970ab6..a965ea99 100644 --- a/frontend/src/screens/ai/AI.tsx +++ b/frontend/src/screens/ai/AI.tsx @@ -662,7 +662,7 @@ const whyLightningItems = [ icon: EyeOffIcon, title: "Private by Default", description: - "No credit cards or personal info shared with merchants. Your agent pays over Lightning — fast, direct, and private.", + "No credit cards or personal info shared with merchants. Your agent pays over lightning — fast, direct, and private.", }, { icon: ZapIcon, diff --git a/frontend/src/screens/apps/NewApp.tsx b/frontend/src/screens/apps/NewApp.tsx index 348cc4d2..baac7814 100644 --- a/frontend/src/screens/apps/NewApp.tsx +++ b/frontend/src/screens/apps/NewApp.tsx @@ -337,7 +337,7 @@ const NewAppInternal = ({ capabilities }: NewAppInternalProps) => { logo ) : undefined } diff --git a/frontend/src/screens/cards/Cards.tsx b/frontend/src/screens/cards/Cards.tsx new file mode 100644 index 00000000..e0e32f22 --- /dev/null +++ b/frontend/src/screens/cards/Cards.tsx @@ -0,0 +1,759 @@ +import { + ArrowUpRightIcon, + CheckIcon, + ClockIcon, + CreditCardIcon, + FingerprintIcon, + InfoIcon, + LinkIcon, + PlusIcon, + ShieldCheckIcon, + XIcon, + ZapIcon, +} from "lucide-react"; +import React from "react"; +import { Link } from "react-router"; +import twoFiatLogo from "src/assets/cards/2fiat.png"; +import freedomiaLogo from "src/assets/cards/freedomia.png"; +import redotpayLogo from "src/assets/cards/redotpay.png"; +import bringinLogo from "src/assets/suggested-apps/bringin.png"; +import wavespaceLogo from "src/assets/suggested-apps/wave-space.png"; +import AppHeader from "src/components/AppHeader"; +import { AlbyIcon } from "src/components/icons/Alby"; +import { AppleIcon } from "src/components/icons/Apple"; +import { GooglePayIcon } from "src/components/icons/GooglePay"; +import { VisaLogo } from "src/components/icons/VisaLogo"; +import { Avatar, AvatarFallback, AvatarImage } from "src/components/ui/avatar"; +import { Badge } from "src/components/ui/badge"; +import { Button } from "src/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "src/components/ui/dialog"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "src/components/ui/table"; +import { ToggleGroup, ToggleGroupItem } from "src/components/ui/toggle-group"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "src/components/ui/tooltip"; +import { localStorageKeys } from "src/constants"; +import { useAlbyMe } from "src/hooks/useAlbyMe"; +import { useInfo } from "src/hooks/useInfo"; +import { cn } from "src/lib/utils"; +import { sendEvent } from "src/utils/sendEvent"; + +type Region = "Global" | "US" | "EU" | "UK" | "LATAM" | "Africa" | "Asia"; + +type FeatureFilter = + | "ApplePay" + | "GooglePay" + | "Self-custody" + | "Lightning-native" + | "No KYC"; + +type Provider = { + id: string; + name: string; + url: string; + logo: string; + initials: string; + network: "Visa" | "Mastercard"; + cardType: "Physical" | "Virtual" | "Both"; + regions: Region[]; + applePay: boolean; + googlePay: boolean; + selfCustody: boolean; + lightningNative: boolean; + kyc: "Full" | "Light" | "None"; + timeToGet: string; + cardCost: string; + fees: string; + /** + * If set, the provider connects via its own NWC flow in the in-hub app store + * (the row's action links to /appstore/) rather than through the + * stablecoin top-up Connect-card dialog. + */ + appStoreId?: string; +}; + +const providers: Provider[] = [ + { + id: "redotpay", + name: "RedotPay", + url: "https://www.redotpay.com", + logo: redotpayLogo, + initials: "RP", + network: "Visa", + cardType: "Virtual", + regions: ["Global", "US", "UK", "EU", "LATAM", "Asia"], + applePay: true, + googlePay: true, + selfCustody: false, + lightningNative: false, + kyc: "Light", + timeToGet: "<10 minutes", + cardCost: "$10", + fees: "~2.2% + FX", + appStoreId: "bitcoin-card-topup", + }, + { + id: "2fiat", + name: "2fiat", + url: "https://2fiat.com/getalby", + logo: twoFiatLogo, + initials: "2F", + network: "Mastercard", + cardType: "Virtual", + regions: ["Global"], + applePay: true, + googlePay: true, + selfCustody: false, + lightningNative: true, + kyc: "None", + timeToGet: "Instant", + cardCost: "$50", + fees: "~6.8% top-up + $0.50", + appStoreId: "2fiat", + }, + { + id: "freedomia", + name: "Freedomia", + url: "https://www.freedomia.io/a/getalby", + logo: freedomiaLogo, + initials: "FR", + network: "Visa", + cardType: "Virtual", + regions: ["Global"], + applePay: false, + googlePay: true, + selfCustody: false, + lightningNative: false, + kyc: "None", + timeToGet: "Instant", + cardCost: "$5–30 / mo", + fees: "1.3–4.3%", + appStoreId: "bitcoin-card-topup", + }, + { + id: "bringin", + name: "Bringin", + url: "https://bringin.app", + logo: bringinLogo, + initials: "BR", + network: "Visa", + cardType: "Both", + regions: ["EU"], + // Direct Apple Pay isn't live — usable today via Curve only, so don't + // claim it. Google/Samsung/Fitbit/Garmin Pay are direct. + applePay: false, + googlePay: true, + selfCustody: false, + lightningNative: true, + kyc: "Full", + timeToGet: "Minutes", + // Their cards page advertises a €3.49/mo subscription that bundles + // both cards. Worth re-confirming during checkout — sources disagree. + cardCost: "€3.49 / mo", + fees: "1% + 0.5%", + appStoreId: "bringin", + }, + { + id: "wavespace", + name: "wavecard by wave.space", + url: "https://app.wave.space/spend/?utm_source=albyhub&affiliate=AlbyHub", + logo: wavespaceLogo, + initials: "WS", + network: "Visa", + cardType: "Both", + // Card is accepted worldwide, but issuance requires EEA residency. + regions: ["EU"], + applePay: false, + googlePay: true, + selfCustody: false, + lightningNative: true, + kyc: "Light", + timeToGet: "Minutes", + cardCost: "€2.99 / €29.99", + fees: "1% + 0.5%", + appStoreId: "wavespace", + }, +]; + +const howItWorksSteps = [ + { + icon: CreditCardIcon, + title: "Get a card", + description: + "Pick a debit card that fits your region. Physical or virtual, Apple Pay or Google Pay ready.", + }, + { + icon: LinkIcon, + title: "Get a top-up link", + description: + "Connect your card once and save the top-up link to your phone. Open it anytime to fund your card.", + }, + { + icon: ZapIcon, + title: "Top up in seconds", + description: + "Pay from your Lightning balance. Your card is funded in seconds, ready to spend.", + }, +]; + +const regionFilters: { label: string; value: Region | "All" }[] = [ + { label: "All", value: "All" }, + { label: "Global", value: "Global" }, + { label: "US", value: "US" }, + { label: "EU", value: "EU" }, + { label: "UK", value: "UK" }, + { label: "LATAM", value: "LATAM" }, + { label: "Africa", value: "Africa" }, + { label: "Asia", value: "Asia" }, +]; + +export function Cards() { + const { data: albyMe } = useAlbyMe(); + const { data: info } = useInfo(); + const cardholderName = ( + albyMe?.name || + info?.nodeAlias || + "Alby Hub User" + ).toUpperCase(); + + const [region, setRegion] = React.useState("All"); + const [features, setFeatures] = React.useState([]); + const [connectOpen, setConnectOpen] = React.useState(false); + const [heroDismissed, setHeroDismissed] = React.useState( + () => localStorage.getItem(localStorageKeys.cardsHeroDismissed) === "true" + ); + + const dismissHero = React.useCallback(() => { + setHeroDismissed(true); + localStorage.setItem(localStorageKeys.cardsHeroDismissed, "true"); + }, []); + + const filtered = providers.filter((p) => { + if ( + region !== "All" && + !p.regions.includes(region) && + !(region !== "Global" && p.regions.includes("Global")) + ) { + return false; + } + for (const f of features) { + if (f === "ApplePay" && !p.applePay) { + return false; + } + if (f === "GooglePay" && !p.googlePay) { + return false; + } + if (f === "Self-custody" && !p.selfCustody) { + return false; + } + if (f === "Lightning-native" && !p.lightningNative) { + return false; + } + if (f === "No KYC" && p.kyc !== "None") { + return false; + } + } + return true; + }); + + return ( + <> + setConnectOpen(true)}> + + Connect card + + } + /> + + + + {/* Hero — AI-style with 3-step strip */} + {!heroDismissed && ( +
+
+ +
+ {/* Left */} +
+

+ Cards + Bitcoin +

+

+ Got your card ready? +
+ Connect it. +

+

+ Spend Bitcoin anywhere. Top up your debit card from your + Lightning balance in under a minute. +

+
+ + {/* Right — card visual */} +
+
+
+
+
+ + ALBY HUB + + +
+ +
+ •••• + •••• + •••• + 4421 +
+ +
+
+

+ Cardholder +

+

+ {cardholderName} +

+
+ +
+
+
+
+
+
+ + {/* Three steps */} +
+ {howItWorksSteps.map((item) => { + const Icon = item.icon; + return ( +
+ +

{item.title}

+

+ {item.description} +

+
+ ); + })} +
+
+
+ )} + + {/* Filter bar */} +
+
+
+

Region

+ v && setRegion(v as Region | "All")} + variant="outline" + size="sm" + className="*:data-[state=on]:bg-primary *:data-[state=on]:text-primary-foreground *:data-[state=on]:border-primary" + > + {regionFilters.map((r) => ( + + {r.label} + + ))} + +
+ +
+

Filter

+ setFeatures(v as FeatureFilter[])} + variant="outline" + size="sm" + spacing={1} + className="*:data-[state=on]:bg-primary *:data-[state=on]:text-primary-foreground *:data-[state=on]:border-primary" + > + {providers.some((p) => p.applePay) && ( + + + Apple Pay + + )} + {providers.some((p) => p.googlePay) && ( + + + Google Pay + + )} + {providers.some((p) => p.selfCustody) && ( + + + Self-custody + + )} + {providers.some((p) => p.lightningNative) && ( + + + Lightning + + )} + {providers.some((p) => p.kyc === "None") && ( + + + No KYC + + )} + +
+
+ + {(region !== "All" || features.length > 0) && ( +
+

+ Showing{" "} + + {filtered.length} + {" "} + of {providers.length} providers +

+ +
+ )} +
+ + {/* Provider table */} +
+
+ + + + Provider + Type + Regions + Mobile + KYC + Time to get + Card cost + Fees + + + + {filtered.length === 0 && ( + + + No providers match these filters. + + + )} + {filtered.map((p) => ( + + ))} + +
+
+ +

+ Alby Hub does not issue or operate these cards. Availability, fees, + and KYC are set by each provider — values here are approximate and may + change. +

+
+ + ); +} + +function ProviderRow({ provider }: { provider: Provider }) { + return ( + { + sendEvent("debit_card_url_clicked", { + name: provider.name, + url: provider.url, + }); + window.open(provider.url, "_blank", "noopener,noreferrer"); + }} + > + +
+ + + + {provider.initials} + + +
+
+ + {provider.name} + + {provider.selfCustody && ( + + + + + + + Self-custodial + + )} + {provider.lightningNative && ( + + + + + + + Lightning-native + + )} +
+

{provider.network}

+
+
+
+ + + {provider.cardType} + + + +
+ {provider.regions.map((r) => ( + + {r} + + ))} +
+
+ +
+ + + + + + + + {provider.applePay ? "Apple Pay supported" : "No Apple Pay"} + + + + + + + + + + {provider.googlePay ? "Google Pay supported" : "No Google Pay"} + + +
+
+ +
+ +
+
+ + + + {provider.timeToGet} + + + +
+ {provider.cardCost} +
+
+ +
+ {provider.fees} +
+
+
+ ); +} + +function ConnectCardDialog({ + open, + onOpenChange, + providers, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + providers: Provider[]; +}) { + return ( + + + + Pick your card provider + + We'll take you to the setup guide for the one you choose. + + + +
+ {providers.map((p) => { + if (!p.appStoreId) { + return null; + } + return ( + { + sendEvent("debit_card_connect", { name: p.name }); + onOpenChange(false); + }} + className="flex items-center gap-3 rounded-lg border border-border p-3 hover:bg-accent/40 transition-colors" + > + + + + {p.initials} + + +
+

{p.name}

+

+ {p.network} · {p.cardType} +

+
+ + + ); + })} + + { + sendEvent("debit_card_connect", { name: "Other" }); + onOpenChange(false); + }} + className="flex items-center gap-3 rounded-lg border border-dashed border-border p-3 hover:bg-accent/40 transition-colors" + > +
+ +
+
+

Other card

+

+ Any crypto card not listed +

+
+ + +
+
+
+ ); +} + +function KycBadge({ kyc }: { kyc: Provider["kyc"] }) { + if (kyc === "None") { + return ( + + + None + + + + + + + + No-KYC cards typically operate via a single merchant-of-record + account. Privacy-friendly, but operationally fragile — the program + can be paused or shut down without notice. + + + + ); + } + if (kyc === "Light") { + return ( + + Light + + + + + + + + ID verification only — no proof of address, employer details, or + source-of-funds questions. + + + + ); + } + return ( + + {kyc} + + ); +} diff --git a/frontend/src/utils/sendEvent.ts b/frontend/src/utils/sendEvent.ts new file mode 100644 index 00000000..581f114a --- /dev/null +++ b/frontend/src/utils/sendEvent.ts @@ -0,0 +1,16 @@ +import { request } from "src/utils/request"; + +export async function sendEvent( + name: string, + properties?: Record +) { + try { + await request(`/api/event`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ event: name, properties }), + }); + } catch (error) { + console.error(error); + } +} diff --git a/http/http_service.go b/http/http_service.go index 74ec814e..1714e895 100644 --- a/http/http_service.go +++ b/http/http_service.go @@ -157,7 +157,7 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) { fullAccessApiGroup.Use(echojwt.WithConfig(jwtConfig)) fullAccessApiGroup.Use(httpSvc.requireFullAccess) - fullAccessApiGroup.POST("/api/event", httpSvc.eventHandler) + fullAccessApiGroup.POST("/event", httpSvc.eventHandler) fullAccessApiGroup.PATCH("/unlock-password", httpSvc.changeUnlockPasswordHandler) fullAccessApiGroup.PATCH("/auto-unlock", httpSvc.autoUnlockHandler) fullAccessApiGroup.PATCH("/settings", httpSvc.updateSettingsHandler) From 4053a4a722a42647d6a3e12d2ade9a02bfab4f5d Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Mon, 25 May 2026 03:44:26 +0700 Subject: [PATCH 012/136] Fix: card page mobile interface (#2377) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- frontend/src/screens/cards/Cards.tsx | 170 +++++++++++++++++++++++---- 1 file changed, 145 insertions(+), 25 deletions(-) diff --git a/frontend/src/screens/cards/Cards.tsx b/frontend/src/screens/cards/Cards.tsx index e0e32f22..095572e4 100644 --- a/frontend/src/screens/cards/Cards.tsx +++ b/frontend/src/screens/cards/Cards.tsx @@ -33,6 +33,13 @@ import { DialogHeader, DialogTitle, } from "src/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "src/components/ui/select"; import { Table, TableBody, @@ -96,7 +103,7 @@ const providers: Provider[] = [ initials: "RP", network: "Visa", cardType: "Virtual", - regions: ["Global", "US", "UK", "EU", "LATAM", "Asia"], + regions: ["Global"], applePay: true, googlePay: true, selfCustody: false, @@ -375,26 +382,27 @@ export function Cards() { {/* Filter bar */}
-
+

Region

- v && setRegion(v as Region | "All")} - variant="outline" - size="sm" - className="*:data-[state=on]:bg-primary *:data-[state=on]:text-primary-foreground *:data-[state=on]:border-primary" + onValueChange={(v) => setRegion(v as Region | "All")} > - {regionFilters.map((r) => ( - - {r.label} - - ))} - + + + + + {regionFilters.map((r) => ( + + {r.label} + + ))} + +
-
+

Filter

{providers.some((p) => p.applePay) && ( @@ -467,9 +475,9 @@ export function Cards() { )}
- {/* Provider table */} + {/* Provider table (desktop) / card list (mobile) */}
-
+
@@ -501,6 +509,18 @@ export function Cards() {
+ {/* Mobile: stacked cards instead of a sideways-scrolling table */} +
+ {filtered.length === 0 && ( +
+ No providers match these filters. +
+ )} + {filtered.map((p) => ( + + ))} +
+

Alby Hub does not issue or operate these cards. Availability, fees, and KYC are set by each provider — values here are approximate and may @@ -511,17 +531,19 @@ export function Cards() { ); } +function openProvider(provider: Provider) { + sendEvent("debit_card_url_clicked", { + name: provider.name, + url: provider.url, + }); + window.open(provider.url, "_blank", "noopener,noreferrer"); +} + function ProviderRow({ provider }: { provider: Provider }) { return ( { - sendEvent("debit_card_url_clicked", { - name: provider.name, - url: provider.url, - }); - window.open(provider.url, "_blank", "noopener,noreferrer"); - }} + onClick={() => openProvider(provider)} >

@@ -632,6 +654,104 @@ function ProviderRow({ provider }: { provider: Provider }) { ); } +function ProviderCard({ provider }: { provider: Provider }) { + return ( +
+
+ + + + {provider.initials} + + +
+
+ {provider.name} + {provider.selfCustody && ( + + )} + {provider.lightningNative && ( + + )} +
+

+ {provider.network} · {provider.cardType} +

+
+
+ +
+ {provider.regions.map((r) => ( + + {r} + + ))} + {(provider.applePay || provider.googlePay) && ( + + {provider.applePay && } + {provider.googlePay && } + + )} +
+ +
+ + + + + + + {provider.timeToGet} + + + + + {provider.cardCost} + + + + {provider.fees} + +
+ + +
+ ); +} + +function CardFact({ + label, + children, +}: { + label: string; + children: React.ReactNode; +}) { + return ( +
+

+ {label} +

+ {children} +
+ ); +} + function ConnectCardDialog({ open, onOpenChange, From e3373474f8b7d0a218c8af9efe684bf2fb5e6d78 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Mon, 25 May 2026 21:56:07 +0700 Subject: [PATCH 013/136] 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 --- api/api.go | 169 ++++++++++++++++++-- api/models.go | 119 ++++++++++++-- lnclient/cashu/cashu.go | 24 ++- lnclient/cln/cln.go | 6 +- lnclient/ldk/ldk.go | 28 +--- lnclient/lnd/lnd.go | 33 +--- lnclient/models.go | 139 +++++++--------- lnclient/phoenixd/phoenixd.go | 24 ++- nip47/controllers/get_balance_controller.go | 2 +- nip47/controllers/pay_keysend_controller.go | 24 ++- swaps/swaps_service.go | 2 +- tests/mock_ln_client.go | 6 +- tests/mocks/LNClient.go | 27 +--- 13 files changed, 386 insertions(+), 217 deletions(-) diff --git a/api/api.go b/api/api.go index e0e3402b..688061ed 100644 --- a/api/api.go +++ b/api/api.go @@ -834,12 +834,20 @@ func (api *api) Stop() error { return nil } -func (api *api) GetNodeConnectionInfo(ctx context.Context) (*lnclient.NodeConnectionInfo, error) { +func (api *api) GetNodeConnectionInfo(ctx context.Context) (*NodeConnectionInfo, error) { lnClient := api.svc.GetLNClient() if lnClient == nil { return nil, ErrLNClientNotStarted } - return lnClient.GetNodeConnectionInfo(ctx) + info, err := lnClient.GetNodeConnectionInfo(ctx) + if err != nil { + return nil, err + } + return &NodeConnectionInfo{ + Pubkey: info.Pubkey, + Address: info.Address, + Port: info.Port, + }, nil } func (api *api) RefundSwap(refundSwapRequest *RefundSwapRequest) error { @@ -1127,20 +1135,47 @@ func (api *api) GetSwapMnemonic() string { return api.keys.GetSwapMnemonic() } -func (api *api) GetNodeStatus(ctx context.Context) (*lnclient.NodeStatus, error) { +func (api *api) GetNodeStatus(ctx context.Context) (*NodeStatus, error) { lnClient := api.svc.GetLNClient() if lnClient == nil { return nil, ErrLNClientNotStarted } - return lnClient.GetNodeStatus(ctx) + nodeStatus, err := lnClient.GetNodeStatus(ctx) + if err != nil { + return nil, err + } + if nodeStatus == nil { + return nil, nil + } + return toApiNodeStatus(nodeStatus), nil } -func (api *api) ListPeers(ctx context.Context) ([]lnclient.PeerDetails, error) { +func toApiNodeStatus(nodeStatus *lnclient.NodeStatus) *NodeStatus { + return &NodeStatus{ + IsReady: nodeStatus.IsReady, + InternalNodeStatus: nodeStatus.InternalNodeStatus, + } +} + +func (api *api) ListPeers(ctx context.Context) ([]PeerDetails, error) { lnClient := api.svc.GetLNClient() if lnClient == nil { return nil, ErrLNClientNotStarted } - return lnClient.ListPeers(ctx) + peers, err := lnClient.ListPeers(ctx) + if err != nil { + return nil, err + } + apiPeers := make([]PeerDetails, 0, len(peers)) + for _, peer := range peers { + apiPeers = append(apiPeers, PeerDetails{ + NodeId: peer.NodeId, + Address: peer.Address, + IsPersisted: peer.IsPersisted, + IsConnected: peer.IsConnected, + }) + } + return apiPeers, nil } func (api *api) ConnectPeer(ctx context.Context, connectPeerRequest *ConnectPeerRequest) error { @@ -1148,7 +1183,11 @@ func (api *api) ConnectPeer(ctx context.Context, connectPeerRequest *ConnectPeer if lnClient == nil { return ErrLNClientNotStarted } - return lnClient.ConnectPeer(ctx, connectPeerRequest) + return lnClient.ConnectPeer(ctx, &lnclient.ConnectPeerRequest{ + Pubkey: connectPeerRequest.Pubkey, + Address: connectPeerRequest.Address, + Port: connectPeerRequest.Port, + }) } func (api *api) OpenChannel(ctx context.Context, openChannelRequest *OpenChannelRequest) (*OpenChannelResponse, error) { @@ -1156,7 +1195,17 @@ func (api *api) OpenChannel(ctx context.Context, openChannelRequest *OpenChannel if lnClient == nil { return nil, ErrLNClientNotStarted } - return lnClient.OpenChannel(ctx, openChannelRequest) + resp, err := lnClient.OpenChannel(ctx, &lnclient.OpenChannelRequest{ + Pubkey: openChannelRequest.Pubkey, + AmountSats: openChannelRequest.AmountSats, + Public: openChannelRequest.Public, + }) + if err != nil { + return nil, err + } + return &OpenChannelResponse{ + FundingTxId: resp.FundingTxId, + }, nil } func (api *api) DisconnectPeer(ctx context.Context, peerId string) error { @@ -1180,11 +1229,15 @@ func (api *api) CloseChannel(ctx context.Context, peerId, channelId string, forc "channel_id": channelId, "force": force, }).Info("Closing channel") - return lnClient.CloseChannel(ctx, &lnclient.CloseChannelRequest{ + err := lnClient.CloseChannel(ctx, &lnclient.CloseChannelRequest{ NodeId: peerId, ChannelId: channelId, Force: force, }) + if err != nil { + return nil, err + } + return &CloseChannelResponse{}, nil } func (api *api) UpdateChannel(ctx context.Context, updateChannelRequest *UpdateChannelRequest) error { @@ -1195,7 +1248,13 @@ func (api *api) UpdateChannel(ctx context.Context, updateChannelRequest *UpdateC logger.Logger.WithFields(logrus.Fields{ "request": updateChannelRequest, }).Info("updating channel") - return lnClient.UpdateChannel(ctx, updateChannelRequest) + return lnClient.UpdateChannel(ctx, &lnclient.UpdateChannelRequest{ + ChannelId: updateChannelRequest.ChannelId, + NodeId: updateChannelRequest.NodeId, + ForwardingFeeBaseMsat: updateChannelRequest.ForwardingFeeBaseMsat, + ForwardingFeeProportionalMillionths: updateChannelRequest.ForwardingFeeProportionalMillionths, + MaxDustHtlcExposureFromFeeRateMultiplier: updateChannelRequest.MaxDustHtlcExposureFromFeeRateMultiplier, + }) } func (api *api) MakeOffer(ctx context.Context, description string) (string, error) { @@ -1306,7 +1365,70 @@ func (api *api) GetBalances(ctx context.Context) (*BalancesResponse, error) { if err != nil { return nil, err } - return balances, nil + return toApiBalances(balances), nil +} + +func toApiBalances(balances *lnclient.BalancesResponse) *BalancesResponse { + totalSpendableMsat := balances.Lightning.TotalSpendableMsat + totalReceivableMsat := balances.Lightning.TotalReceivableMsat + nextMaxSpendableMsat := balances.Lightning.NextMaxSpendableMsat + nextMaxReceivableMsat := balances.Lightning.NextMaxReceivableMsat + nextMaxSpendableMPPMsat := balances.Lightning.NextMaxSpendableMPPMsat + nextMaxReceivableMPPMsat := balances.Lightning.NextMaxReceivableMPPMsat + + return &BalancesResponse{ + Onchain: OnchainBalanceResponse{ + Spendable: balances.Onchain.SpendableSat, + SpendableSat: balances.Onchain.SpendableSat, + Total: balances.Onchain.TotalSat, + TotalSat: balances.Onchain.TotalSat, + Reserved: balances.Onchain.ReservedSat, + ReservedSat: balances.Onchain.ReservedSat, + PendingBalancesFromChannelClosures: balances.Onchain.PendingBalancesFromChannelClosuresSat, + PendingBalancesFromChannelClosuresSat: balances.Onchain.PendingBalancesFromChannelClosuresSat, + PendingBalancesDetails: toApiPendingBalanceDetails(balances.Onchain.PendingBalancesDetails), + PendingSweepBalancesDetails: toApiPendingBalanceDetails(balances.Onchain.PendingSweepBalancesDetails), + InternalBalances: balances.Onchain.InternalBalances, + }, + Lightning: LightningBalanceResponse{ + TotalSpendable: totalSpendableMsat, + TotalSpendableSat: totalSpendableMsat / 1000, + TotalSpendableMsat: totalSpendableMsat, + TotalReceivable: totalReceivableMsat, + TotalReceivableSat: totalReceivableMsat / 1000, + TotalReceivableMsat: totalReceivableMsat, + NextMaxSpendable: nextMaxSpendableMsat, + NextMaxSpendableSat: nextMaxSpendableMsat / 1000, + NextMaxSpendableMsat: nextMaxSpendableMsat, + NextMaxReceivable: nextMaxReceivableMsat, + NextMaxReceivableSat: nextMaxReceivableMsat / 1000, + NextMaxReceivableMsat: nextMaxReceivableMsat, + NextMaxSpendableMPP: nextMaxSpendableMPPMsat, + NextMaxSpendableMPPSat: nextMaxSpendableMPPMsat / 1000, + NextMaxSpendableMPPMsat: nextMaxSpendableMPPMsat, + NextMaxReceivableMPP: nextMaxReceivableMPPMsat, + NextMaxReceivableMPPSat: nextMaxReceivableMPPMsat / 1000, + NextMaxReceivableMPPMsat: nextMaxReceivableMPPMsat, + }, + } +} + +func toApiPendingBalanceDetails(details []lnclient.PendingBalanceDetails) []PendingBalanceDetails { + if details == nil { + return nil + } + apiDetails := make([]PendingBalanceDetails, 0, len(details)) + for _, d := range details { + apiDetails = append(apiDetails, PendingBalanceDetails{ + ChannelId: d.ChannelId, + NodeId: d.NodeId, + Amount: d.AmountSat, + AmountSat: d.AmountSat, + FundingTxId: d.FundingTxId, + FundingTxVout: d.FundingTxVout, + }) + } + return apiDetails } // TODO: remove dependency on this endpoint @@ -1738,12 +1860,27 @@ func (api *api) SyncWallet() error { lnClient.UpdateLastWalletSyncRequest() return nil } -func (api *api) ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error) { +func (api *api) ListOnchainTransactions(ctx context.Context) ([]OnchainTransaction, error) { lnClient := api.svc.GetLNClient() if lnClient == nil { return nil, ErrLNClientNotStarted } - return lnClient.ListOnchainTransactions(ctx) + transactions, err := lnClient.ListOnchainTransactions(ctx) + if err != nil { + return nil, err + } + apiTransactions := make([]OnchainTransaction, 0, len(transactions)) + for _, t := range transactions { + apiTransactions = append(apiTransactions, OnchainTransaction{ + AmountSat: t.AmountSat, + CreatedAt: t.CreatedAt, + State: t.State, + Type: t.Type, + NumConfirmations: t.NumConfirmations, + TxId: t.TxId, + }) + } + return apiTransactions, nil } func (api *api) GetLogOutput(ctx context.Context, logType string, getLogRequest *GetLogOutputRequest) (*GetLogOutputResponse, error) { @@ -1819,7 +1956,11 @@ func (api *api) Health(ctx context.Context) (*HealthResponse, error) { if lnClient != nil { nodeStatus, _ := lnClient.GetNodeStatus(ctx) if nodeStatus == nil || !nodeStatus.IsReady { - alarms = append(alarms, NewHealthAlarm(HealthAlarmKindNodeNotReady, nodeStatus)) + var apiNodeStatus *NodeStatus + if nodeStatus != nil { + apiNodeStatus = toApiNodeStatus(nodeStatus) + } + alarms = append(alarms, NewHealthAlarm(HealthAlarmKindNodeNotReady, apiNodeStatus)) } channels, err := lnClient.ListChannels(ctx) diff --git a/api/models.go b/api/models.go index 4664dfbd..1761b40d 100644 --- a/api/models.go +++ b/api/models.go @@ -8,7 +8,6 @@ import ( "github.com/getAlby/hub/alby" "github.com/getAlby/hub/db" - "github.com/getAlby/hub/lnclient" "github.com/getAlby/hub/swaps" ) @@ -28,9 +27,9 @@ type API interface { 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) @@ -44,7 +43,7 @@ type API interface { 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) + 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) (*MakeInvoiceResponse, error) LookupInvoice(ctx context.Context, paymentHash string) (*LookupInvoiceResponse, error) @@ -361,11 +360,68 @@ 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"` @@ -389,8 +445,45 @@ 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 @@ -503,7 +596,7 @@ type BasicRestoreWailsRequest struct { UnlockPassword string `json:"unlockPassword"` } -type NetworkGraphResponse = lnclient.NetworkGraphResponse +type NetworkGraphResponse = interface{} type LSPOrderRequest struct { Amount *uint64 `json:"amount"` // deprecated diff --git a/lnclient/cashu/cashu.go b/lnclient/cashu/cashu.go index 5e0255b2..bc641698 100644 --- a/lnclient/cashu/cashu.go +++ b/lnclient/cashu/cashu.go @@ -171,19 +171,19 @@ func (cs *CashuService) GetNodeConnectionInfo(ctx context.Context) (nodeConnecti } func (cs *CashuService) ConnectPeer(ctx context.Context, connectPeerRequest *lnclient.ConnectPeerRequest) error { - return nil + return errors.New("not implemented") } func (cs *CashuService) OpenChannel(ctx context.Context, openChannelRequest *lnclient.OpenChannelRequest) (*lnclient.OpenChannelResponse, error) { - return nil, nil + return nil, errors.New("not implemented") } -func (cs *CashuService) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) (*lnclient.CloseChannelResponse, error) { - return nil, nil +func (cs *CashuService) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) error { + return errors.New("not implemented") } func (cs *CashuService) GetNewOnchainAddress(ctx context.Context) (string, error) { - return "", nil + return "", errors.New("not implemented") } func (cs *CashuService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainBalanceResponse, error) { @@ -191,7 +191,7 @@ func (cs *CashuService) GetOnchainBalance(ctx context.Context) (*lnclient.Onchai } func (cs *CashuService) RedeemOnchainFunds(ctx context.Context, toAddress string, amountSat uint64, feeRate *uint64, sendAll bool) (string, error) { - return "", nil + return "", errors.New("not implemented") } func (cs *CashuService) ResetRouter(key string) error { @@ -218,11 +218,11 @@ func (cs *CashuService) ResetRouter(key string) error { } func (cs *CashuService) SignMessage(ctx context.Context, message string) (string, error) { - return "", nil + return "", errors.New("not implemented") } func (cs *CashuService) DisconnectPeer(ctx context.Context, peerId string) error { - return nil + return errors.New("not implemented") } func (cs *CashuService) ListPeers(ctx context.Context) ([]lnclient.PeerDetails, error) { @@ -247,7 +247,7 @@ func (cs *CashuService) GetNodeStatus(ctx context.Context) (nodeStatus *lnclient } func (cs *CashuService) UpdateChannel(ctx context.Context, updateChannelRequest *lnclient.UpdateChannelRequest) error { - return nil + return errors.New("not implemented") } func (cs *CashuService) GetBalances(ctx context.Context, includeInactiveChannels bool) (*lnclient.BalancesResponse, error) { @@ -259,14 +259,8 @@ func (cs *CashuService) GetBalances(ctx context.Context, includeInactiveChannels PendingBalancesDetails: []lnclient.PendingBalanceDetails{}, PendingSweepBalancesDetails: []lnclient.PendingBalanceDetails{}}, Lightning: lnclient.LightningBalanceResponse{ - TotalSpendable: balance, - TotalSpendableSat: balance / 1000, TotalSpendableMsat: balance, - NextMaxSpendable: balance, - NextMaxSpendableSat: balance / 1000, NextMaxSpendableMsat: balance, - NextMaxSpendableMPP: balance, - NextMaxSpendableMPPSat: balance / 1000, NextMaxSpendableMPPMsat: balance, }, }, nil diff --git a/lnclient/cln/cln.go b/lnclient/cln/cln.go index acd11a1e..3cd39812 100644 --- a/lnclient/cln/cln.go +++ b/lnclient/cln/cln.go @@ -882,7 +882,7 @@ func clnHoldInvoiceToTransaction(invoice *clngrpcHold.Invoice, decodedInvoice *c return tx, nil } -func (c *CLNService) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) (*lnclient.CloseChannelResponse, error) { +func (c *CLNService) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) error { logger.Logger.WithFields(logrus.Fields{ "closeChannelRequest": closeChannelRequest, }).Debug("Closing Channel") @@ -901,10 +901,10 @@ func (c *CLNService) CloseChannel(ctx context.Context, closeChannelRequest *lncl _, err := c.client.Close(ctx, req) if err != nil { logger.Logger.WithError(err).Error("Failed to close channel") - return nil, fmt.Errorf("close failed: %w", err) + return fmt.Errorf("close failed: %w", err) } - return &lnclient.CloseChannelResponse{}, err + return nil } func (c *CLNService) ConnectPeer(ctx context.Context, connectPeerRequest *lnclient.ConnectPeerRequest) error { diff --git a/lnclient/ldk/ldk.go b/lnclient/ldk/ldk.go index 8ea9ce50..b1295a8a 100644 --- a/lnclient/ldk/ldk.go +++ b/lnclient/ldk/ldk.go @@ -1093,7 +1093,7 @@ func (ls *LDKService) UpdateChannel(ctx context.Context, updateChannelRequest *l return nil } -func (ls *LDKService) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) (*lnclient.CloseChannelResponse, error) { +func (ls *LDKService) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) error { logger.Logger.WithFields(logrus.Fields{ "request": closeChannelRequest, }).Info("Closing Channel") @@ -1106,9 +1106,9 @@ func (ls *LDKService) CloseChannel(ctx context.Context, closeChannelRequest *lnc } if err != nil { logger.Logger.WithError(err).Error("CloseChannel failed") - return nil, err + return err } - return &lnclient.CloseChannelResponse{}, nil + return nil } func (ls *LDKService) GetNewOnchainAddress(ctx context.Context) (string, error) { @@ -1149,7 +1149,6 @@ func (ls *LDKService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainB pendingBalancesDetails = append(pendingBalancesDetails, lnclient.PendingBalanceDetails{ NodeId: nodeId, ChannelId: channelId, - Amount: amountSat, AmountSat: amountSat, FundingTxId: fundingTxId, FundingTxVout: uint32(fundingTxIndex), @@ -1186,7 +1185,6 @@ func (ls *LDKService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainB pendingSweepBalanceDetails = append(pendingSweepBalanceDetails, lnclient.PendingBalanceDetails{ NodeId: *nodeId, ChannelId: *channelId, - Amount: amountSat, AmountSat: amountSat, FundingTxId: *fundingTxId, FundingTxVout: uint32(*fundingTxIndex), @@ -1212,13 +1210,9 @@ func (ls *LDKService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainB } return &lnclient.OnchainBalanceResponse{ - Spendable: int64(balances.SpendableOnchainBalanceSats), SpendableSat: int64(balances.SpendableOnchainBalanceSats), - Total: int64(balances.TotalOnchainBalanceSats - balances.TotalAnchorChannelsReserveSats), TotalSat: int64(balances.TotalOnchainBalanceSats - balances.TotalAnchorChannelsReserveSats), - Reserved: int64(balances.TotalAnchorChannelsReserveSats), ReservedSat: int64(balances.TotalAnchorChannelsReserveSats), - PendingBalancesFromChannelClosures: pendingBalancesFromChannelClosuresSat, PendingBalancesFromChannelClosuresSat: pendingBalancesFromChannelClosuresSat, PendingBalancesDetails: pendingBalancesDetails, PendingSweepBalancesDetails: pendingSweepBalanceDetails, @@ -1598,7 +1592,7 @@ func (ls *LDKService) handleLdkEvent(event *ldk_node.Event) { fundingTxId = details.FundingTxId fundingTxVout = details.FundingTxVout fundingTxUrl = fmt.Sprintf("https://mempool.space/tx/%s#flow=&vout=%d", fundingTxId, fundingTxVout) - pendingBalance += details.Amount + pendingBalance += details.AmountSat } } for _, details := range onchainBalance.PendingSweepBalancesDetails { @@ -1606,7 +1600,7 @@ func (ls *LDKService) handleLdkEvent(event *ldk_node.Event) { fundingTxId = details.FundingTxId fundingTxVout = details.FundingTxVout fundingTxUrl = fmt.Sprintf("https://mempool.space/tx/%s#flow=&vout=%d", fundingTxId, fundingTxVout) - pendingBalance += details.Amount + pendingBalance += details.AmountSat } } } @@ -1868,23 +1862,11 @@ func (ls *LDKService) GetBalances(ctx context.Context, includeInactiveChannels b return &lnclient.BalancesResponse{ Onchain: *onchainBalance, Lightning: lnclient.LightningBalanceResponse{ - TotalSpendable: totalSpendable, - TotalSpendableSat: totalSpendable / 1000, TotalSpendableMsat: totalSpendable, - TotalReceivable: totalReceivable, - TotalReceivableSat: totalReceivable / 1000, TotalReceivableMsat: totalReceivable, - NextMaxSpendable: nextMaxSpendable, - NextMaxSpendableSat: nextMaxSpendable / 1000, NextMaxSpendableMsat: nextMaxSpendable, - NextMaxReceivable: nextMaxReceivable, - NextMaxReceivableSat: nextMaxReceivable / 1000, NextMaxReceivableMsat: nextMaxReceivable, - NextMaxSpendableMPP: nextMaxSpendableMPP, - NextMaxSpendableMPPSat: nextMaxSpendableMPP / 1000, NextMaxSpendableMPPMsat: nextMaxSpendableMPP, - NextMaxReceivableMPP: nextMaxReceivableMPP, - NextMaxReceivableMPPSat: nextMaxReceivableMPP / 1000, NextMaxReceivableMPPMsat: nextMaxReceivableMPP, }, }, nil diff --git a/lnclient/lnd/lnd.go b/lnclient/lnd/lnd.go index cbfbb316..636bf73b 100644 --- a/lnclient/lnd/lnd.go +++ b/lnclient/lnd/lnd.go @@ -1169,7 +1169,7 @@ func (svc *LNDService) UpdateChannel(ctx context.Context, updateChannelRequest * return nil } -func (svc *LNDService) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) (*lnclient.CloseChannelResponse, error) { +func (svc *LNDService) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) error { logger.Logger.WithFields(logrus.Fields{ "request": closeChannelRequest, }).Info("Closing Channel") @@ -1177,7 +1177,7 @@ func (svc *LNDService) CloseChannel(ctx context.Context, closeChannelRequest *ln resp, err := svc.client.ListChannels(ctx, &lnrpc.ListChannelsRequest{}) if err != nil { logger.Logger.WithError(err).Error("Failed to fetch channels") - return nil, err + return err } var foundChannel *lnrpc.Channel @@ -1191,12 +1191,12 @@ func (svc *LNDService) CloseChannel(ctx context.Context, closeChannelRequest *ln if foundChannel == nil { logger.Logger.WithField("request", closeChannelRequest).Error("Failed to find channel to close") - return nil, errors.New("no channel exists with the given id") + return errors.New("no channel exists with the given id") } channelPoint, err := svc.parseChannelPoint(foundChannel.ChannelPoint) if err != nil { - return nil, err + return err } stream, err := svc.client.CloseChannel(ctx, &lnrpc.CloseChannelRequest{ @@ -1205,13 +1205,13 @@ func (svc *LNDService) CloseChannel(ctx context.Context, closeChannelRequest *ln }) if err != nil { logger.Logger.WithField("request", closeChannelRequest).WithError(err).Error("Failed to close channel") - return nil, err + return err } for { resp, err := stream.Recv() if err != nil { - return nil, err + return err } switch update := resp.Update.(type) { @@ -1219,13 +1219,13 @@ func (svc *LNDService) CloseChannel(ctx context.Context, closeChannelRequest *ln closingHash := update.ClosePending.Txid txid, err := chainhash.NewHash(closingHash) if err != nil { - return nil, err + return err } logger.Logger.WithFields(logrus.Fields{ "closingTxid": txid.String(), }).Info("Channel close pending") // TODO: return the closing tx id or fire an event - return &lnclient.CloseChannelResponse{}, nil + return nil } } } @@ -1263,7 +1263,6 @@ func (svc *LNDService) GetOnchainBalance(ctx context.Context) (*lnclient.Onchain } pendingBalancesDetails = append(pendingBalancesDetails, lnclient.PendingBalanceDetails{ NodeId: closingChannel.Channel.RemoteNodePub, - Amount: uint64(closingChannel.LimboBalance), AmountSat: uint64(closingChannel.LimboBalance), FundingTxId: channelPoint.GetFundingTxidStr(), FundingTxVout: channelPoint.GetOutputIndex(), @@ -1274,13 +1273,9 @@ func (svc *LNDService) GetOnchainBalance(ctx context.Context) (*lnclient.Onchain "balances": balances, }).Debug("Listed Balances") return &lnclient.OnchainBalanceResponse{ - Spendable: int64(balances.ConfirmedBalance), SpendableSat: int64(balances.ConfirmedBalance), - Total: int64(balances.TotalBalance), TotalSat: int64(balances.TotalBalance), - Reserved: int64(balances.ReservedBalanceAnchorChan), ReservedSat: int64(balances.ReservedBalanceAnchorChan), - PendingBalancesFromChannelClosures: pendingBalancesFromChannelClosures, PendingBalancesFromChannelClosuresSat: pendingBalancesFromChannelClosures, PendingBalancesDetails: pendingBalancesDetails, PendingSweepBalancesDetails: []lnclient.PendingBalanceDetails{}, @@ -1446,23 +1441,11 @@ func (svc *LNDService) GetBalances(ctx context.Context, includeInactiveChannels return &lnclient.BalancesResponse{ Onchain: *onchainBalance, Lightning: lnclient.LightningBalanceResponse{ - TotalSpendable: totalSpendable, - TotalSpendableSat: totalSpendable / 1000, TotalSpendableMsat: totalSpendable, - TotalReceivable: totalReceivable, - TotalReceivableSat: totalReceivable / 1000, TotalReceivableMsat: totalReceivable, - NextMaxSpendable: nextMaxSpendable, - NextMaxSpendableSat: nextMaxSpendable / 1000, NextMaxSpendableMsat: nextMaxSpendable, - NextMaxReceivable: nextMaxReceivable, - NextMaxReceivableSat: nextMaxReceivable / 1000, NextMaxReceivableMsat: nextMaxReceivable, - NextMaxSpendableMPP: nextMaxSpendableMPP, - NextMaxSpendableMPPSat: nextMaxSpendableMPP / 1000, NextMaxSpendableMPPMsat: nextMaxSpendableMPP, - NextMaxReceivableMPP: nextMaxReceivableMPP, - NextMaxReceivableMPPSat: nextMaxReceivableMPP / 1000, NextMaxReceivableMPPMsat: nextMaxReceivableMPP, }, }, nil diff --git a/lnclient/models.go b/lnclient/models.go index 43d88b87..1e62feea 100644 --- a/lnclient/models.go +++ b/lnclient/models.go @@ -5,8 +5,9 @@ import ( "errors" ) -// TODO: remove JSON tags from these models (LNClient models should not be exposed directly) - +// TLVRecord JSON tags are kept because values flow through the freeform +// transaction Metadata blob and are surfaced to NIP-47 clients via +// lookup_invoice / list_transactions. type TLVRecord struct { Type uint64 `json:"type"` // hex-encoded value @@ -42,18 +43,18 @@ type Transaction struct { } 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"` + AmountSat uint64 + CreatedAt uint64 + State string + Type string + NumConfirmations uint32 + TxId string } type NodeConnectionInfo struct { - Pubkey string `json:"pubkey"` - Address string `json:"address"` - Port int `json:"port"` + Pubkey string + Address string + Port int } type LNClient interface { @@ -73,7 +74,7 @@ type LNClient interface { GetNodeStatus(ctx context.Context) (nodeStatus *NodeStatus, err error) ConnectPeer(ctx context.Context, connectPeerRequest *ConnectPeerRequest) error OpenChannel(ctx context.Context, openChannelRequest *OpenChannelRequest) (*OpenChannelResponse, error) - CloseChannel(ctx context.Context, closeChannelRequest *CloseChannelRequest) (*CloseChannelResponse, error) + CloseChannel(ctx context.Context, closeChannelRequest *CloseChannelRequest) error UpdateChannel(ctx context.Context, updateChannelRequest *UpdateChannelRequest) error DisconnectPeer(ctx context.Context, peerId string) error MakeOffer(ctx context.Context, description string) (string, error) @@ -116,111 +117,91 @@ type Channel struct { } type NodeStatus struct { - IsReady bool `json:"isReady"` - InternalNodeStatus interface{} `json:"internalNodeStatus"` + IsReady bool + InternalNodeStatus interface{} } type ConnectPeerRequest struct { - Pubkey string `json:"pubkey"` - Address string `json:"address"` - Port uint16 `json:"port"` + Pubkey string + Address string + Port uint16 } type OpenChannelRequest struct { - Pubkey string `json:"pubkey"` - AmountSats int64 `json:"amountSats"` - Public bool `json:"public"` + Pubkey string + AmountSats int64 + Public bool } type OpenChannelResponse struct { - FundingTxId string `json:"fundingTxId"` + FundingTxId string } type CloseChannelRequest struct { - ChannelId string `json:"channelId"` - NodeId string `json:"nodeId"` - Force bool `json:"force"` + ChannelId string + NodeId string + Force bool } type UpdateChannelRequest struct { - ChannelId string `json:"channelId"` - NodeId string `json:"nodeId"` - ForwardingFeeBaseMsat uint32 `json:"forwardingFeeBaseMsat"` - ForwardingFeeProportionalMillionths uint32 `json:"forwardingFeeProportionalMillionths"` - MaxDustHtlcExposureFromFeeRateMultiplier uint64 `json:"maxDustHtlcExposureFromFeeRateMultiplier"` -} - -type CloseChannelResponse struct { + ChannelId string + NodeId string + ForwardingFeeBaseMsat uint32 + ForwardingFeeProportionalMillionths uint32 + MaxDustHtlcExposureFromFeeRateMultiplier uint64 } 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"` + ChannelId string + NodeId string + AmountSat uint64 + FundingTxId string + FundingTxVout uint32 } 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"` + SpendableSat int64 + TotalSat int64 + ReservedSat int64 + PendingBalancesFromChannelClosuresSat uint64 + PendingBalancesDetails []PendingBalanceDetails + PendingSweepBalancesDetails []PendingBalanceDetails + InternalBalances interface{} } type PeerDetails struct { - NodeId string `json:"nodeId"` - Address string `json:"address"` - IsPersisted bool `json:"isPersisted"` - IsConnected bool `json:"isConnected"` + NodeId string + Address string + IsPersisted bool + IsConnected bool } 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"` + TotalSpendableMsat int64 + TotalReceivableMsat int64 + NextMaxSpendableMsat int64 + NextMaxReceivableMsat int64 + NextMaxSpendableMPPMsat int64 + NextMaxReceivableMPPMsat int64 } type PayInvoiceResponse struct { - Preimage string `json:"preimage"` - FeeMsat uint64 `json:"feeMsat"` + Preimage string + FeeMsat uint64 } type PayOfferResponse = struct { - Preimage string `json:"preimage"` - FeeMsat uint64 `json:"feeMsat"` - PaymentHash string `json:"paymentHash"` + Preimage string + FeeMsat uint64 + PaymentHash string } type PayKeysendResponse struct { - FeeMsat uint64 `json:"feeMsat"` + FeeMsat uint64 } type BalancesResponse struct { - Onchain OnchainBalanceResponse `json:"onchain"` - Lightning LightningBalanceResponse `json:"lightning"` + Onchain OnchainBalanceResponse + Lightning LightningBalanceResponse } type NetworkGraphResponse = interface{} diff --git a/lnclient/phoenixd/phoenixd.go b/lnclient/phoenixd/phoenixd.go index 4355fe76..b3684f04 100644 --- a/lnclient/phoenixd/phoenixd.go +++ b/lnclient/phoenixd/phoenixd.go @@ -131,14 +131,8 @@ func (svc *PhoenixService) GetBalances(ctx context.Context, includeInactiveChann PendingBalancesDetails: []lnclient.PendingBalanceDetails{}, PendingSweepBalancesDetails: []lnclient.PendingBalanceDetails{}}, Lightning: lnclient.LightningBalanceResponse{ - TotalSpendable: balance, - TotalSpendableSat: balance / 1000, TotalSpendableMsat: balance, - NextMaxSpendable: balance, - NextMaxSpendableSat: balance / 1000, NextMaxSpendableMsat: balance, - NextMaxSpendableMPP: balance, - NextMaxSpendableMPPSat: balance / 1000, NextMaxSpendableMPPMsat: balance, }, }, nil @@ -351,7 +345,7 @@ func (svc *PhoenixService) RedeemOnchainFunds(ctx context.Context, toAddress str } func (svc *PhoenixService) ResetRouter(key string) error { - return nil + return errors.New("not implemented") } func (svc *PhoenixService) Shutdown() error { @@ -390,18 +384,18 @@ func (svc *PhoenixService) GetNodeConnectionInfo(ctx context.Context) (nodeConne } func (svc *PhoenixService) ConnectPeer(ctx context.Context, connectPeerRequest *lnclient.ConnectPeerRequest) error { - return nil + return errors.New("not implemented") } func (svc *PhoenixService) OpenChannel(ctx context.Context, openChannelRequest *lnclient.OpenChannelRequest) (*lnclient.OpenChannelResponse, error) { - return nil, nil + return nil, errors.New("not implemented") } -func (svc *PhoenixService) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) (*lnclient.CloseChannelResponse, error) { - return nil, nil +func (svc *PhoenixService) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) error { + return errors.New("not implemented") } func (svc *PhoenixService) GetNewOnchainAddress(ctx context.Context) (string, error) { - return "", nil + return "", errors.New("not implemented") } func (svc *PhoenixService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainBalanceResponse, error) { @@ -442,11 +436,11 @@ func (svc *PhoenixService) GetNetworkGraph(ctx context.Context, nodeIds []string func (svc *PhoenixService) UpdateLastWalletSyncRequest() {} func (svc *PhoenixService) DisconnectPeer(ctx context.Context, peerId string) error { - return nil + return errors.New("not implemented") } func (svc *PhoenixService) UpdateChannel(ctx context.Context, updateChannelRequest *lnclient.UpdateChannelRequest) error { - return nil + return errors.New("not implemented") } func (svc *PhoenixService) GetSupportedNIP47Methods() []string { @@ -499,7 +493,7 @@ func (svc *PhoenixService) GetCustomNodeCommandDefinitions() []lnclient.CustomNo } func (svc *PhoenixService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) { - return nil, nil + return nil, lnclient.ErrUnknownCustomNodeCommand } func (svc *PhoenixService) MakeOffer(ctx context.Context, description string) (string, error) { diff --git a/nip47/controllers/get_balance_controller.go b/nip47/controllers/get_balance_controller.go index a4d87e66..a8eb752f 100644 --- a/nip47/controllers/get_balance_controller.go +++ b/nip47/controllers/get_balance_controller.go @@ -53,7 +53,7 @@ func (controller *nip47Controller) HandleGetBalanceEvent(ctx context.Context, ni }, nostr.Tags{}) return } - balanceMsat = balances.Lightning.TotalSpendable + balanceMsat = balances.Lightning.TotalSpendableMsat } responsePayload := &getBalanceResponse{ diff --git a/nip47/controllers/pay_keysend_controller.go b/nip47/controllers/pay_keysend_controller.go index d75b7b1b..aab9cce2 100644 --- a/nip47/controllers/pay_keysend_controller.go +++ b/nip47/controllers/pay_keysend_controller.go @@ -11,11 +11,17 @@ import ( "github.com/sirupsen/logrus" ) +type tlvRecord struct { + Type uint64 `json:"type"` + // hex-encoded value + Value string `json:"value"` +} + type payKeysendParams struct { - Amount uint64 `json:"amount"` - Pubkey string `json:"pubkey"` - Preimage string `json:"preimage"` - TLVRecords []lnclient.TLVRecord `json:"tlv_records"` + Amount uint64 `json:"amount"` + Pubkey string `json:"pubkey"` + Preimage string `json:"preimage"` + TLVRecords []tlvRecord `json:"tlv_records"` } func (controller *nip47Controller) HandlePayKeysendEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, app *db.App, publishResponse publishFunc, tags nostr.Tags) { @@ -35,7 +41,15 @@ func (controller *nip47Controller) payKeysend(ctx context.Context, payKeysendPar "senderPubkey": payKeysendParams.Pubkey, }).Info("Sending keysend payment") - transaction, err := controller.transactionsService.SendKeysend(payKeysendParams.Amount, payKeysendParams.Pubkey, payKeysendParams.TLVRecords, payKeysendParams.Preimage, controller.lnClient, &app.ID, &requestEventId) + tlvRecords := make([]lnclient.TLVRecord, 0, len(payKeysendParams.TLVRecords)) + for _, r := range payKeysendParams.TLVRecords { + tlvRecords = append(tlvRecords, lnclient.TLVRecord{ + Type: r.Type, + Value: r.Value, + }) + } + + transaction, err := controller.transactionsService.SendKeysend(payKeysendParams.Amount, payKeysendParams.Pubkey, tlvRecords, payKeysendParams.Preimage, controller.lnClient, &app.ID, &requestEventId) if err != nil { logger.Logger.WithFields(logrus.Fields{ "request_event_id": requestEventId, diff --git a/swaps/swaps_service.go b/swaps/swaps_service.go index 6de1f10a..407dd372 100644 --- a/swaps/swaps_service.go +++ b/swaps/swaps_service.go @@ -213,7 +213,7 @@ func (svc *swapsService) EnableAutoSwapOut(encryptionKey string) error { logger.Logger.WithError(err).Error("Failed to get balance") continue } - lightningBalance := uint64(balance.Lightning.TotalSpendable) + lightningBalance := uint64(balance.Lightning.TotalSpendableMsat) balanceThresholdMilliSats := balanceThreshold * 1000 if lightningBalance < balanceThresholdMilliSats { logger.Logger.Info("Threshold requirements not met for swap, ignoring") diff --git a/tests/mock_ln_client.go b/tests/mock_ln_client.go index 07ca25c8..35c775d2 100644 --- a/tests/mock_ln_client.go +++ b/tests/mock_ln_client.go @@ -29,8 +29,6 @@ var MockNodeInfo = lnclient.NodeInfo{ var MockLNClientBalances = lnclient.BalancesResponse{ Lightning: lnclient.LightningBalanceResponse{ - TotalSpendable: 21000, - TotalSpendableSat: 21, TotalSpendableMsat: 21000, }, } @@ -181,8 +179,8 @@ func (mln *MockLn) ConnectPeer(ctx context.Context, connectPeerRequest *lnclient func (mln *MockLn) OpenChannel(ctx context.Context, openChannelRequest *lnclient.OpenChannelRequest) (*lnclient.OpenChannelResponse, error) { return nil, nil } -func (mln *MockLn) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) (*lnclient.CloseChannelResponse, error) { - return nil, nil +func (mln *MockLn) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) error { + return nil } func (mln *MockLn) GetNewOnchainAddress(ctx context.Context) (string, error) { return "", nil diff --git a/tests/mocks/LNClient.go b/tests/mocks/LNClient.go index 3a0e6dec..f230c47d 100644 --- a/tests/mocks/LNClient.go +++ b/tests/mocks/LNClient.go @@ -96,31 +96,20 @@ func (_c *MockLNClient_CancelHoldInvoice_Call) RunAndReturn(run func(ctx context } // CloseChannel provides a mock function for the type MockLNClient -func (_mock *MockLNClient) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) (*lnclient.CloseChannelResponse, error) { +func (_mock *MockLNClient) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) error { ret := _mock.Called(ctx, closeChannelRequest) if len(ret) == 0 { panic("no return value specified for CloseChannel") } - var r0 *lnclient.CloseChannelResponse - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, *lnclient.CloseChannelRequest) (*lnclient.CloseChannelResponse, error)); ok { - return returnFunc(ctx, closeChannelRequest) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, *lnclient.CloseChannelRequest) *lnclient.CloseChannelResponse); ok { + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *lnclient.CloseChannelRequest) error); ok { r0 = returnFunc(ctx, closeChannelRequest) } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*lnclient.CloseChannelResponse) - } + r0 = ret.Error(0) } - if returnFunc, ok := ret.Get(1).(func(context.Context, *lnclient.CloseChannelRequest) error); ok { - r1 = returnFunc(ctx, closeChannelRequest) - } else { - r1 = ret.Error(1) - } - return r0, r1 + return r0 } // MockLNClient_CloseChannel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CloseChannel' @@ -153,12 +142,12 @@ func (_c *MockLNClient_CloseChannel_Call) Run(run func(ctx context.Context, clos return _c } -func (_c *MockLNClient_CloseChannel_Call) Return(closeChannelResponse *lnclient.CloseChannelResponse, err error) *MockLNClient_CloseChannel_Call { - _c.Call.Return(closeChannelResponse, err) +func (_c *MockLNClient_CloseChannel_Call) Return(err error) *MockLNClient_CloseChannel_Call { + _c.Call.Return(err) return _c } -func (_c *MockLNClient_CloseChannel_Call) RunAndReturn(run func(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) (*lnclient.CloseChannelResponse, error)) *MockLNClient_CloseChannel_Call { +func (_c *MockLNClient_CloseChannel_Call) RunAndReturn(run func(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) error) *MockLNClient_CloseChannel_Call { _c.Call.Return(run) return _c } From e29fe81eb44e7c1da4217414bc5a1b14b221f2a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= <100827540+reneaaron@users.noreply.github.com> Date: Mon, 25 May 2026 17:08:29 +0200 Subject: [PATCH 014/136] 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 * 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 * 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 --------- Co-authored-by: Claude Opus 4.7 --- frontend/src/hooks/useRegisterProtocolHandler.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/frontend/src/hooks/useRegisterProtocolHandler.ts b/frontend/src/hooks/useRegisterProtocolHandler.ts index 90ab0b43..912f9aff 100644 --- a/frontend/src/hooks/useRegisterProtocolHandler.ts +++ b/frontend/src/hooks/useRegisterProtocolHandler.ts @@ -2,6 +2,8 @@ import React from "react"; import { isHttpMode } from "src/utils/isHttpMode"; +const STORAGE_KEY = "bitcoin-protocol-handler-registered"; + export function useRegisterProtocolHandler(basePath: string) { React.useEffect(() => { if (!isHttpMode() || !("registerProtocolHandler" in navigator)) { @@ -9,9 +11,20 @@ export function useRegisterProtocolHandler(basePath: string) { } try { + // Browsers re-prompt every time registerProtocolHandler is called if the + // user previously dismissed the prompt without accepting or denying it. + // Limit to once per browser session so we don't ask on every page load, + // but users still get re-asked in a new session if they didn't opt in. + // sessionStorage access can throw in restricted/private modes, so it's + // inside the same try/catch as registerProtocolHandler. + if (sessionStorage.getItem(STORAGE_KEY)) { + return; + } + const normalizedBasePath = basePath.replace(/\/$/, ""); const handlerUrl = `${window.location.origin}${normalizedBasePath}/wallet/send?bip21=%s`; navigator.registerProtocolHandler("bitcoin", handlerUrl); + sessionStorage.setItem(STORAGE_KEY, "true"); } catch (e) { console.error("Failed to register bitcoin protocol handler", e); } From 318c6228875810ac0d47193ebbf8bbc2e79c3eae Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Tue, 26 May 2026 14:38:25 +0700 Subject: [PATCH 015/136] chore: simplify kyc info on cards page (#2378) --- frontend/src/screens/cards/Cards.tsx | 59 +++++++++++++--------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/frontend/src/screens/cards/Cards.tsx b/frontend/src/screens/cards/Cards.tsx index 095572e4..aac35f3c 100644 --- a/frontend/src/screens/cards/Cards.tsx +++ b/frontend/src/screens/cards/Cards.tsx @@ -82,7 +82,7 @@ type Provider = { googlePay: boolean; selfCustody: boolean; lightningNative: boolean; - kyc: "Full" | "Light" | "None"; + kyc: boolean; timeToGet: string; cardCost: string; fees: string; @@ -108,7 +108,7 @@ const providers: Provider[] = [ googlePay: true, selfCustody: false, lightningNative: false, - kyc: "Light", + kyc: true, timeToGet: "<10 minutes", cardCost: "$10", fees: "~2.2% + FX", @@ -127,7 +127,7 @@ const providers: Provider[] = [ googlePay: true, selfCustody: false, lightningNative: true, - kyc: "None", + kyc: false, timeToGet: "Instant", cardCost: "$50", fees: "~6.8% top-up + $0.50", @@ -146,7 +146,7 @@ const providers: Provider[] = [ googlePay: true, selfCustody: false, lightningNative: false, - kyc: "None", + kyc: false, timeToGet: "Instant", cardCost: "$5–30 / mo", fees: "1.3–4.3%", @@ -167,7 +167,7 @@ const providers: Provider[] = [ googlePay: true, selfCustody: false, lightningNative: true, - kyc: "Full", + kyc: true, timeToGet: "Minutes", // Their cards page advertises a €3.49/mo subscription that bundles // both cards. Worth re-confirming during checkout — sources disagree. @@ -189,7 +189,7 @@ const providers: Provider[] = [ googlePay: true, selfCustody: false, lightningNative: true, - kyc: "Light", + kyc: true, timeToGet: "Minutes", cardCost: "€2.99 / €29.99", fees: "1% + 0.5%", @@ -271,7 +271,7 @@ export function Cards() { if (f === "Lightning-native" && !p.lightningNative) { return false; } - if (f === "No KYC" && p.kyc !== "None") { + if (f === "No KYC" && p.kyc) { return false; } } @@ -440,7 +440,7 @@ export function Cards() { Lightning )} - {providers.some((p) => p.kyc === "None") && ( + {providers.some((p) => !p.kyc) && ( No KYC @@ -833,11 +833,11 @@ function ConnectCardDialog({ } function KycBadge({ kyc }: { kyc: Provider["kyc"] }) { - if (kyc === "None") { + if (!kyc) { return ( - None + No @@ -845,27 +845,10 @@ function KycBadge({ kyc }: { kyc: Provider["kyc"] }) { - No-KYC cards typically operate via a single merchant-of-record - account. Privacy-friendly, but operationally fragile — the program - can be paused or shut down without notice. - - - - ); - } - if (kyc === "Light") { - return ( - - Light - - - - - - - - ID verification only — no proof of address, employer details, or - source-of-funds questions. + No identity verification required. These cards typically operate via + a single merchant-of-record account — privacy-friendly, but + operationally fragile, and the program can be paused or shut down + without notice. @@ -873,7 +856,19 @@ function KycBadge({ kyc }: { kyc: Provider["kyc"] }) { } return ( - {kyc} + Yes + + + + + + + + Identity verification required. What you'll need depends on your + passport and country — it can be as little as a passport and selfie, + or also include a tax number, proof of address, and other details. + + ); } From 48ab9efe2c15e6ee53fe78acbc8a25c3dbd51b4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Tue, 26 May 2026 14:06:42 +0200 Subject: [PATCH 016/136] chore: use redotpay referral link on cards page --- frontend/src/screens/cards/Cards.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/screens/cards/Cards.tsx b/frontend/src/screens/cards/Cards.tsx index aac35f3c..0d88c061 100644 --- a/frontend/src/screens/cards/Cards.tsx +++ b/frontend/src/screens/cards/Cards.tsx @@ -98,7 +98,7 @@ const providers: Provider[] = [ { id: "redotpay", name: "RedotPay", - url: "https://www.redotpay.com", + url: "https://wap.redotpay.com/en/invite/?referralId=qr1a5", logo: redotpayLogo, initials: "RP", network: "Visa", From efee2b85e88b4d16c99521c82115030243e790df Mon Sep 17 00:00:00 2001 From: saunter <68239231+stackingsaunter@users.noreply.github.com> Date: Wed, 27 May 2026 18:11:45 +0200 Subject: [PATCH 017/136] 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 --- .../src/components/AnchorReserveAlert.tsx | 14 +- .../src/components/CurrencyInputField.tsx | 524 ++++++++++++++++++ .../screens/wallet/receive/ReceiveInvoice.tsx | 44 +- .../screens/wallet/receive/ReceiveOnchain.tsx | 70 +-- frontend/src/screens/wallet/send/LnurlPay.tsx | 55 +- frontend/src/screens/wallet/send/Onchain.tsx | 126 ++--- .../src/screens/wallet/send/ZeroAmount.tsx | 55 +- frontend/src/screens/wallet/swap/AutoSwap.tsx | 53 +- frontend/src/screens/wallet/swap/index.tsx | 150 ++--- 9 files changed, 730 insertions(+), 361 deletions(-) create mode 100644 frontend/src/components/CurrencyInputField.tsx diff --git a/frontend/src/components/AnchorReserveAlert.tsx b/frontend/src/components/AnchorReserveAlert.tsx index a4aa1ee5..0cf5983f 100644 --- a/frontend/src/components/AnchorReserveAlert.tsx +++ b/frontend/src/components/AnchorReserveAlert.tsx @@ -32,12 +32,14 @@ export function AnchorReserveAlert({ Channel Anchor Reserves will be depleted - 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{" "} - {" "} - on-chain. +

+ 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{" "} + {" "} + on-chain. +

); diff --git a/frontend/src/components/CurrencyInputField.tsx b/frontend/src/components/CurrencyInputField.tsx new file mode 100644 index 00000000..56456195 --- /dev/null +++ b/frontend/src/components/CurrencyInputField.tsx @@ -0,0 +1,524 @@ +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, + "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 ( + + {unit === "₿" && {unit}} + {amount} + {unit !== "₿" && {unit}} + + ); +} + +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("bitcoin"); + const [fiatValue, setFiatValue] = React.useState(""); + const [bitcoinDenomination, setBitcoinDenomination] = + React.useState("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) { + 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 ( + + {label && {label}} + + + + {isFiatMode ? ( + + {getCurrencySymbol(currency)} + + ) : ( + + {isBtcDenominated ? "BTC" : bitcoinUnit} + + )} + + + {isFiatMode ? ( + + {alternateBitcoinValue.unit === "₿" && ( + {alternateBitcoinValue.unit} + )} + + {alternateBitcoinValue.amount} + + {alternateBitcoinValue.unit !== "₿" && ( + {alternateBitcoinValue.unit} + )} + + ) : ( + + {alternateValue ?? } + + )} + + + {!!contextRows?.length && ( +
+ {contextRows.map((row) => ( +
+ {row.label}: + + {row.value ?? ( + + )} + +
+ ))} +
+ )} + {description && {description}} + {error && {error}} +
+ ); +} diff --git a/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx b/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx index b025b20b..ad593eca 100644 --- a/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx +++ b/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx @@ -9,6 +9,7 @@ import TickSVG from "public/images/illustrations/tick.svg"; import React from "react"; import { toast } from "sonner"; import AppHeader from "src/components/AppHeader"; +import { CurrencyInputField } from "src/components/CurrencyInputField"; import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; import FormattedFiatAmount from "src/components/FormattedFiatAmount"; import Loading from "src/components/Loading"; @@ -23,7 +24,6 @@ import { CardTitle, } from "src/components/ui/card"; import { ExternalLinkButton } from "src/components/ui/custom/external-link-button"; -import { InputWithAdornment } from "src/components/ui/custom/input-with-adornment"; import { LinkButton } from "src/components/ui/custom/link-button"; import { LoadingButton } from "src/components/ui/custom/loading-button"; import { Input } from "src/components/ui/input"; @@ -197,26 +197,28 @@ export default function ReceiveInvoice() { ) : (
-
- - { - setAmountSat(e.target.value.trim()); - }} - min={1} - autoFocus - endAdornment={ - - } - /> -
+
)} -
- - setSwapAmountSat(e.target.value)} - required - endAdornment={ - - } - /> -
-
-
- Receiving Capacity:{" "} - -
- -
-
-
+ : swapInfo.maxAmountSat + : hasChannelManagement + ? balances.lightning.totalReceivableSat * 0.99 + : undefined + } + required + contextRows={ + hasChannelManagement + ? [ + { + label: "Receive limit", + amountSat: balances.lightning.totalReceivableSat, + }, + ] + : undefined + } + />
)} -
- - { - setAmountSat(e.target.value.trim()); - }} - min={1} - max={balances.lightning.totalSpendableSat} - required - autoFocus - endAdornment={ - - } - /> -
-
-
- Lightning Balance:{" "} - -
- -
-
-
+ {!!lnAddress.lnurlpData?.commentAllowed && (
diff --git a/frontend/src/screens/wallet/send/Onchain.tsx b/frontend/src/screens/wallet/send/Onchain.tsx index 51d1c8da..3b966137 100644 --- a/frontend/src/screens/wallet/send/Onchain.tsx +++ b/frontend/src/screens/wallet/send/Onchain.tsx @@ -10,15 +10,13 @@ import { Link, useLocation, useNavigate } from "react-router"; import { toast } from "sonner"; import { AnchorReserveAlert } from "src/components/AnchorReserveAlert"; import AppHeader from "src/components/AppHeader"; +import { CurrencyInputField } from "src/components/CurrencyInputField"; import ExternalLink from "src/components/ExternalLink"; -import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; -import FormattedFiatAmount from "src/components/FormattedFiatAmount"; import { InsufficientLightningBalanceAlert } from "src/components/InsufficientLightningBalanceAlert"; import Loading from "src/components/Loading"; import { MempoolAlert } from "src/components/MempoolAlert"; import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert"; import { Button } from "src/components/ui/button"; -import { InputWithAdornment } from "src/components/ui/custom/input-with-adornment"; import { LinkButton } from "src/components/ui/custom/link-button"; import { LoadingButton } from "src/components/ui/custom/loading-button"; import { Input } from "src/components/ui/input"; @@ -190,40 +188,21 @@ function OnchainForm({ return ( -
- - { - setAmountSat(e.target.value.trim()); - }} - min={ONCHAIN_DUST_SATS} - max={balances.onchain.spendableSat} - required - autoFocus - endAdornment={ - - } - /> -
-
- On-chain Balance:{" "} - -
- -
-
+
@@ -405,35 +368,28 @@ function SwapOutForm() {

- - setSwapAmountSat(e.target.value)} required + contextRows={[ + { + label: "Lightning balance", + amountSat: balances.lightning.totalSpendableSat, + }, + { + label: "Minimum", + amountSat: swapInfo.minAmountSat, + }, + ]} /> - -
- {balances && ( -

- Balance:{" "} - -

- )} -

- Minimum:{" "} - -

-
From 81b1b2f69584e42cad020349703a740cc86bea41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= <100827540+reneaaron@users.noreply.github.com> Date: Fri, 29 May 2026 14:11:09 +0200 Subject: [PATCH 018/136] fix: prevent scrollbar on incoming capacity page (#2383) --- frontend/src/screens/channels/IncreaseIncomingCapacity.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/screens/channels/IncreaseIncomingCapacity.tsx b/frontend/src/screens/channels/IncreaseIncomingCapacity.tsx index df803b04..d059eaa0 100644 --- a/frontend/src/screens/channels/IncreaseIncomingCapacity.tsx +++ b/frontend/src/screens/channels/IncreaseIncomingCapacity.tsx @@ -487,8 +487,8 @@ function NewChannelInternal({ -
-

+

+

Other options

Date: Tue, 2 Jun 2026 10:50:15 +0200 Subject: [PATCH 019/136] feat: stories (#2172) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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) * chore(stories): post-review cleanups - Use react-router Link for in-tab CTA instead of plain . - 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) * 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) * 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) * 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) * 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) * 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) * 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) * 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) * 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) * Revert "chore(stories): bump story button to w-24 so titles fit one line" This reverts commit 0b47438f501aee4b2a91827bec4267ac06072747. * 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 Co-authored-by: Claude Opus 4.7 (1M context) --- alby/alby_oauth_service.go | 46 ++++ alby/models.go | 15 ++ api/api.go | 4 + api/models.go | 1 + .../components/home/widgets/StoriesWidget.tsx | 251 ++++++++++++++++++ frontend/src/constants.ts | 1 + frontend/src/screens/Home.tsx | 2 + frontend/vite.config.ts | 2 +- http/alby_http_service.go | 12 + http/http_service.go | 2 +- tests/mocks/AlbyOAuthService.go | 28 ++ wails/wails_handlers.go | 11 + 12 files changed, 373 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/home/widgets/StoriesWidget.tsx diff --git a/alby/alby_oauth_service.go b/alby/alby_oauth_service.go index 445412a6..bba77dae 100644 --- a/alby/alby_oauth_service.go +++ b/alby/alby_oauth_service.go @@ -1369,6 +1369,52 @@ func (svc *albyOAuthService) requestAutoChannel(ctx context.Context, url string, }, 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) diff --git a/alby/models.go b/alby/models.go index 2fe5e6b3..9c216846 100644 --- a/alby/models.go +++ b/alby/models.go @@ -32,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 { @@ -153,6 +154,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"` diff --git a/api/api.go b/api/api.go index 688061ed..c43a5fbe 100644 --- a/api/api.go +++ b/api/api.go @@ -756,6 +756,10 @@ func (api *api) GetChannelPeerSuggestions(ctx context.Context) ([]alby.ChannelPe return api.albySvc.GetChannelPeerSuggestions(ctx) } +func (api *api) GetStories(ctx context.Context) ([]alby.Story, error) { + return api.albyOAuthSvc.GetStories(ctx) +} + func (api *api) GetLSPChannelOffer(ctx context.Context) (*alby.LSPChannelOffer, error) { return api.albyOAuthSvc.GetLSPChannelOffer(ctx) } diff --git a/api/models.go b/api/models.go index 1761b40d..0f0cf279 100644 --- a/api/models.go +++ b/api/models.go @@ -22,6 +22,7 @@ type API interface { 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 diff --git a/frontend/src/components/home/widgets/StoriesWidget.tsx b/frontend/src/components/home/widgets/StoriesWidget.tsx new file mode 100644 index 00000000..8c585c2e --- /dev/null +++ b/frontend/src/components/home/widgets/StoriesWidget.tsx @@ -0,0 +1,251 @@ +import { XIcon } from "lucide-react"; +import React from "react"; +import { Link } from "react-router"; +import useSWR from "swr"; +import ExternalLink from "src/components/ExternalLink"; +import { Button } from "src/components/ui/button"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "src/components/ui/card"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogTitle, +} from "src/components/ui/dialog"; +import { localStorageKeys } from "src/constants"; +import { cn } from "src/lib/utils"; +import { swrFetcher } from "src/utils/swr"; + +type StoryCta = { + label: string; + url: string; + openInNewTab: boolean; +}; + +type Story = { + id: string; + title: string; + avatar: string; + videoId?: string; + cta?: StoryCta; +}; + +type StoryApiResponse = { + id: number; + title: string; + avatar: string; + videoId?: string; + cta?: StoryCta; +}; + +function youTubeEmbedUrl(videoId: string) { + return `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&rel=0`; +} + +function loadViewedStoryIds(): Set { + try { + const raw = localStorage.getItem(localStorageKeys.homeStoriesViewed); + if (!raw) { + return new Set(); + } + const parsed = JSON.parse(raw) as unknown; + if (!Array.isArray(parsed)) { + return new Set(); + } + return new Set(parsed.filter((id): id is string => typeof id === "string")); + } catch { + return new Set(); + } +} + +function persistViewedStoryIds(ids: Set) { + try { + localStorage.setItem( + localStorageKeys.homeStoriesViewed, + JSON.stringify([...ids]) + ); + } catch { + // ignore quota / private mode + } +} + +function StoryAvatar({ story, viewed }: { story: Story; viewed: boolean }) { + return ( +
+
+ {`${story.title} +
+
+ ); +} + +export function StoriesWidget() { + const { data, error, isLoading } = useSWR( + "/api/alby/stories", + swrFetcher + ); + const [activeStory, setActiveStory] = React.useState(null); + const [viewedIds, setViewedIds] = + React.useState>(loadViewedStoryIds); + + const stories = React.useMemo( + () => + error || !data + ? [] + : data.map((story) => ({ + id: String(story.id), + title: story.title, + avatar: story.avatar, + videoId: story.videoId, + cta: story.cta, + })), + [data, error] + ); + + const markStoryViewed = React.useCallback((storyId: string) => { + setViewedIds((prev) => { + if (prev.has(storyId)) { + return prev; + } + const next = new Set(prev); + next.add(storyId); + persistViewedStoryIds(next); + return next; + }); + }, []); + + if (!isLoading && stories.length === 0) { + return null; + } + + return ( + <> + + + Stories + + +
+ {isLoading && ( + + Loading stories... + + )} + {!isLoading && + stories.map((story) => { + const viewed = viewedIds.has(story.id); + return ( + + ); + })} +
+
+
+ + !open && setActiveStory(null)} + > + + {activeStory && ( +
+ {activeStory.title} + + Watch the latest update + + + {activeStory.videoId && ( +
+ -
- - - - Confirm payment - -
-
-
Description
-
{invoice?.description}
-
-
-
Amount
-
- - - - -
-
-
- - Cancel - - {loading && } - Pay now - - -
-
- - ); -} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 95203415..e34442fe 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -98,7 +98,7 @@ const insertDevCSPPlugin: Plugin = { "", ` - ` + ` ); }, }, diff --git a/http/http_service.go b/http/http_service.go index bc1b67ce..b36091a6 100644 --- a/http/http_service.go +++ b/http/http_service.go @@ -66,7 +66,7 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) { e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ ContentTypeNosniff: "nosniff", XFrameOptions: "DENY", - ContentSecurityPolicy: "default-src 'self'; img-src 'self' https://uploads.getalby-assets.com https://cdn.getalby-assets.com https://getalby.com; connect-src 'self' https://api.getalby.com https://getalby.com https://zapplanner.albylabs.com wss://relay.getalby.com wss://relay2.getalby.com; frame-src https://embed.bitrefill.com https://www.youtube-nocookie.com", + ContentSecurityPolicy: "default-src 'self'; img-src 'self' https://uploads.getalby-assets.com https://cdn.getalby-assets.com https://getalby.com; connect-src 'self' https://api.getalby.com https://getalby.com https://zapplanner.albylabs.com wss://relay.getalby.com wss://relay2.getalby.com; frame-src https://www.youtube-nocookie.com", ReferrerPolicy: "no-referrer", })) e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ From 2657c89aa8381822f5db64bc575a54c9d2ba5cbd Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Tue, 9 Jun 2026 20:50:30 +0700 Subject: [PATCH 053/136] feat: reframe AI agent inspiration tab around managing the hub (#2418) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * fix: icon * chore: name * chore: remove stray .claude/worktrees gitlinks --------- Co-authored-by: Claude Opus 4.8 Co-authored-by: René Aaron --- frontend/src/screens/ai/AI.tsx | 182 ++++++++++++++++++--------------- 1 file changed, 100 insertions(+), 82 deletions(-) diff --git a/frontend/src/screens/ai/AI.tsx b/frontend/src/screens/ai/AI.tsx index 9ed7597c..65ccc6d0 100644 --- a/frontend/src/screens/ai/AI.tsx +++ b/frontend/src/screens/ai/AI.tsx @@ -63,6 +63,7 @@ import { DEFAULT_APP_BUDGET_SATS, localStorageKeys, } from "src/constants"; +import { useInfo } from "src/hooks/useInfo"; import { copyToClipboard } from "src/lib/clipboard"; import { createApp } from "src/requests/createApp"; import { handleRequestError } from "src/utils/handleRequestError"; @@ -680,95 +681,108 @@ const whyLightningItems = [ }, ]; -const inspirationCategories: { +function getInspirationCategories(hasChannelManagement: boolean): { label: string; icon: LucideIcon; prompts: string[]; skill?: { prompt: string; skillName: string; url: string }; -}[] = [ - { - label: "Wallet", - icon: ZapIcon, - prompts: [ - "send $5 to hub@getalby.com for coffee", - "how much is $10 in sats right now?", - "make an invoice for 50,000 sats", - ], - }, - { - label: "Shopping", - icon: ShoppingBagIcon, - prompts: [ - "buy a $25 Netflix gift card on bitrefill.com", - "get me an eSIM with 5GB of data for my trip to Portugal on bitrefill.com", - "what gift cards are available in the US on bitrefill.com?", - ], - skill: { - prompt: - "Run `npx -y skills add bitrefill/agents -y` to install the Bitrefill Skill", - skillName: "Bitrefill Skill", - url: "https://bitrefill.com/agents", +}[] { + return [ + { + label: "Wallet", + icon: ZapIcon, + prompts: [ + "send $5 to hub@getalby.com for coffee", + "how much is $10 in sats right now?", + "make an invoice for 50,000 sats", + ], }, - }, - { - label: "Creative", - icon: SparklesIcon, - prompts: [ - "generate a watercolor painting of a mountain cabin at sunset on ppq.ai", - "generate a cool bitcoin logo on ppq.ai and print it on a t-shirt on unhuman.store", - "create a logo for my coffee shop using ppq.ai image generation", - ], - }, - { - label: "Services", - icon: LayersIcon, - prompts: [ - "search podcasts for discussions about bitcoin scaling", - "set up an anonymous email address on lnemail.net", - "buy the domain my-awesome-project.dev on unhuman.domains", - "spin up a VPS with 2 cores and 4GB RAM on lnvps.net", - ], - }, - { - label: "Automation", - icon: RepeatIcon, - prompts: [ - "read payouts.csv and send 1,000 sats to each lightning address", - "calculate how much I spent this month and break it down by day", - "export all my transactions from the last 12 months as a CSV", - ], - }, - { - label: "Build Apps", - icon: HammerIcon, - prompts: [ - "build an AI image generator that charges 500 sats per image", - "create a file converter that charges 50 sats per conversion", - "build a blog where readers unlock articles for 50 sats each", - ], - skill: { - prompt: - "Run `npx -y skills add getAlby/builder-skill -y` to install the Builder Skill", - skillName: "Builder Skill", - url: "https://github.com/getAlby/builder-skill", + { + label: "Shopping", + icon: ShoppingBagIcon, + prompts: [ + "buy a $25 Netflix gift card on bitrefill.com", + "get me an eSIM with 5GB of data for my trip to Portugal on bitrefill.com", + "what gift cards are available in the US on bitrefill.com?", + ], + skill: { + prompt: + "Run `npx -y skills add bitrefill/agents -y` to install the Bitrefill Skill", + skillName: "Bitrefill Skill", + url: "https://bitrefill.com/agents", + }, }, - }, - { - label: "Node", - icon: BoxIcon, - prompts: [ - "open a channel with 2M sats to ACINQ's node", - "show me my channels and their balances", - "what's my node's connection info?", - ], - skill: { - prompt: - "Run `npx -y skills add getAlby/hub-skill -y` to install the Alby Hub Skill", - skillName: "Alby Hub Skill", - url: "https://github.com/getAlby/hub-skill", + { + label: "Creative", + icon: SparklesIcon, + prompts: [ + "generate a watercolor painting of a mountain cabin at sunset on ppq.ai", + "generate a cool bitcoin logo on ppq.ai and print it on a t-shirt on unhuman.store", + "create a logo for my coffee shop using ppq.ai image generation", + ], }, - }, -]; + { + label: "Services", + icon: LayersIcon, + prompts: [ + "search podcasts for discussions about bitcoin scaling", + "set up an anonymous email address on lnemail.net", + "buy the domain my-awesome-project.dev on unhuman.domains", + "spin up a VPS with 2 cores and 4GB RAM on lnvps.net", + ], + }, + { + label: "Automation", + icon: RepeatIcon, + prompts: [ + "read payouts.csv and send 1,000 sats to each lightning address", + "calculate how much I spent this month and break it down by day", + "export all my transactions from the last 12 months as a CSV", + ], + }, + { + label: "Build Apps", + icon: HammerIcon, + prompts: [ + "build an AI image generator that charges 500 sats per image", + "create a file converter that charges 50 sats per conversion", + "build a blog where readers unlock articles for 50 sats each", + ], + skill: { + prompt: + "Run `npx -y skills add getAlby/builder-skill -y` to install the Builder Skill", + skillName: "Builder Skill", + url: "https://github.com/getAlby/builder-skill", + }, + }, + { + label: "Alby Hub", + icon: BoxIcon, + prompts: [ + "create a sub-wallet for my mum", + "setup a new alby hub on my VPS", + "give me an on-chain deposit address", + "create a new app connection with a 21,000 sat monthly budget", + "list all my app connections and their budgets", + "make a read-only connection I can share with my accountant", + 'revoke the connection called "old laptop"', + ...(hasChannelManagement + ? [ + "open a channel with 2M sats to ACINQ's node", + "show me my channels and their balances", + "what's my node's connection info?", + ] + : []), + ], + skill: { + prompt: + "Run `npx -y skills add getAlby/hub-skill -y` to install the Alby Hub Skill", + skillName: "Alby Hub Skill", + url: "https://github.com/getAlby/hub-skill", + }, + }, + ]; +} function RotatingPrompt({ prompts }: { prompts: string[] }) { const [index, setIndex] = React.useState(0); @@ -832,6 +846,10 @@ function RotatingPrompt({ prompts }: { prompts: string[] }) { } function InspirationPrompts() { + const { hasChannelManagement } = useInfo(); + const inspirationCategories = + getInspirationCategories(!!hasChannelManagement); + return (
From d1636cbcce603a12a6a0e30167235b4586380fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= <100827540+reneaaron@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:52:40 +0200 Subject: [PATCH 054/136] fix: reduce wallet balance/transaction polling interval to 10s (#2425) * 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 --- frontend/src/hooks/useBalances.ts | 2 +- frontend/src/hooks/useTransactions.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/hooks/useBalances.ts b/frontend/src/hooks/useBalances.ts index 74809356..e6352bb3 100644 --- a/frontend/src/hooks/useBalances.ts +++ b/frontend/src/hooks/useBalances.ts @@ -4,7 +4,7 @@ import { BalancesResponse } from "src/types"; import { swrFetcher } from "src/utils/swr"; const pollConfiguration: SWRConfiguration = { - refreshInterval: 3000, + refreshInterval: 10000, }; export function useBalances(poll = false) { diff --git a/frontend/src/hooks/useTransactions.ts b/frontend/src/hooks/useTransactions.ts index 90d8f00c..492e4e65 100644 --- a/frontend/src/hooks/useTransactions.ts +++ b/frontend/src/hooks/useTransactions.ts @@ -4,7 +4,7 @@ import { ListTransactionsResponse } from "src/types"; import { swrFetcher } from "src/utils/swr"; const pollConfiguration: SWRConfiguration = { - refreshInterval: 3000, + refreshInterval: 10000, }; export function getTransactionsUrl(appId?: number, limit = 100, page = 1) { From 8fc5cb25b4d4833c365b736981653e05e8d8b970 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:39:51 +0700 Subject: [PATCH 055/136] chore: prevent committing worktrees (#2427) --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1b519bd7..7cfd951f 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ glalby *.db-shm *.db-wal *.db-journal -albyhub-data \ No newline at end of file +albyhub-data +.claude/worktrees \ No newline at end of file From 3453b69a1cba4aa1faeaac0e810b72fcfbb6faa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= <100827540+reneaaron@users.noreply.github.com> Date: Wed, 10 Jun 2026 08:00:43 +0200 Subject: [PATCH 056/136] feat: refine wallet empty states (#2382) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- frontend/src/components/EmptyState.tsx | 27 ++++++++++++------- .../components/OnchainTransactionsList.tsx | 9 +++---- frontend/src/components/TransactionsList.tsx | 23 +++++++++------- .../ChannelWaitingForConfirmations.tsx | 1 + .../connections/AppTransactionList.tsx | 9 ++++++- .../components/connections/ConnectedApps.tsx | 1 + frontend/src/screens/channels/Channels.tsx | 1 + 7 files changed, 44 insertions(+), 27 deletions(-) diff --git a/frontend/src/components/EmptyState.tsx b/frontend/src/components/EmptyState.tsx index 9604399c..829705b2 100644 --- a/frontend/src/components/EmptyState.tsx +++ b/frontend/src/components/EmptyState.tsx @@ -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 = { + dashed: "shadow-xs border border-dashed", + muted: "bg-muted", + none: "", +}; const EmptyState: React.FC = ({ icon: Icon, title: message, description: subMessage, + variant = "muted", buttonText, buttonLink, - showButton = true, - showBorder = true, }) => { return (

{message}

{subMessage}

- {showButton && ( + {buttonText && buttonLink && ( {buttonText} diff --git a/frontend/src/components/OnchainTransactionsList.tsx b/frontend/src/components/OnchainTransactionsList.tsx index a81d087b..0d4dea5d 100644 --- a/frontend/src/components/OnchainTransactionsList.tsx +++ b/frontend/src/components/OnchainTransactionsList.tsx @@ -1,4 +1,4 @@ -import { LinkIcon } from "lucide-react"; +import { BitcoinIcon } from "lucide-react"; import EmptyState from "src/components/EmptyState"; import Loading from "src/components/Loading"; import OnchainTransactionItem from "src/components/OnchainTransactionItem"; @@ -18,12 +18,9 @@ export function OnchainTransactionsList() { return (
); diff --git a/frontend/src/components/TransactionsList.tsx b/frontend/src/components/TransactionsList.tsx index 3efaa987..83ff8fdf 100644 --- a/frontend/src/components/TransactionsList.tsx +++ b/frontend/src/components/TransactionsList.tsx @@ -1,4 +1,4 @@ -import { DrumIcon } from "lucide-react"; +import { LucideIcon, ZapIcon } from "lucide-react"; import { useRef, useState } from "react"; import { CustomPagination } from "src/components/CustomPagination"; import EmptyState from "src/components/EmptyState"; @@ -9,12 +9,18 @@ import { getTransactionsUrl, useTransactions } from "src/hooks/useTransactions"; type TransactionsListProps = { appId?: number; - showReceiveButton?: boolean; + emptyIcon?: LucideIcon; + emptyTitle?: string; + emptyDescription?: string; + emptyVariant?: "dashed" | "muted" | "none"; }; function TransactionsList({ appId, - showReceiveButton = true, + emptyIcon = ZapIcon, + emptyTitle = "No lightning payments yet", + emptyDescription = "Your payments will appear here as you start using your wallet.", + emptyVariant, }: TransactionsListProps) { const [page, setPage] = useState(1); const transactionListRef = useRef(null); @@ -48,13 +54,10 @@ function TransactionsList({
{!transactions.length ? ( ) : ( <> diff --git a/frontend/src/components/channels/ChannelWaitingForConfirmations.tsx b/frontend/src/components/channels/ChannelWaitingForConfirmations.tsx index 1774b0dc..bacb4279 100644 --- a/frontend/src/components/channels/ChannelWaitingForConfirmations.tsx +++ b/frontend/src/components/channels/ChannelWaitingForConfirmations.tsx @@ -49,6 +49,7 @@ export function ChannelWaitingForConfirmations({ icon={FootprintsIcon} title="Browse While You Wait" description="Feel free to leave this page or browse around Alby Hub! We'll send you an email as soon as your channel is active." + variant="dashed" buttonText="Explore Apps" buttonLink="/apps?tab=app-store" /> diff --git a/frontend/src/components/connections/AppTransactionList.tsx b/frontend/src/components/connections/AppTransactionList.tsx index 53c6f435..bd9380a1 100644 --- a/frontend/src/components/connections/AppTransactionList.tsx +++ b/frontend/src/components/connections/AppTransactionList.tsx @@ -1,3 +1,4 @@ +import { ArrowDownUpIcon } from "lucide-react"; import TransactionsList from "src/components/TransactionsList"; import { TransactionsListMenu } from "src/components/TransactionsListMenu"; import { @@ -15,7 +16,13 @@ export function AppTransactionList({ appId }: { appId: number }) { - + ); diff --git a/frontend/src/components/connections/ConnectedApps.tsx b/frontend/src/components/connections/ConnectedApps.tsx index a67164c2..8e2f0e55 100644 --- a/frontend/src/components/connections/ConnectedApps.tsx +++ b/frontend/src/components/connections/ConnectedApps.tsx @@ -88,6 +88,7 @@ function ConnectedApps() { icon={CableIcon} title="Connect Your First App" description="Connect your app of choice, fine-tune permissions and enjoy a seamless and secure wallet experience." + variant="dashed" buttonText="See Recommended Apps" buttonLink="/apps?tab=app-store" /> diff --git a/frontend/src/screens/channels/Channels.tsx b/frontend/src/screens/channels/Channels.tsx index 5ada897e..e25d654b 100644 --- a/frontend/src/screens/channels/Channels.tsx +++ b/frontend/src/screens/channels/Channels.tsx @@ -551,6 +551,7 @@ export default function Channels() { icon={UnplugIcon} title="No Channels Available" description="Connect to the Lightning Network by establishing your first channel and start transacting." + variant="dashed" buttonText="Open Channel" buttonLink="/channels/incoming" /> From c6f45e75e32c7896804692b60f3b58d8e6830183 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:13:23 +0700 Subject: [PATCH 057/136] chore: update bark bindings to v0.8.0 (#2428) * chore: update bark bindings to v0.8.0 * docs: update BARK_SERVER_ACCESS_TOKEN usage --- README.md | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7d259af3..545f443f 100644 --- a/README.md +++ b/README.md @@ -291,7 +291,7 @@ Bark connects to an [Ark](https://second.tech/) server. It can be configured via - `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 required by the Ark server (pre-public mainnet launch). +- `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 diff --git a/go.mod b/go.mod index f6fee6a1..f6e50065 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/tyler-smith/go-bip39 v1.1.0 github.com/wailsapp/wails/v2 v2.12.0 - gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.7.0 + gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.8.0 golang.org/x/crypto v0.52.0 golang.org/x/oauth2 v0.36.0 google.golang.org/grpc v1.79.3 diff --git a/go.sum b/go.sum index bf1e9237..6354051f 100644 --- a/go.sum +++ b/go.sum @@ -662,8 +662,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.7.0 h1:X8h/JbdfDoHGvw2UQZJmIkpwAgANzBK5KxrZG7zChmc= -gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.7.0/go.mod h1:1jAwB/XR4i3D72fz3qWAd41tQLYcOCGfWZHMagn5fNg= +gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.8.0 h1:YKHSM8iNFMmTQ/QPUxC5U9KVim10F7sIGt3ou2wet+o= +gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.8.0/go.mod h1:1jAwB/XR4i3D72fz3qWAd41tQLYcOCGfWZHMagn5fNg= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.etcd.io/etcd/api/v3 v3.5.16 h1:WvmyJVbjWqK4R1E+B12RRHz3bRGy9XVfh++MgbN+6n0= From 98e7d987bb1e7a20e326b9506fee6d91a4060e51 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:16:02 +0700 Subject: [PATCH 058/136] 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) * 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) * chore: use more general copy for card topup app install guide * fix: remove subtree commits --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../BitcoinCardTopupInstallGuide.tsx | 33 +++++++++++++++++++ .../connections/SuggestedAppData.tsx | 30 ++--------------- frontend/src/screens/cards/Cards.tsx | 8 ++++- 3 files changed, 42 insertions(+), 29 deletions(-) create mode 100644 frontend/src/components/connections/BitcoinCardTopupInstallGuide.tsx diff --git a/frontend/src/components/connections/BitcoinCardTopupInstallGuide.tsx b/frontend/src/components/connections/BitcoinCardTopupInstallGuide.tsx new file mode 100644 index 00000000..34e65d3f --- /dev/null +++ b/frontend/src/components/connections/BitcoinCardTopupInstallGuide.tsx @@ -0,0 +1,33 @@ +import { useLocation } from "react-router"; +import ExternalLink from "src/components/ExternalLink"; + +// The card topup app supports configuration presets selected via a `provider` +// query param (e.g. linked from the Cards page). Read it straight from the URL +// so the link opens card.albylabs.com pre-configured for the chosen provider. +export function BitcoinCardTopupInstallGuide() { + const provider = new URLSearchParams(useLocation().search).get("provider"); + const url = provider + ? `https://card.albylabs.com?provider=${encodeURIComponent(provider)}` + : "https://card.albylabs.com"; + + return ( +
+
    +
  • + Open{" "} + + card.albylabs.com + {" "} + on the device you'll top up from. +
  • +
  • + + Add it to your home screen + {" "} + (or bookmark it) so you can reopen it later. +
  • +
  • Enter your card's deposit details to set it up.
  • +
+
+ ); +} diff --git a/frontend/src/components/connections/SuggestedAppData.tsx b/frontend/src/components/connections/SuggestedAppData.tsx index bce2cdcf..75def3bd 100644 --- a/frontend/src/components/connections/SuggestedAppData.tsx +++ b/frontend/src/components/connections/SuggestedAppData.tsx @@ -53,6 +53,7 @@ import zapplepay from "src/assets/suggested-apps/zapple-pay.png"; import zappybird from "src/assets/suggested-apps/zappy-bird.png"; import zapstore from "src/assets/suggested-apps/zapstore.png"; import zeus from "src/assets/suggested-apps/zeus.png"; +import { BitcoinCardTopupInstallGuide } from "src/components/connections/BitcoinCardTopupInstallGuide"; import ExternalLink from "src/components/ExternalLink"; import { App } from "src/types"; @@ -228,34 +229,7 @@ export const appStoreApps: AppStoreApp[] = ( extendedDescription: "A generic top-up app that swaps Lightning sats to a stablecoin and sends them to your card's deposit address. Works with RedotPay, Freedomia, Nexo, Bybit, and any other card that accepts on-chain crypto deposits.", webLink: "https://card.albylabs.com", - installGuide: ( - <> -
-
    -
  • - Open{" "} - - card.albylabs.com - {" "} - on the device you'll top up from. -
  • -
  • - - Add it to your home screen - {" "} - (or bookmark it) so you can reopen it later. -
  • -
  • - Enter your card's deposit address, network, and currency to set - it up. -
  • -
-
- - ), + installGuide: , finalizeGuide: ( <>
diff --git a/frontend/src/screens/cards/Cards.tsx b/frontend/src/screens/cards/Cards.tsx index 0d88c061..bfcea1bc 100644 --- a/frontend/src/screens/cards/Cards.tsx +++ b/frontend/src/screens/cards/Cards.tsx @@ -776,10 +776,16 @@ function ConnectCardDialog({ if (!p.appStoreId) { return null; } + // The generic card topup app pre-configures itself from a `provider` + // query param; pass the selected provider so its preset is applied. + const to = + p.appStoreId === "bitcoin-card-topup" + ? `/apps/new?app=${p.appStoreId}&provider=${encodeURIComponent(p.id)}` + : `/apps/new?app=${p.appStoreId}`; return ( { sendEvent("debit_card_connect", { name: p.name }); onOpenChange(false); From e5dc19ae68a240f4efdca14e34af08cea3f02281 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:17:13 +0700 Subject: [PATCH 059/136] feat: add readonly option for app store apps (#2415) --- frontend/src/components/Scopes.tsx | 18 +++++++--------- .../connections/SuggestedAppData.tsx | 21 +++++++++---------- frontend/src/screens/apps/NewApp.tsx | 11 ++++++++++ frontend/src/types.ts | 11 ++++++++++ 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/Scopes.tsx b/frontend/src/components/Scopes.tsx index 7982b0bf..b325a265 100644 --- a/frontend/src/components/Scopes.tsx +++ b/frontend/src/components/Scopes.tsx @@ -19,7 +19,12 @@ import { SheetTitle, } from "src/components/ui/sheet"; import { cn } from "src/lib/utils"; -import { Scope, WalletCapabilities, scopeDescriptions } from "src/types"; +import { + READ_ONLY_SCOPES, + Scope, + WalletCapabilities, + scopeDescriptions, +} from "src/types"; const scopeGroups = ["full_access", "read_only", "isolated", "custom"] as const; type ScopeGroup = (typeof scopeGroups)[number]; @@ -65,17 +70,8 @@ const Scopes: React.FC = ({ }, [capabilities.scopes]); const readOnlyScopes: Scope[] = React.useMemo(() => { - const readOnlyScopes: Scope[] = [ - "get_balance", - "get_info", - "make_invoice", - "lookup_invoice", - "list_transactions", - "notifications", - ]; - return capabilities.scopes.filter((scope) => - readOnlyScopes.includes(scope) + READ_ONLY_SCOPES.includes(scope) ); }, [capabilities.scopes]); diff --git a/frontend/src/components/connections/SuggestedAppData.tsx b/frontend/src/components/connections/SuggestedAppData.tsx index 75def3bd..f514d129 100644 --- a/frontend/src/components/connections/SuggestedAppData.tsx +++ b/frontend/src/components/connections/SuggestedAppData.tsx @@ -84,6 +84,9 @@ export type AppStoreApp = { hideConnectionQr?: boolean; internal?: boolean; superuser?: boolean; + // Receive-only apps (e.g. merchant payment receivers) default to read-only + // permissions in the new connection flow. + readonly?: boolean; addedDate?: string; }; @@ -796,6 +799,7 @@ export const appStoreApps: AppStoreApp[] = ( }, { id: "sat-sorter", + readonly: true, title: "Sat Sorter", description: "A Bitcoin Budgeting App", webLink: "https://satsorter.com", @@ -895,6 +899,7 @@ export const appStoreApps: AppStoreApp[] = ( }, { id: "bitrequest", + readonly: true, title: "Bitrequest", description: "Non-custodial payment requests", webLink: "https://www.bitrequest.io", @@ -917,17 +922,6 @@ export const appStoreApps: AppStoreApp[] = ( {" "} in your browser, or download the app on iOS or Android

-

- In the next step, set wallet permissions to{" "} - Custom and - enable: -

-
    -
  • Read your node info
  • -
  • Create invoices
  • -
  • Lookup status of invoices
  • -
  • Read transaction history
  • -
), @@ -967,6 +961,7 @@ export const appStoreApps: AppStoreApp[] = ( }, { id: "btcpay", + readonly: true, title: "BTCPay Server", description: "Bitcoin payment processor", webLink: "https://btcpayserver.org/", @@ -1455,6 +1450,7 @@ export const appStoreApps: AppStoreApp[] = ( }, { id: "clams", + readonly: true, title: "Clams", description: "Multi wallet accounting tool", webLink: "https://clams.tech/", @@ -1491,6 +1487,7 @@ export const appStoreApps: AppStoreApp[] = ( }, { id: "nostrcheck-server", + readonly: true, title: "Nostrcheck Server", description: "Sovereign Nostr services", webLink: "https://github.com/quentintaranpino/nostrcheck-server", @@ -1798,6 +1795,7 @@ export const appStoreApps: AppStoreApp[] = ( }, { id: "nakapay", + readonly: true, title: "NakaPay", description: "Non-custodial Lightning payments for businesses via NWC", webLink: "https://www.nakapay.app", @@ -2338,6 +2336,7 @@ export const appStoreApps: AppStoreApp[] = ( }, { id: "takemysats", + readonly: true, title: "Take My Sats", description: "Create your online store and accept Bitcoin payments", webLink: "https://www.takemysats.com", diff --git a/frontend/src/screens/apps/NewApp.tsx b/frontend/src/screens/apps/NewApp.tsx index baac7814..d0db8162 100644 --- a/frontend/src/screens/apps/NewApp.tsx +++ b/frontend/src/screens/apps/NewApp.tsx @@ -31,6 +31,7 @@ import { CreateAppResponse, Nip47NotificationType, Nip47RequestMethod, + READ_ONLY_SCOPES, Scope, WalletCapabilities, validBudgetRenewals, @@ -105,6 +106,14 @@ const NewAppInternal = ({ capabilities }: NewAppInternalProps) => { /* eslint-disable react-hooks/preserve-manual-memoization */ const initialScopes: Scope[] = React.useMemo(() => { + // Receive-only app store apps (e.g. merchant payment receivers) default to + // read-only permissions, unless the deep link explicitly requests methods. + if (appStoreApp?.readonly && !reqMethodsParam) { + return capabilities.scopes.filter((scope) => + READ_ONLY_SCOPES.includes(scope) + ); + } + const methods = reqMethodsParam ? reqMethodsParam.split(" ") : capabilities.methods; @@ -185,8 +194,10 @@ const NewAppInternal = ({ capabilities }: NewAppInternalProps) => { return scopes; }, [ + appStoreApp?.readonly, capabilities.methods, capabilities.notificationTypes, + capabilities.scopes, isolatedParam, notificationTypesParam, reqMethodsParam, diff --git a/frontend/src/types.ts b/frontend/src/types.ts index cf0c9a88..bbda4156 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -80,6 +80,17 @@ export const validBudgetRenewals: BudgetRenewalType[] = [ "never", ]; +// Scopes granted to a read-only connection: receive payments and view +// balance/history, but never send/spend. +export const READ_ONLY_SCOPES: Scope[] = [ + "get_balance", + "get_info", + "make_invoice", + "lookup_invoice", + "list_transactions", + "notifications", +]; + export const scopeDescriptions: Record = { get_balance: "Read your balance", get_info: "Read your node info", From b55978d7bcd474cb5f9eff095f911a4ac9900c05 Mon Sep 17 00:00:00 2001 From: frnandu Date: Wed, 10 Jun 2026 09:47:07 +0200 Subject: [PATCH 060/136] feat: just in time channels with lsps2 (#2275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-authored-by: saunter <68239231+stackingsaunter@users.noreply.github.com> Co-authored-by: fmar Co-authored-by: René Aaron Co-authored-by: Roland Bewick --- README.md | 1 + alby/models.go | 1 + api/api.go | 50 +++- api/models.go | 7 +- config/models.go | 1 + .../src/components/CurrencyInputField.tsx | 4 + .../src/components/FirstChannelJitAlert.tsx | 48 ++++ .../src/components/ReceiveToLightning.tsx | 74 +++--- frontend/src/components/TransactionItem.tsx | 4 +- .../src/components/layouts/SettingsLayout.tsx | 6 + frontend/src/hooks/useOnboardingData.ts | 3 +- frontend/src/routes.tsx | 6 + frontend/src/screens/channels/Channels.tsx | 13 +- frontend/src/screens/settings/About.tsx | 51 ++++ .../src/screens/settings/NodeSettings.tsx | 84 ++++++ frontend/src/screens/wallet/Lightning.tsx | 7 +- .../screens/wallet/receive/ReceiveInvoice.tsx | 130 +++++++-- frontend/src/types.ts | 14 +- go.mod | 2 +- go.sum | 4 +- lnclient/ldk/ldk.go | 247 +++++++++++++++++- lsp/models.go | 1 + nip47/controllers/make_invoice_controller.go | 2 +- service/start.go | 7 +- transactions/transactions_service.go | 1 + 25 files changed, 678 insertions(+), 90 deletions(-) create mode 100644 frontend/src/components/FirstChannelJitAlert.tsx create mode 100644 frontend/src/screens/settings/NodeSettings.tsx diff --git a/README.md b/README.md index 545f443f..c92fc752 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,7 @@ _To configure via env, the following parameters must be provided:_ - `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 `@:`. When set, the "Open Your First Channel" prompts are hidden since the first channel is created automatically on the first receive. #### LDK Network Configuration diff --git a/alby/models.go b/alby/models.go index 9c216846..f51a636f 100644 --- a/alby/models.go +++ b/alby/models.go @@ -119,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"` diff --git a/api/api.go b/api/api.go index 9b4005d4..da75b818 100644 --- a/api/api.go +++ b/api/api.go @@ -1493,6 +1493,7 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { info := InfoResponse{} backendType, _ := api.cfg.Get("LNBackendType", "") ldkVssEnabled, _ := api.cfg.Get("LdkVssEnabled", "") + jitChannelsEnabled, _ := api.cfg.Get("JitChannelsEnabled", "") autoUnlockPassword, _ := api.cfg.Get("AutoUnlockPassword", "") setupCompleted, err := api.cfg.SetupCompleted() if err != nil { @@ -1516,6 +1517,7 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { info.EnableAdvancedSetup = api.cfg.GetEnv().EnableAdvancedSetup info.HideUpdateBanner = api.cfg.GetEnv().HideUpdateBanner info.LdkVssEnabled = ldkVssEnabled == "true" + info.JitChannelsEnabled = jitChannelsEnabled != "false" info.VssSupported = backendType == config.LDKBackendType && api.cfg.GetEnv().LDKVssUrl != "" info.SupportsBolt12 = backendType == config.LDKBackendType || backendType == config.CLNBackendType info.AutoUnlockPasswordEnabled = autoUnlockPassword != "" @@ -1552,10 +1554,28 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { type chainSourceProvider interface { GetChainDataSource() (string, string) } + type lsps2SourceProvider interface { + GetLiquiditySourceLsps2() string + } + type lsps2MinPaymentSizeProvider interface { + GetLiquiditySourceLsps2MinPaymentSizeMsat() *uint64 + } + type lsps2MaxPaymentSizeProvider interface { + GetLiquiditySourceLsps2MaxPaymentSizeMsat() *uint64 + } if ldkService, ok := api.svc.GetLNClient().(chainSourceProvider); ok { info.ChainDataSourceType, info.ChainDataSourceAddress = ldkService.GetChainDataSource() } + if ldkService, ok := api.svc.GetLNClient().(lsps2SourceProvider); ok { + info.JitChannelsLiquiditySource = ldkService.GetLiquiditySourceLsps2() + } + if ldkService, ok := api.svc.GetLNClient().(lsps2MinPaymentSizeProvider); ok { + info.JitChannelsMinPaymentSizeMsat = ldkService.GetLiquiditySourceLsps2MinPaymentSizeMsat() + } + if ldkService, ok := api.svc.GetLNClient().(lsps2MaxPaymentSizeProvider); ok { + info.JitChannelsMaxPaymentSizeMsat = ldkService.GetLiquiditySourceLsps2MaxPaymentSizeMsat() + } } } @@ -1566,7 +1586,7 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { return &info, nil } -func (api *api) SetCurrency(currency string) error { +func (api *api) setCurrency(currency string) error { if currency == "" { return fmt.Errorf("currency value cannot be empty") } @@ -1580,7 +1600,7 @@ func (api *api) SetCurrency(currency string) error { return nil } -func (api *api) SetBitcoinDisplayFormat(format string) error { +func (api *api) setBitcoinDisplayFormat(format string) error { if format != constants.BITCOIN_DISPLAY_FORMAT_SATS && format != constants.BITCOIN_DISPLAY_FORMAT_BIP177 { return fmt.Errorf("bitcoin display format must be '%s' or '%s'", constants.BITCOIN_DISPLAY_FORMAT_SATS, constants.BITCOIN_DISPLAY_FORMAT_BIP177) } @@ -1594,21 +1614,43 @@ func (api *api) SetBitcoinDisplayFormat(format string) error { return nil } +func (api *api) setJitChannelsEnabled(enabled bool) error { + value := "true" + if !enabled { + value = "false" + } + + err := api.cfg.SetUpdate("JitChannelsEnabled", value, "") + if err != nil { + logger.Logger.WithError(err).Error("Failed to update JIT channels setting") + return err + } + + return nil +} + func (api *api) UpdateSettings(updateSettingsRequest *UpdateSettingsRequest) error { if updateSettingsRequest.Currency != "" { - err := api.SetCurrency(updateSettingsRequest.Currency) + err := api.setCurrency(updateSettingsRequest.Currency) if err != nil { return fmt.Errorf("failed to set currency: %w", err) } } if updateSettingsRequest.BitcoinDisplayFormat != "" { - err := api.SetBitcoinDisplayFormat(updateSettingsRequest.BitcoinDisplayFormat) + err := api.setBitcoinDisplayFormat(updateSettingsRequest.BitcoinDisplayFormat) if err != nil { return fmt.Errorf("failed to set bitcoin display format: %w", err) } } + if updateSettingsRequest.JitChannelsEnabled != nil { + err := api.setJitChannelsEnabled(*updateSettingsRequest.JitChannelsEnabled) + if err != nil { + return fmt.Errorf("failed to set JIT channels setting: %w", err) + } + } + return nil } diff --git a/api/models.go b/api/models.go index 0f0cf279..5448465d 100644 --- a/api/models.go +++ b/api/models.go @@ -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) @@ -332,6 +330,10 @@ type InfoResponse struct { 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"` } @@ -339,6 +341,7 @@ type InfoResponse struct { type UpdateSettingsRequest struct { Currency string `json:"currency"` BitcoinDisplayFormat string `json:"bitcoinDisplayFormat"` + JitChannelsEnabled *bool `json:"jitChannelsEnabled"` } type SetNodeAliasRequest struct { diff --git a/config/models.go b/config/models.go index ca0be744..0fb88709 100644 --- a/config/models.go +++ b/config/models.go @@ -38,6 +38,7 @@ type AppConfig struct { 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"` diff --git a/frontend/src/components/CurrencyInputField.tsx b/frontend/src/components/CurrencyInputField.tsx index 56456195..33014d91 100644 --- a/frontend/src/components/CurrencyInputField.tsx +++ b/frontend/src/components/CurrencyInputField.tsx @@ -298,6 +298,10 @@ export function CurrencyInputField({ } function handleChangeMode(event: React.ChangeEvent) { + // 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") { diff --git a/frontend/src/components/FirstChannelJitAlert.tsx b/frontend/src/components/FirstChannelJitAlert.tsx new file mode 100644 index 00000000..973b6af1 --- /dev/null +++ b/frontend/src/components/FirstChannelJitAlert.tsx @@ -0,0 +1,48 @@ +import { InfoIcon } from "lucide-react"; +import ExternalLink from "src/components/ExternalLink"; +import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; +import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert"; +import { useChannels } from "src/hooks/useChannels"; +import { useInfo } from "src/hooks/useInfo"; + +export default function FirstChannelJitAlert() { + const { data: info } = useInfo(); + const { data: channels } = useChannels(); + + // a JIT channel only opens when the feature is enabled AND an LSPS2 liquidity + // source is actually configured (jitChannelsEnabled alone is just a settings + // toggle and can be true on backends without an LSPS2 source). + const lsps2Source = info?.jitChannelsEnabled + ? info.jitChannelsLiquiditySource + : undefined; + + // only relevant when the user has no channels yet - their first received + // payment will open the channel. + if (!lsps2Source || !channels || channels.length > 0) { + return null; + } + + const minPaymentSizeMsat = info?.jitChannelsMinPaymentSizeMsat; + + return ( + + + First payment opens a channel + + A channel fee applies.{" "} + {!!minPaymentSizeMsat && ( + <> + Minimum payment{" "} + .{" "} + + )} + + Learn more + + + + ); +} diff --git a/frontend/src/components/ReceiveToLightning.tsx b/frontend/src/components/ReceiveToLightning.tsx index 444774eb..3e6e3f96 100644 --- a/frontend/src/components/ReceiveToLightning.tsx +++ b/frontend/src/components/ReceiveToLightning.tsx @@ -1,4 +1,5 @@ import { CopyIcon, LinkIcon, ReceiptTextIcon, ZapIcon } from "lucide-react"; +import FirstChannelJitAlert from "src/components/FirstChannelJitAlert"; import Loading from "src/components/Loading"; import QRCode from "src/components/QRCode"; import { Button } from "src/components/ui/button"; @@ -18,43 +19,46 @@ export function ReceiveToLightning() { } return ( - - - -

- {me.lightning_address} -

-
- - - - - - Create Invoice - - {info.supportsBolt12 && ( - + + + + +

+ {me.lightning_address} +

+
+ + + + + + Create Invoice - )} - - - Receive from On-chain / Other Cryptocurrency - - -
+ {info.supportsBolt12 && ( + + + Lightning Offer + + )} + + + Receive from On-chain / Other Cryptocurrency + +
+
+
); } diff --git a/frontend/src/components/TransactionItem.tsx b/frontend/src/components/TransactionItem.tsx index dd60caa0..8ad3d5fa 100644 --- a/frontend/src/components/TransactionItem.tsx +++ b/frontend/src/components/TransactionItem.tsx @@ -277,10 +277,10 @@ function TransactionItem({ tx, transactionListKey }: Props) { {updatedAt.format("D MMMM YYYY, HH:mm")} - {tx.state != "failed" && type == "outgoing" && ( + {tx.state != "failed" && tx.feesPaidMsat > 0 && ( - {tx.feesPaidMsat > 0 && ( + {type == "outgoing" && ( <>  ( {((tx.feesPaidMsat / tx.amountMsat) * 100).toFixed(2)}%) diff --git a/frontend/src/components/layouts/SettingsLayout.tsx b/frontend/src/components/layouts/SettingsLayout.tsx index d403af4e..6129402c 100644 --- a/frontend/src/components/layouts/SettingsLayout.tsx +++ b/frontend/src/components/layouts/SettingsLayout.tsx @@ -6,6 +6,7 @@ import { useInfo } from "src/hooks/useInfo"; import { ArrowRightLeftIcon, + BoxIcon, BugIcon, CloudBackupIcon, CodeIcon, @@ -162,6 +163,11 @@ export default function SettingsLayout() { + {info?.backendType === "LDK" && ( + + Node + + )} Developer diff --git a/frontend/src/hooks/useOnboardingData.ts b/frontend/src/hooks/useOnboardingData.ts index bef166bf..10810a95 100644 --- a/frontend/src/hooks/useOnboardingData.ts +++ b/frontend/src/hooks/useOnboardingData.ts @@ -62,7 +62,8 @@ export const useOnboardingData = (): UseOnboardingDataResponse => { transactions.totalCount > 0 || balances.lightning.totalSpendableSat > 0; const checklistItems: Omit[] = [ - ...(hasChannelManagement + ...(hasChannelManagement && + !(info.jitChannelsEnabled && info.jitChannelsLiquiditySource) ? [ { title: "Open your first channel", diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 73636512..9cffc14f 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -49,6 +49,7 @@ import Peers from "src/screens/peers/Peers"; import { About } from "src/screens/settings/About"; import { AlbyAccount } from "src/screens/settings/AlbyAccount"; import { AutoUnlock } from "src/screens/settings/AutoUnlock"; +import { NodeSettings } from "src/screens/settings/NodeSettings"; import Backup from "src/screens/settings/Backup"; import { ChangeUnlockPassword } from "src/screens/settings/ChangeUnlockPassword"; import DebugTools from "src/screens/settings/DebugTools"; @@ -253,6 +254,11 @@ const routes: RouteObject[] = [ element: , handle: { crumb: () => "Auto Unlock" }, }, + { + path: "node", + element: , + handle: { crumb: () => "Node" }, + }, { path: "change-unlock-password", element: , diff --git a/frontend/src/screens/channels/Channels.tsx b/frontend/src/screens/channels/Channels.tsx index e25d654b..7c21150f 100644 --- a/frontend/src/screens/channels/Channels.tsx +++ b/frontend/src/screens/channels/Channels.tsx @@ -326,11 +326,14 @@ export default function Channels() { {!!channels?.length && ( <> {/* If all channels have less than 20% incoming capacity, show a warning */} - {channels?.every( - (channel) => - channel.remoteBalanceMsat < - (channel.localBalanceMsat + channel.remoteBalanceMsat) * 0.2 - ) && } + {!( + info?.jitChannelsEnabled && info?.jitChannelsLiquiditySource + ) && + channels?.every( + (channel) => + channel.remoteBalanceMsat < + (channel.localBalanceMsat + channel.remoteBalanceMsat) * 0.2 + ) && } )} diff --git a/frontend/src/screens/settings/About.tsx b/frontend/src/screens/settings/About.tsx index 161958c4..f503265f 100644 --- a/frontend/src/screens/settings/About.tsx +++ b/frontend/src/screens/settings/About.tsx @@ -1,13 +1,30 @@ +import { ExternalLinkIcon } from "lucide-react"; +import ExternalLink from "src/components/ExternalLink"; import Loading from "src/components/Loading"; import SettingsHeader from "src/components/SettingsHeader"; import { Badge } from "src/components/ui/badge"; import { useAlbyMe } from "src/hooks/useAlbyMe"; +import { useNodeDetails } from "src/hooks/useNodeDetails"; import { useInfo } from "src/hooks/useInfo"; export function About() { const { data: info } = useInfo(); const { data: albyMe, error: albyMeError } = useAlbyMe(); + const lsps2Source = info?.jitChannelsLiquiditySource; + const lsps2Pubkey = lsps2Source?.includes("@") + ? lsps2Source.split("@")[0] + : undefined; + const { data: lsps2NodeDetails } = useNodeDetails(lsps2Pubkey); + const lsps2Label = + lsps2NodeDetails?.alias || + (lsps2Pubkey ? lsps2Pubkey.slice(0, 8) + "..." : lsps2Source); + const lsps2MinPaymentSizeSat = info?.jitChannelsMinPaymentSizeMsat + ? Math.ceil(info.jitChannelsMinPaymentSizeMsat / 1000) + : undefined; + const lsps2MaxPaymentSizeSat = info?.jitChannelsMaxPaymentSizeMsat + ? Math.floor(info.jitChannelsMaxPaymentSizeMsat / 1000) + : undefined; if (!info || (info.albyAccountConnected && !albyMe && !albyMeError)) { return ; @@ -56,6 +73,40 @@ export function About() {
)} + {info.jitChannelsLiquiditySource && ( +
+

+ Just-in-Time channels Liquidity Source LSPS2 +

+
+ {lsps2Pubkey ? ( + + {lsps2Label} + + + ) : ( +

{lsps2Label}

+ )} +

{info.jitChannelsLiquiditySource}

+ {(lsps2MinPaymentSizeSat || lsps2MaxPaymentSizeSat) && ( +

+ JIT payment size:{" "} + {lsps2MinPaymentSizeSat + ? new Intl.NumberFormat().format(lsps2MinPaymentSizeSat) + : "?"} + {" - "} + {lsps2MaxPaymentSizeSat + ? new Intl.NumberFormat().format(lsps2MaxPaymentSizeSat) + : "?"}{" "} + sats +

+ )} +
+
+ )}

Nostr Relays

{info.relays.map((relay) => ( diff --git a/frontend/src/screens/settings/NodeSettings.tsx b/frontend/src/screens/settings/NodeSettings.tsx new file mode 100644 index 00000000..f432bc7d --- /dev/null +++ b/frontend/src/screens/settings/NodeSettings.tsx @@ -0,0 +1,84 @@ +import { toast } from "sonner"; +import ExternalLink from "src/components/ExternalLink"; +import Loading from "src/components/Loading"; +import SettingsHeader from "src/components/SettingsHeader"; +import { Checkbox } from "src/components/ui/checkbox"; +import { Label } from "src/components/ui/label"; + +import { useInfo } from "src/hooks/useInfo"; +import { handleRequestError } from "src/utils/handleRequestError"; +import { request } from "src/utils/request"; + +export function NodeSettings() { + const { data: info, mutate: refetchInfo } = useInfo(); + + if (!info) { + return ; + } + if (info.backendType !== "LDK") { + return

Your Hub does not support this feature.

; + } + + const hasJitSource = !!info.jitChannelsLiquiditySource; + + async function setJitChannelsEnabled(enabled: boolean) { + try { + await request("/api/settings", { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ jitChannelsEnabled: enabled }), + }); + await refetchInfo(); + toast(enabled ? "JIT channels enabled" : "JIT channels disabled"); + } catch (error) { + handleRequestError("Failed to update JIT channels setting", error); + } + } + + return ( + <> + +
+
+

+ JIT (just-in-time) channels let you receive payments larger than + your current inbound capacity by automatically opening a new channel + through a liquidity provider. The provider's fee is deducted from + the incoming payment.{" "} + + Learn more + +

+
+
+ + setJitChannelsEnabled(checked === true) + } + /> + +
+ {!hasJitSource && ( +

+ No JIT liquidity source is available for your network, so JIT + channels can't be used. +

+ )} +
+ + ); +} diff --git a/frontend/src/screens/wallet/Lightning.tsx b/frontend/src/screens/wallet/Lightning.tsx index cc299fd8..259a5e93 100644 --- a/frontend/src/screens/wallet/Lightning.tsx +++ b/frontend/src/screens/wallet/Lightning.tsx @@ -16,7 +16,7 @@ import { useChannels } from "src/hooks/useChannels"; import { useInfo } from "src/hooks/useInfo"; export default function Lightning() { - const { hasChannelManagement } = useInfo(); + const { data: info, hasChannelManagement } = useInfo(); const { data: balances } = useBalances(true); const { data: channels } = useChannels(); @@ -37,7 +37,10 @@ export default function Lightning() { balances.lightning.totalReceivableMsat < balances.lightning.totalSpendableMsat * 0.1; const showOpenFirstChannel = - hasChannelManagement && channels && !hasChannelsOpen; + hasChannelManagement && + channels && + !hasChannelsOpen && + !(info?.jitChannelsEnabled && info?.jitChannelsLiquiditySource); return ( <> diff --git a/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx b/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx index ad593eca..3c394adc 100644 --- a/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx +++ b/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx @@ -10,6 +10,7 @@ import React from "react"; import { toast } from "sonner"; import AppHeader from "src/components/AppHeader"; import { CurrencyInputField } from "src/components/CurrencyInputField"; +import ExternalLink from "src/components/ExternalLink"; import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; import FormattedFiatAmount from "src/components/FormattedFiatAmount"; import Loading from "src/components/Loading"; @@ -30,6 +31,7 @@ import { Input } from "src/components/ui/input"; import { Label } from "src/components/ui/label"; import { useAlbyMe } from "src/hooks/useAlbyMe"; import { useBalances } from "src/hooks/useBalances"; +import { useChannels } from "src/hooks/useChannels"; import { useInfo } from "src/hooks/useInfo"; import { useTransaction } from "src/hooks/useTransaction"; @@ -42,6 +44,7 @@ export default function ReceiveInvoice() { const { data: info, hasChannelManagement } = useInfo(); const { data: me } = useAlbyMe(); const { data: balances } = useBalances(); + const { data: channels } = useChannels(); const [isLoading, setLoading] = React.useState(false); const [amountSat, setAmountSat] = React.useState(""); @@ -49,17 +52,49 @@ export default function ReceiveInvoice() { const [transaction, setTransaction] = React.useState( null ); - const [paymentDone, setPaymentDone] = React.useState(false); const { data: invoiceData } = useTransaction( transaction ? transaction.paymentHash : "", true ); - - React.useEffect(() => { - if (invoiceData?.settledAt) { - setPaymentDone(true); + const paymentDone = !!invoiceData?.settledAt; + const jitChannelsEnabled = !!info?.jitChannelsEnabled; + const configuredLsps2Source = info?.jitChannelsLiquiditySource; + const lsps2Source = jitChannelsEnabled ? configuredLsps2Source : undefined; + const lsps2MinimumPaymentSizeSat = React.useMemo(() => { + if (jitChannelsEnabled && info?.jitChannelsMinPaymentSizeMsat) { + return Math.ceil(info.jitChannelsMinPaymentSizeMsat / 1000); } - }, [invoiceData]); + return undefined; + }, [info?.jitChannelsMinPaymentSizeMsat, jitChannelsEnabled]); + // only enforce the minimum on the input when the user has no channels yet - + // their first channel must meet the minimum size. + const jitMinimumReceiveSat = channels?.length + ? undefined + : lsps2MinimumPaymentSizeSat; + const lsps2MaximumPaymentSizeSat = React.useMemo(() => { + if (jitChannelsEnabled && info?.jitChannelsMaxPaymentSizeMsat) { + return Math.floor(info.jitChannelsMaxPaymentSizeMsat / 1000); + } + return undefined; + }, [info?.jitChannelsMaxPaymentSizeMsat, jitChannelsEnabled]); + const jitMaximumReceiveSat = + hasChannelManagement && lsps2Source + ? lsps2MaximumPaymentSizeSat + : !lsps2Source && hasChannelManagement + ? balances?.lightning.totalReceivableSat + : undefined; + const totalReceivableMsat = balances?.lightning.totalReceivableMsat ?? 0; + const requestedAmountMsat = +amountSat * 1000 || transaction?.amountMsat || 0; + const isNearReceivingCapacity = + !!hasChannelManagement && requestedAmountMsat >= 0.8 * totalReceivableMsat; + const isJitReceiveInvoice = + !!hasChannelManagement && + !!lsps2Source && + !!transaction && + transaction.amountMsat > totalReceivableMsat; + const displayedJitFeeMsat = paymentDone + ? (invoiceData?.feesPaidMsat ?? 0) + : (transaction?.feesPaidMsat ?? 0); if (!balances || !info || (info.albyAccountConnected && !me)) { return ; @@ -88,8 +123,25 @@ export default function ReceiveInvoice() { toast("Successfully created invoice"); } } catch (e) { + const requestedAmountSat = parseInt(amountSat) || 0; + // the user already has channels but this amount exceeds their receiving + // capacity (so a new channel is needed) and is below the LSP's minimum + // channel size - the receive may have failed because the amount was too + // small to open a second channel, so add a hint alongside the error. + const likelyTooSmallForNewChannel = + jitChannelsEnabled && + !!channels?.length && + !!lsps2MinimumPaymentSizeSat && + requestedAmountSat < lsps2MinimumPaymentSizeSat && + requestedAmountSat * 1000 > totalReceivableMsat; + let description = "" + e; + if (likelyTooSmallForNewChannel) { + description += `\n\nThis amount is over your receiving capacity and may be too small to open a new Lightning channel. Try receiving at least ${new Intl.NumberFormat().format( + lsps2MinimumPaymentSizeSat as number + )} sats, or lower the amount to fit your current capacity.`; + } toast.error("Failed to create invoice", { - description: "" + e, + description, }); console.error(e); } finally { @@ -101,6 +153,19 @@ export default function ReceiveInvoice() { copyToClipboard(transaction?.invoice as string); }; + const newChannelFeeAlert = ( +

+ Includes a {" "} + channel fee.{" "} + + Learn more + +

+ ); + return (
- {hasChannelManagement && - (+amountSat * 1000 || transaction?.amountMsat || 0) >= - 0.8 * balances.lightning.totalReceivableMsat && ( - - )} + {!lsps2Source && !transaction && isNearReceivingCapacity && ( + + )}
{transaction ? ( @@ -138,6 +201,9 @@ export default function ReceiveInvoice() { className="text-xl" />
+ {isJitReceiveInvoice && displayedJitFeeMsat >= 1000 && ( +
{newChannelFeeAlert}
+ )}
-

{provider.network}

+

+ {provider.networks.join(" / ")} +

@@ -679,7 +701,7 @@ function ProviderCard({ provider }: { provider: Provider }) { )}

- {provider.network} · {provider.cardType} + {provider.networks.join(" / ")} · {provider.cardType}

@@ -780,7 +802,7 @@ function ConnectCardDialog({ // query param; pass the selected provider so its preset is applied. const to = p.appStoreId === "bitcoin-card-topup" - ? `/apps/new?app=${p.appStoreId}&provider=${encodeURIComponent(p.id)}` + ? `/apps/new?app=${p.appStoreId}&provider=${encodeURIComponent(p.id)}&name=${encodeURIComponent(`${p.name} - Bitcoin Card Topup`)}` : `/apps/new?app=${p.appStoreId}`; return (

{p.name}

- {p.network} · {p.cardType} + {p.networks.join(" / ")} · {p.cardType}

From a21c320fde229cbd5f4dea6870ce30e48c03518c Mon Sep 17 00:00:00 2001 From: Alchemist Date: Thu, 11 Jun 2026 13:20:35 +0100 Subject: [PATCH 067/136] feat: simplify receive screen (#2426) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-authored-by: René Aaron --- .../src/components/ReceiveToLightning.tsx | 124 ++++++++++++------ .../screens/wallet/receive/ReceiveInvoice.tsx | 53 +++++--- 2 files changed, 119 insertions(+), 58 deletions(-) diff --git a/frontend/src/components/ReceiveToLightning.tsx b/frontend/src/components/ReceiveToLightning.tsx index 3e6e3f96..892e94a3 100644 --- a/frontend/src/components/ReceiveToLightning.tsx +++ b/frontend/src/components/ReceiveToLightning.tsx @@ -1,11 +1,22 @@ -import { CopyIcon, LinkIcon, ReceiptTextIcon, ZapIcon } from "lucide-react"; +import { + ChevronRightIcon, + CopyIcon, + LinkIcon, + ReceiptTextIcon, + ZapIcon, +} from "lucide-react"; +import { Link } from "react-router"; import FirstChannelJitAlert from "src/components/FirstChannelJitAlert"; import Loading from "src/components/Loading"; import QRCode from "src/components/QRCode"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "src/components/ui/accordion"; import { Button } from "src/components/ui/button"; -import { Card, CardContent, CardFooter } from "src/components/ui/card"; -import { LinkButton } from "src/components/ui/custom/link-button"; -import { Separator } from "src/components/ui/separator"; +import { Card, CardContent } from "src/components/ui/card"; import { useAlbyMe } from "src/hooks/useAlbyMe"; import { useInfo } from "src/hooks/useInfo"; import { copyToClipboard } from "src/lib/clipboard"; @@ -19,45 +30,82 @@ export function ReceiveToLightning() { } return ( -
+
-

- {me.lightning_address} -

-
- - - - - - Create Invoice - - {info.supportsBolt12 && ( - +

+ {me.lightning_address} +

+ +
+ + + + + + + Other ways to receive + + + +
+

Create Invoice

+

+ Request a specific amount with a one-time invoice +

+
+ + + {info.supportsBolt12 && ( + + +
+

Lightning Offer

+

+ Share a reusable payment code that never expires +

+
+ + + )} + + +
+

+ On-chain or Other Cryptocurrency +

+

+ Swap funds from on-chain bitcoin or other cryptocurrencies +

+
+ + +
+
+
+
); diff --git a/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx b/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx index 3c394adc..d844007f 100644 --- a/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx +++ b/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx @@ -16,6 +16,12 @@ import FormattedFiatAmount from "src/components/FormattedFiatAmount"; import Loading from "src/components/Loading"; import LowReceivingCapacityAlert from "src/components/LowReceivingCapacityAlert"; import QRCode from "src/components/QRCode"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "src/components/ui/accordion"; import { Button } from "src/components/ui/button"; import { Card, @@ -333,26 +339,33 @@ export default function ReceiveInvoice() { Create Invoice {(!info?.albyAccountConnected || !me?.lightning_address) && ( -
- {!info?.albyAccountConnected && info.supportsBolt12 && ( - - - Lightning Offer - - )} - - - Receive from On-chain / Other Cryptocurrency - -
+ + + + View other ways to receive + + + {!info?.albyAccountConnected && info.supportsBolt12 && ( + + + Lightning Offer + + )} + + + Receive from On-chain / Other Cryptocurrency + + + + )} )} From b1c0d4eac86c2effc96e81754f773ef7f9294511 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:09:27 +0700 Subject: [PATCH 068/136] 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 --- .../src/components/FirstChannelJitAlert.tsx | 145 +++++++++++++++++- .../screens/wallet/receive/ReceiveInvoice.tsx | 20 +++ 2 files changed, 160 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/FirstChannelJitAlert.tsx b/frontend/src/components/FirstChannelJitAlert.tsx index 973b6af1..7f05ccf1 100644 --- a/frontend/src/components/FirstChannelJitAlert.tsx +++ b/frontend/src/components/FirstChannelJitAlert.tsx @@ -1,13 +1,21 @@ -import { InfoIcon } from "lucide-react"; +import { AlertTriangleIcon, InfoIcon } from "lucide-react"; +import React from "react"; +import { Link } from "react-router"; import ExternalLink from "src/components/ExternalLink"; import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert"; +import { useBalances } from "src/hooks/useBalances"; import { useChannels } from "src/hooks/useChannels"; import { useInfo } from "src/hooks/useInfo"; +import { CreateInvoiceRequest, Transaction } from "src/types"; +import { request } from "src/utils/request"; + +const PROBE_TIMEOUT_MS = 5000; export default function FirstChannelJitAlert() { const { data: info } = useInfo(); const { data: channels } = useChannels(); + const { data: balances } = useBalances(); // a JIT channel only opens when the feature is enabled AND an LSPS2 liquidity // source is actually configured (jitChannelsEnabled alone is just a settings @@ -16,13 +24,140 @@ export default function FirstChannelJitAlert() { ? info.jitChannelsLiquiditySource : undefined; - // only relevant when the user has no channels yet - their first received - // payment will open the channel. - if (!lsps2Source || !channels || channels.length > 0) { + const minPaymentSizeMsat = info?.jitChannelsMinPaymentSizeMsat; + + const isJitEnabled = !!lsps2Source && !!channels; + // the user's first received payment opens the channel when they have none yet. + const isFirstChannel = isJitEnabled && channels.length === 0; + + // probe whether a JIT channel can actually be obtained by requesting an + // invoice for the minimum payment size. If it (or waiting for the minimum + // payment size) doesn't succeed within the timeout, we surface a fallback + // alert depending on whether the user already has channels. + const [probeState, setProbeState] = React.useState< + "loading" | "ok" | "failed" + >("loading"); + const deadlineRef = React.useRef(null); + // single-flight the non-idempotent probe invoice: hold the in-flight request + // so effect re-entry (e.g. StrictMode remount) reuses the same POST instead + // of creating a duplicate invoice. Reset when the probe window ends. + const probeRequestRef = React.useRef | null>( + null + ); + + React.useEffect(() => { + if (!isJitEnabled) { + deadlineRef.current = null; + probeRequestRef.current = null; + return; + } + + // start the 5s clock once when we enter JIT mode - it keeps ticking while + // we wait for the minimum payment size to become available. + if (deadlineRef.current === null) { + deadlineRef.current = Date.now() + PROBE_TIMEOUT_MS; + } + + let cancelled = false; + const remainingMs = deadlineRef.current - Date.now(); + if (remainingMs <= 0) { + setProbeState("failed"); + return; + } + + const timer = setTimeout(() => { + if (!cancelled) { + setProbeState("failed"); + } + }, remainingMs); + + // wait for the minimum payment size before requesting the probe invoice. + if (minPaymentSizeMsat) { + // reuse an already in-flight probe so a re-run doesn't issue a second POST. + const probeRequest = + probeRequestRef.current ?? + (probeRequestRef.current = request("/api/invoices", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + amountMsat: minPaymentSizeMsat, + description: "", + } as CreateInvoiceRequest), + })); + probeRequest + .then(() => { + if (!cancelled) { + clearTimeout(timer); + setProbeState("ok"); + } + }) + .catch(() => { + if (!cancelled) { + clearTimeout(timer); + setProbeState("failed"); + } + }); + } + + return () => { + cancelled = true; + clearTimeout(timer); + }; + }, [isJitEnabled, minPaymentSizeMsat]); + + if (!isJitEnabled || probeState === "loading") { return null; } - const minPaymentSizeMsat = info?.jitChannelsMinPaymentSizeMsat; + if (probeState === "failed") { + // no channels yet and a JIT channel couldn't be obtained - the user has no + // receiving capacity at all and will fail to receive until a channel opens. + if (isFirstChannel) { + return ( + + + Can't receive payments yet + + You won't be able to receive payments until you{" "} + + open a channel + + . + + + ); + } + + // they already have channels but a JIT channel couldn't be obtained, so they + // can only receive up to their current capacity without opening one. Wait for + // balances so we don't claim a misleading "0" receivable amount. + if (!balances) { + return null; + } + return ( + + + + You can currently receive up to{" "} + + . If you want to receive a larger payment,{" "} + + open a channel + + . + + + ); + } + + // probe succeeded - only the first-channel case needs an informational alert. + if (!isFirstChannel) { + return null; + } return ( diff --git a/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx b/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx index d844007f..b3826b64 100644 --- a/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx +++ b/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx @@ -1,4 +1,5 @@ import { + AlertTriangleIcon, ArrowLeftIcon, CopyIcon, LinkIcon, @@ -7,6 +8,7 @@ import { } from "lucide-react"; import TickSVG from "public/images/illustrations/tick.svg"; import React from "react"; +import { Link } from "react-router"; import { toast } from "sonner"; import AppHeader from "src/components/AppHeader"; import { CurrencyInputField } from "src/components/CurrencyInputField"; @@ -22,6 +24,7 @@ import { AccordionItem, AccordionTrigger, } from "src/components/ui/accordion"; +import { Alert, AlertDescription } from "src/components/ui/alert"; import { Button } from "src/components/ui/button"; import { Card, @@ -53,6 +56,8 @@ export default function ReceiveInvoice() { const { data: channels } = useChannels(); const [isLoading, setLoading] = React.useState(false); + const [jitChannelRequestFailed, setJitChannelRequestFailed] = + React.useState(false); const [amountSat, setAmountSat] = React.useState(""); const [description, setDescription] = React.useState(""); const [transaction, setTransaction] = React.useState( @@ -111,6 +116,7 @@ export default function ReceiveInvoice() { try { setLoading(true); + setJitChannelRequestFailed(false); const invoice = await request("/api/invoices", { method: "POST", headers: { @@ -149,6 +155,9 @@ export default function ReceiveInvoice() { toast.error("Failed to create invoice", { description, }); + if (lsps2Source) { + setJitChannelRequestFailed(true); + } console.error(e); } finally { setLoading(false); @@ -370,6 +379,17 @@ export default function ReceiveInvoice() { )}
+ {!transaction && jitChannelRequestFailed && ( + + + + Failed to request a just-in-time channel invoice.{" "} + + Manually open a channel. + + + + )}
{!transaction && (!info?.albyAccountConnected || !me?.lightning_address) && ( From bf9c346a98990a4e1040560764aa8ab96c775c12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= <100827540+reneaaron@users.noreply.github.com> Date: Fri, 12 Jun 2026 06:11:17 +0200 Subject: [PATCH 069/136] feat: use switch and improve copy on node settings page (#2441) * feat: use switch and improve copy on node settings page * fix: allow disabling JIT channels when no liquidity source exists --- .../src/screens/settings/NodeSettings.tsx | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/frontend/src/screens/settings/NodeSettings.tsx b/frontend/src/screens/settings/NodeSettings.tsx index f432bc7d..d32a4f4c 100644 --- a/frontend/src/screens/settings/NodeSettings.tsx +++ b/frontend/src/screens/settings/NodeSettings.tsx @@ -2,8 +2,8 @@ import { toast } from "sonner"; import ExternalLink from "src/components/ExternalLink"; import Loading from "src/components/Loading"; import SettingsHeader from "src/components/SettingsHeader"; -import { Checkbox } from "src/components/ui/checkbox"; import { Label } from "src/components/ui/label"; +import { Switch } from "src/components/ui/switch"; import { useInfo } from "src/hooks/useInfo"; import { handleRequestError } from "src/utils/handleRequestError"; @@ -31,9 +31,13 @@ export function NodeSettings() { body: JSON.stringify({ jitChannelsEnabled: enabled }), }); await refetchInfo(); - toast(enabled ? "JIT channels enabled" : "JIT channels disabled"); + toast( + enabled + ? "Just-in-time channels enabled" + : "Just-in-time channels disabled" + ); } catch (error) { - handleRequestError("Failed to update JIT channels setting", error); + handleRequestError("Failed to update just-in-time channels", error); } } @@ -42,40 +46,36 @@ export function NodeSettings() {
-
-

- JIT (just-in-time) channels let you receive payments larger than - your current inbound capacity by automatically opening a new channel - through a liquidity provider. The provider's fee is deducted from - the incoming payment.{" "} - - Learn more - -

-
-
- +
+ +

+ Automatically open a new channel through a liquidity provider when + you receive a payment that exceeds your receive limit. The + provider's fee is deducted from that payment.{" "} + + Learn more + +

+
+ - setJitChannelsEnabled(checked === true) - } + disabled={!hasJitSource && !info.jitChannelsEnabled} + onCheckedChange={setJitChannelsEnabled} /> -
{!hasJitSource && (

- No JIT liquidity source is available for your network, so JIT - channels can't be used. + Just-in-time channels are currently not available on your network.

)}
From 222031a97e0594d8978d1acf58404e4fd119d45c Mon Sep 17 00:00:00 2001 From: Alchemist Date: Fri, 19 Jun 2026 09:48:18 +0100 Subject: [PATCH 070/136] fix: soften channel routing warning (#2458) * 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> --- lnclient/ldk/ldk.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnclient/ldk/ldk.go b/lnclient/ldk/ldk.go index e87fd1a7..28c0767c 100644 --- a/lnclient/ldk/ldk.go +++ b/lnclient/ldk/ldk.go @@ -978,7 +978,7 @@ func (ls *LDKService) ListChannels(ctx context.Context) ([]lnclient.Channel, err channelError = &channelErrorValue } else if ldkChannel.IsUsable && ldkChannel.CounterpartyForwardingInfoFeeBaseMsat == nil { // if we don't have this, routing will not work (LND <-> LDK interoperability bug - https://github.com/lightningnetwork/lnd/issues/6870 ) - channelErrorValue := "Counterparty forwarding info not available. Please contact support@getalby.com" + channelErrorValue := "Counterparty forwarding info is not yet available, but normally resolves automatically. Try restarting Alby Hub if this warning does not resolve within a few hours." channelError = &channelErrorValue } From b70f40e42a6601f363b0865d508d0085c2ca0a58 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Tue, 14 Jul 2026 16:50:37 +0200 Subject: [PATCH 071/136] fix: require full access api key for swaps/mnemonic (#2473) 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. --- http/http_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http/http_service.go b/http/http_service.go index b36091a6..9f270c24 100644 --- a/http/http_service.go +++ b/http/http_service.go @@ -148,7 +148,6 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) { readOnlyApiGroup.GET("/swaps/:swapId", httpSvc.lookupSwapHandler) readOnlyApiGroup.GET("/swaps/out/info", httpSvc.getSwapOutInfoHandler) readOnlyApiGroup.GET("/swaps/in/info", httpSvc.getSwapInInfoHandler) - readOnlyApiGroup.GET("/swaps/mnemonic", httpSvc.swapMnemonicHandler) readOnlyApiGroup.GET("/autoswap", httpSvc.getAutoSwapConfigHandler) readOnlyApiGroup.GET("/forwards", httpSvc.forwardsHandler) @@ -191,6 +190,7 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) { fullAccessApiGroup.POST("/swaps/out", httpSvc.initiateSwapOutHandler) fullAccessApiGroup.POST("/swaps/in", httpSvc.initiateSwapInHandler) fullAccessApiGroup.POST("/swaps/refund", httpSvc.refundSwapHandler) + fullAccessApiGroup.GET("/swaps/mnemonic", httpSvc.swapMnemonicHandler) fullAccessApiGroup.POST("/autoswap", httpSvc.enableAutoSwapOutHandler) fullAccessApiGroup.DELETE("/autoswap", httpSvc.disableAutoSwapOutHandler) fullAccessApiGroup.POST("/node/alias", httpSvc.setNodeAliasHandler) From 270d23d273020242eebcde30104cc06ec05f0231 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:18:50 +0700 Subject: [PATCH 072/136] 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package.json | 2 +- frontend/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 9817edea..802e0079 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -60,7 +60,7 @@ "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.2.4", - "@types/node": "^25.8.0", + "@types/node": "^25.9.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.0.0", "@types/react-lottie": "^1.2.10", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 8b3bb2b8..860f74d9 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2755,10 +2755,10 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@types/node@^25.8.0": - version "25.8.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-25.8.0.tgz#d13033397d1c186876bed4c9b9d7f3f962097eb3" - integrity sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ== +"@types/node@^25.9.3": + version "25.9.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.9.3.tgz#11dfe7a33e68fa5c560f0aa76cc5595621ef26b9" + integrity sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg== dependencies: undici-types ">=7.24.0 <7.24.7" From e6fcc3e3c10c9714eac34eadd2df08e3b9801021 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:19:40 +0700 Subject: [PATCH 073/136] 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package.json | 2 +- frontend/yarn.lock | 142 +++++++++++++++++++++--------------------- 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 802e0079..6a0b25cf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -76,7 +76,7 @@ "shx": "^0.4.0", "tailwindcss": "^4.3.0", "typescript": "^5.9.3", - "typescript-eslint": "^8.60.1", + "typescript-eslint": "^8.61.0", "vite": "^5.4.0", "vite-plugin-pwa": "^1.3.0", "vite-tsconfig-paths": "^6.1.1" diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 860f74d9..40cb3a19 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2791,100 +2791,100 @@ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== -"@typescript-eslint/eslint-plugin@8.60.1": - version "8.60.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz#c1060bb8fa4be80624d3f3dec8dd9caca373af76" - integrity sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg== +"@typescript-eslint/eslint-plugin@8.61.0": + version "8.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz#db20271974b94a3a54d3b9544e5f5b3481448400" + integrity sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw== dependencies: "@eslint-community/regexpp" "^4.12.2" - "@typescript-eslint/scope-manager" "8.60.1" - "@typescript-eslint/type-utils" "8.60.1" - "@typescript-eslint/utils" "8.60.1" - "@typescript-eslint/visitor-keys" "8.60.1" + "@typescript-eslint/scope-manager" "8.61.0" + "@typescript-eslint/type-utils" "8.61.0" + "@typescript-eslint/utils" "8.61.0" + "@typescript-eslint/visitor-keys" "8.61.0" ignore "^7.0.5" natural-compare "^1.4.0" ts-api-utils "^2.5.0" -"@typescript-eslint/parser@8.60.1": - version "8.60.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.60.1.tgz#a9d7f30850384d34b41f4687dd8944823c09e289" - integrity sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA== +"@typescript-eslint/parser@8.61.0": + version "8.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.61.0.tgz#1afe73c9ccce16b7a26d6b95f9400b0ccc34af87" + integrity sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w== dependencies: - "@typescript-eslint/scope-manager" "8.60.1" - "@typescript-eslint/types" "8.60.1" - "@typescript-eslint/typescript-estree" "8.60.1" - "@typescript-eslint/visitor-keys" "8.60.1" + "@typescript-eslint/scope-manager" "8.61.0" + "@typescript-eslint/types" "8.61.0" + "@typescript-eslint/typescript-estree" "8.61.0" + "@typescript-eslint/visitor-keys" "8.61.0" debug "^4.4.3" -"@typescript-eslint/project-service@8.60.1": - version "8.60.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.60.1.tgz#eb29712f58d72c222fc727162e92f2ab4670971b" - integrity sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw== +"@typescript-eslint/project-service@8.61.0": + version "8.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.61.0.tgz#417a2feac32e8ebd336d63f068c3b42b736ea1ac" + integrity sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA== dependencies: - "@typescript-eslint/tsconfig-utils" "^8.60.1" - "@typescript-eslint/types" "^8.60.1" + "@typescript-eslint/tsconfig-utils" "^8.61.0" + "@typescript-eslint/types" "^8.61.0" debug "^4.4.3" -"@typescript-eslint/scope-manager@8.60.1": - version "8.60.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz#2f875962eaad0a0789cc3c36aea9b4ddeb2dd9c8" - integrity sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w== +"@typescript-eslint/scope-manager@8.61.0": + version "8.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz#93c2520d05653fe65eb9ee98efc74fd0134a7852" + integrity sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA== dependencies: - "@typescript-eslint/types" "8.60.1" - "@typescript-eslint/visitor-keys" "8.60.1" + "@typescript-eslint/types" "8.61.0" + "@typescript-eslint/visitor-keys" "8.61.0" -"@typescript-eslint/tsconfig-utils@8.60.1", "@typescript-eslint/tsconfig-utils@^8.60.1": - version "8.60.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz#bee8b942a13679a878101c9c74577d732062ed93" - integrity sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA== +"@typescript-eslint/tsconfig-utils@8.61.0", "@typescript-eslint/tsconfig-utils@^8.61.0": + version "8.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz#05d6e3ff20001674ebcd22d03dac29ee448043ba" + integrity sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ== -"@typescript-eslint/type-utils@8.60.1": - version "8.60.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz#1ae45f0f2a701354beea4a58c2161e40a5e3c379" - integrity sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A== +"@typescript-eslint/type-utils@8.61.0": + version "8.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz#50219b57e6b89cecfb1a15f093b15ec9ee019974" + integrity sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A== dependencies: - "@typescript-eslint/types" "8.60.1" - "@typescript-eslint/typescript-estree" "8.60.1" - "@typescript-eslint/utils" "8.60.1" + "@typescript-eslint/types" "8.61.0" + "@typescript-eslint/typescript-estree" "8.61.0" + "@typescript-eslint/utils" "8.61.0" debug "^4.4.3" ts-api-utils "^2.5.0" -"@typescript-eslint/types@8.60.1", "@typescript-eslint/types@^8.60.1": - version "8.60.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.60.1.tgz#ccdc482ba9e17f9723a10ce240b5e67dad3046c4" - integrity sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w== +"@typescript-eslint/types@8.61.0", "@typescript-eslint/types@^8.61.0": + version "8.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.0.tgz#0ddb46e012a4288292950bdd253db42f278ce64d" + integrity sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg== -"@typescript-eslint/typescript-estree@8.60.1": - version "8.60.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz#016630b119228bf483ddc652703a6a038f3fdd74" - integrity sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew== +"@typescript-eslint/typescript-estree@8.61.0": + version "8.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz#98ca47260bbf627fc28f018b3a0abf00e3090690" + integrity sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA== dependencies: - "@typescript-eslint/project-service" "8.60.1" - "@typescript-eslint/tsconfig-utils" "8.60.1" - "@typescript-eslint/types" "8.60.1" - "@typescript-eslint/visitor-keys" "8.60.1" + "@typescript-eslint/project-service" "8.61.0" + "@typescript-eslint/tsconfig-utils" "8.61.0" + "@typescript-eslint/types" "8.61.0" + "@typescript-eslint/visitor-keys" "8.61.0" debug "^4.4.3" minimatch "^10.2.2" semver "^7.7.3" tinyglobby "^0.2.15" ts-api-utils "^2.5.0" -"@typescript-eslint/utils@8.60.1": - version "8.60.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.60.1.tgz#31cf566095602d9fe8ad91837d2eb520b8de762b" - integrity sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg== +"@typescript-eslint/utils@8.61.0": + version "8.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.61.0.tgz#ed3546a052787e84ea6c5064d0919fc5eea8522f" + integrity sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA== dependencies: "@eslint-community/eslint-utils" "^4.9.1" - "@typescript-eslint/scope-manager" "8.60.1" - "@typescript-eslint/types" "8.60.1" - "@typescript-eslint/typescript-estree" "8.60.1" + "@typescript-eslint/scope-manager" "8.61.0" + "@typescript-eslint/types" "8.61.0" + "@typescript-eslint/typescript-estree" "8.61.0" -"@typescript-eslint/visitor-keys@8.60.1": - version "8.60.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz#165d1d8901137b944efaf18f00ab5ecb57f06995" - integrity sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag== +"@typescript-eslint/visitor-keys@8.61.0": + version "8.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz#39b4e1ab8936d23bea973d39fd092f9aa21f275e" + integrity sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ== dependencies: - "@typescript-eslint/types" "8.60.1" + "@typescript-eslint/types" "8.61.0" eslint-visitor-keys "^5.0.0" "@vitejs/plugin-react-swc@^4.3.1": @@ -6013,15 +6013,15 @@ typed-array-length@^1.0.7: possible-typed-array-names "^1.0.0" reflect.getprototypeof "^1.0.6" -typescript-eslint@^8.60.1: - version "8.60.1" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.60.1.tgz#13db05c6eabb89669deec44545b788a0e9aee640" - integrity sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA== +typescript-eslint@^8.61.0: + version "8.61.0" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.61.0.tgz#6927fb94f5f29623e370d33fd9fa61f15d6d996b" + integrity sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw== dependencies: - "@typescript-eslint/eslint-plugin" "8.60.1" - "@typescript-eslint/parser" "8.60.1" - "@typescript-eslint/typescript-estree" "8.60.1" - "@typescript-eslint/utils" "8.60.1" + "@typescript-eslint/eslint-plugin" "8.61.0" + "@typescript-eslint/parser" "8.61.0" + "@typescript-eslint/typescript-estree" "8.61.0" + "@typescript-eslint/utils" "8.61.0" typescript@^5.9.3: version "5.9.3" From b6bce030d0223047927a13c3042ea35e7d2bd41f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:20:51 +0700 Subject: [PATCH 074/136] 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 798bb1b0..076bc4c6 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/go-gormigrate/gormigrate/v2 v2.1.6 github.com/google/uuid v1.6.0 github.com/labstack/echo/v4 v4.15.2 - github.com/mattn/go-sqlite3 v1.14.45 + github.com/mattn/go-sqlite3 v1.14.46 github.com/nbd-wtf/ln-decodepay v1.13.0 github.com/orandin/lumberjackrus v1.0.1 github.com/peterldowns/pgtestdb v0.1.1 diff --git a/go.sum b/go.sum index a6541cce..feac3c37 100644 --- a/go.sum +++ b/go.sum @@ -455,8 +455,8 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= -github.com/mattn/go-sqlite3 v1.14.45 h1:6KA/spDguL3KV8rnybG7ezSaE4SeMR3KC9VbUoAQaIk= -github.com/mattn/go-sqlite3 v1.14.45/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= +github.com/mattn/go-sqlite3 v1.14.46 h1:ZfaNcYO/CGNMRxkN1vvG9qf+Y+uvXfgT9a6MlEw+HmU= +github.com/mattn/go-sqlite3 v1.14.46/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= From 005ee814a848f2186c9da23b402986f827eb1133 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:48:54 +0700 Subject: [PATCH 075/136] 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] * chore: remove unused LND wrapper interface and methods * chore: bump LND to v0.21.0-beta --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Adithya Vardhan --- go.mod | 23 ++++++++------- go.sum | 48 +++++++++++++++++-------------- lnclient/lnd/wrapper/interface.go | 41 -------------------------- lnclient/lnd/wrapper/lnd.go | 43 +++++++-------------------- 4 files changed, 48 insertions(+), 107 deletions(-) delete mode 100644 lnclient/lnd/wrapper/interface.go diff --git a/go.mod b/go.mod index 076bc4c6..1872de88 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.2 require ( github.com/adrg/xdg v0.5.3 - github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6 + github.com/btcsuite/btcd v0.25.1-0.20260310163610-1c55c7c18179 github.com/btcsuite/btcd/btcutil v1.2.0 github.com/elnosh/gonuts v0.4.2 github.com/getAlby/go-nostr v0.0.0-20260513161014-22fb7840c7a4 @@ -45,11 +45,12 @@ require ( github.com/aead/siphash v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bep/debounce v1.2.1 // indirect - github.com/btcsuite/btcd/btcutil/psbt v1.1.9 // indirect + github.com/btcsuite/btcd/btcutil/psbt v1.1.10 // indirect github.com/btcsuite/btcd/chainhash/v2 v2.0.0 // indirect - github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c // indirect + github.com/btcsuite/btcd/v2transport v1.0.1 // indirect + github.com/btcsuite/btclog v1.0.0 // indirect github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b // indirect - github.com/btcsuite/btcwallet v0.16.17 // indirect + github.com/btcsuite/btcwallet v0.16.18 // indirect github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5 // indirect github.com/btcsuite/btcwallet/wallet/txrules v1.2.2 // indirect github.com/btcsuite/btcwallet/wallet/txsizes v1.2.5 // indirect @@ -135,15 +136,15 @@ require ( github.com/leaanthony/u v1.1.1 // indirect github.com/lib/pq v1.10.9 // indirect github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect - github.com/lightninglabs/neutrino v0.16.1 // indirect - github.com/lightninglabs/neutrino/cache v1.1.2 // indirect - github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9 // indirect + github.com/lightninglabs/neutrino v0.17.1 // indirect + github.com/lightninglabs/neutrino/cache v1.1.3 // indirect + github.com/lightningnetwork/lightning-onion v1.3.0 // indirect github.com/lightningnetwork/lnd/clock v1.1.1 // indirect github.com/lightningnetwork/lnd/fn/v2 v2.0.9 // indirect github.com/lightningnetwork/lnd/healthcheck v1.2.6 // indirect github.com/lightningnetwork/lnd/kvdb v1.4.16 // indirect - github.com/lightningnetwork/lnd/queue v1.1.1 // indirect - github.com/lightningnetwork/lnd/sqldb v1.0.12-0.20260113193010-8565d12e40b1 // indirect + github.com/lightningnetwork/lnd/queue v1.2.0 // indirect + github.com/lightningnetwork/lnd/sqldb v1.0.13 // indirect github.com/lightningnetwork/lnd/ticker v1.1.1 // indirect github.com/lightningnetwork/lnd/tlv v1.3.2 // indirect github.com/lightningnetwork/lnd/tor v1.1.6 // indirect @@ -267,10 +268,10 @@ require ( github.com/joho/godotenv v1.5.1 github.com/kelseyhightower/envconfig v1.4.0 github.com/labstack/echo-jwt/v4 v4.4.0 - github.com/lightningnetwork/lnd v0.20.1-beta + github.com/lightningnetwork/lnd v0.21.0-beta github.com/sirupsen/logrus v1.9.4 github.com/tyler-smith/go-bip32 v1.0.0 - golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect + golang.org/x/exp v0.0.0-20250811191247-51f88131bc50 // indirect gorm.io/datatypes v1.2.7 ) diff --git a/go.sum b/go.sum index feac3c37..f16c9462 100644 --- a/go.sum +++ b/go.sum @@ -36,24 +36,26 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= -github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6 h1:8n9k3I7e8DkpdQ5YAP4j8ly/LSsbe6qX9vmVbrUGvVw= -github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6/go.mod h1:OmM4kFtB0klaG/ZqT86rQiyw/1iyXlJgc3UHClPhhbs= +github.com/btcsuite/btcd v0.25.1-0.20260310163610-1c55c7c18179 h1:yJOTxkbxxtuSFrErMqYRvqZLfWggHssioBiWebkV9yo= +github.com/btcsuite/btcd v0.25.1-0.20260310163610-1c55c7c18179/go.mod h1:qbPE+pEiR9643E1s1xu57awsRhlCIm1ZIi6FfeRA4KE= github.com/btcsuite/btcd/btcec/v2 v2.5.0 h1:KioMXOWa76b86sTZZOmbzv/ldaQCmB8KFAyn5PbB8E8= github.com/btcsuite/btcd/btcec/v2 v2.5.0/go.mod h1:+K/MYXcLBtHEQjRbjHuJChuybk4LCgjdjgRwil+e+Kk= github.com/btcsuite/btcd/btcutil v1.2.0 h1:p3+S2g3Q+7G5NOh4Ji+2UrBOrg5Z0Q4ykzShWG1Dhgs= github.com/btcsuite/btcd/btcutil v1.2.0/go.mod h1:/Taflm113pYjUpbWKKQEfa6XOtI/+WS8awxeMZpY75k= -github.com/btcsuite/btcd/btcutil/psbt v1.1.9 h1:UmfOIiWMZcVMOLaN+lxbbLSuoINGS1WmK1TZNI0b4yk= -github.com/btcsuite/btcd/btcutil/psbt v1.1.9/go.mod h1:ehBEvU91lxSlXtA+zZz3iFYx7Yq9eqnKx4/kSrnsvMY= +github.com/btcsuite/btcd/btcutil/psbt v1.1.10 h1:TC1zhxhFfhnGqoPjsrlEpoqzh+9TPOHrCgnPR47Mj9I= +github.com/btcsuite/btcd/btcutil/psbt v1.1.10/go.mod h1:ehBEvU91lxSlXtA+zZz3iFYx7Yq9eqnKx4/kSrnsvMY= github.com/btcsuite/btcd/chaincfg/chainhash v1.2.0 h1:yMIg99+4aBvqfl/HzJRKfxTX9rGfikoI9uvFzterhc8= github.com/btcsuite/btcd/chaincfg/chainhash v1.2.0/go.mod h1:Y72Ren9gfhlEvnwnT78BGcSNO2UMphTKLn9AorF+5rg= github.com/btcsuite/btcd/chainhash/v2 v2.0.0 h1:PMLlSloHJuEeB80XG9EjpXWNEKAZAMLl6YHZ6YsEuoA= github.com/btcsuite/btcd/chainhash/v2 v2.0.0/go.mod h1:mKxcZ7oGTXE7IRV+sS9hP4EVBwc/SzfNR+52IsOP9j8= -github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c h1:4HxD1lBUGUddhzgaNgrCPsFWd7cGYNpeFUgd9ZIgyM0= -github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c/go.mod h1:w7xnGOhwT3lmrS4H3b/D1XAXxvh+tbhUm8xeHN2y3TQ= +github.com/btcsuite/btcd/v2transport v1.0.1 h1:pIyyyBCPwd087K3Wdb/9tIvUubAQdzTJghjPgzTQVsE= +github.com/btcsuite/btcd/v2transport v1.0.1/go.mod h1:N6H0HGSElVVJKntzaYHYVbW71DtWDLMw2yhwVRO3ZOE= +github.com/btcsuite/btclog v1.0.0 h1:sEkpKJMmfGiyZjADwEIgB1NSwMyfdD1FB8v6+w1T0Ns= +github.com/btcsuite/btclog v1.0.0/go.mod h1:w7xnGOhwT3lmrS4H3b/D1XAXxvh+tbhUm8xeHN2y3TQ= github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b h1:MQ+Q6sDy37V1wP1Yu79A5KqJutolqUGwA99UZWQDWZM= github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b/go.mod h1:XItGUfVOxotJL8kkuk2Hj3EVow5KCugXl3wWfQ6K0AE= -github.com/btcsuite/btcwallet v0.16.17 h1:1N6lHznRdcjDopBvcofxaIHknArkJ/EcVKgLKfGL4Dg= -github.com/btcsuite/btcwallet v0.16.17/go.mod h1:YO+W745BAH8n/Rpgj68QsLR6eLlgM4W2do4RejT0buo= +github.com/btcsuite/btcwallet v0.16.18 h1:6h0kMxij4igPu35jOPAWZbn22ceOC4me4L3jj8Za6Zk= +github.com/btcsuite/btcwallet v0.16.18/go.mod h1:4TTru0cgIPbCZpY4aRfAVwX87zrQw4GXM8MH6+A5xZw= github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5 h1:Rr0njWI3r341nhSPesKQ2JF+ugDSzdPoeckS75SeDZk= github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5/go.mod h1:+tXJ3Ym0nlQc/iHSwW1qzjmPs3ev+UVWMbGgfV1OZqU= github.com/btcsuite/btcwallet/wallet/txrules v1.2.2 h1:YEO+Lx1ZJJAtdRrjuhXjWrYsmAk26wLTlNzxt2q0lhk= @@ -405,16 +407,18 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= -github.com/lightninglabs/neutrino v0.16.1 h1:5Kz4ToxncEVkpKC6fwUjXKtFKJhuxlG3sBB3MdJTJjs= -github.com/lightninglabs/neutrino v0.16.1/go.mod h1:L+5UAccpUdyM7yDgmQySgixf7xmwBgJtOfs/IP26jCs= -github.com/lightninglabs/neutrino/cache v1.1.2 h1:C9DY/DAPaPxbFC+xNNEI/z1SJY9GS3shmlu5hIQ798g= -github.com/lightninglabs/neutrino/cache v1.1.2/go.mod h1:XJNcgdOw1LQnanGjw8Vj44CvguYA25IMKjWFZczwZuo= +github.com/lightninglabs/neutrino v0.17.1 h1:lNhgq7ix/N81R6oATroP/kHMzH1qzVVF2dEGcTlN2t4= +github.com/lightninglabs/neutrino v0.17.1/go.mod h1:tcwCgRTGWcaua0L/xzdwllW8eslHDbux4XkiYsivvHE= +github.com/lightninglabs/neutrino/cache v1.1.3 h1:rgnabC41W+XaPuBTQrdeFjFCCAVKh1yctAgmb3Se9zA= +github.com/lightninglabs/neutrino/cache v1.1.3/go.mod h1:qxkJb+pUxR5p84jl5uIGFCR4dGdFkhNUwMSxw3EUWls= github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display h1:Y2WiPkBS/00EiEg0qp0FhehxnQfk3vv8U6Xt3nN+rTY= github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9 h1:6D3LrdagJweLLdFm1JNodZsBk6iU4TTsBBFLQ4yiXfI= -github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9/go.mod h1:EDqJ3MuZIbMq0QI1czTIKDJ/GS8S14RXPwapHw8cw6w= -github.com/lightningnetwork/lnd v0.20.1-beta h1:wDMNgks5uST1CY+WwjIZ4+McPMMFpr2pIIGJp7ytDI4= -github.com/lightningnetwork/lnd v0.20.1-beta/go.mod h1:oIKh9EqE1sJJpQPq9ZCMFc4Ot287NrotZ1oZn0zUI+M= +github.com/lightningnetwork/lightning-onion v1.3.0 h1:FqILgHjD6euc/Muo1VOzZ4+XDPuFnw6EYROBq0rR/5c= +github.com/lightningnetwork/lightning-onion v1.3.0/go.mod h1:nP85zMHG7c0si/eHBbSQpuDCtnIXfSvFrK3tW6YWzmU= +github.com/lightningnetwork/lnd v0.21.0-beta h1:bDP5UH15E7DVGTztsmBPQLqgyilq5EXDrglvQFmRc3U= +github.com/lightningnetwork/lnd v0.21.0-beta/go.mod h1:HcKq9DyxbVEZXuR28TIyGbIIgAjANCxI+N6dqOnRBAA= +github.com/lightningnetwork/lnd/actor v0.0.6 h1:Ge8N2wivARG+27qJBwTlB0vwsypStZYZy8vk4Zl38sU= +github.com/lightningnetwork/lnd/actor v0.0.6/go.mod h1:YAsoniSbY/cAM9HTVNfZLvt7RI6swDxy6wzPspTcMZg= github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0= github.com/lightningnetwork/lnd/clock v1.1.1/go.mod h1:mGnAhPyjYZQJmebS7aevElXKTFDuO+uNFFfMXK1W8xQ= github.com/lightningnetwork/lnd/fn/v2 v2.0.9 h1:ZytG4ltPac/sCyg1EJDn10RGzPIDJeyennUMRdOw7Y8= @@ -423,10 +427,10 @@ github.com/lightningnetwork/lnd/healthcheck v1.2.6 h1:1sWhqr93GdkWy4+6U7JxBfcyZI github.com/lightningnetwork/lnd/healthcheck v1.2.6/go.mod h1:Mu02um4CWY/zdTOvFje7WJgJcHyX2zq/FG3MhOAiGaQ= github.com/lightningnetwork/lnd/kvdb v1.4.16 h1:9BZgWdDfjmHRHLS97cz39bVuBAqMc4/p3HX1xtUdbDI= github.com/lightningnetwork/lnd/kvdb v1.4.16/go.mod h1:HW+bvwkxNaopkz3oIgBV6NEnV4jCEZCACFUcNg4xSjM= -github.com/lightningnetwork/lnd/queue v1.1.1 h1:99ovBlpM9B0FRCGYJo6RSFDlt8/vOkQQZznVb18iNMI= -github.com/lightningnetwork/lnd/queue v1.1.1/go.mod h1:7A6nC1Qrm32FHuhx/mi1cieAiBZo5O6l8IBIoQxvkz4= -github.com/lightningnetwork/lnd/sqldb v1.0.12-0.20260113193010-8565d12e40b1 h1:PkEppKL17cZh0Dr9h/T9BEVJUbd/p2tjJ/x8ffG3R0M= -github.com/lightningnetwork/lnd/sqldb v1.0.12-0.20260113193010-8565d12e40b1/go.mod h1:tB2jlqu79TIOR9uhAZOmPxpVFUhB2s+oxKnqRRL1oc0= +github.com/lightningnetwork/lnd/queue v1.2.0 h1:sSrn+u84OLuOT/F+xGxgg8VfknXeIZEAFQoMH6BL60s= +github.com/lightningnetwork/lnd/queue v1.2.0/go.mod h1:qLNP0L3B7piRGvDyhAyJKic4xTt+Mw4D7mWrQeuAwxY= +github.com/lightningnetwork/lnd/sqldb v1.0.13 h1:CcG9mrHNW/hIuZnqgosdiNmS7QhjSyfR/XkSFJB7EC8= +github.com/lightningnetwork/lnd/sqldb v1.0.13/go.mod h1:ew3kMfknA0B4djTtrQSAkxvro+8+c++L8LuNaoT7GQA= github.com/lightningnetwork/lnd/ticker v1.1.1 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6ijJlbdiZFbSM= github.com/lightningnetwork/lnd/ticker v1.1.1/go.mod h1:waPTRAAcwtu7Ji3+3k+u/xH5GHovTsCoSVpho0KDvdA= github.com/lightningnetwork/lnd/tlv v1.3.2 h1:MO4FCk7F4k5xPMqVZF6Nb/kOpxlwPrUQpYjmyKny5s0= @@ -747,8 +751,8 @@ golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZP golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/exp v0.0.0-20250811191247-51f88131bc50 h1:3yiSh9fhy5/RhCSntf4Sy0Tnx50DmMpQ4MQdKKk4yg4= +golang.org/x/exp v0.0.0-20250811191247-51f88131bc50/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= diff --git a/lnclient/lnd/wrapper/interface.go b/lnclient/lnd/wrapper/interface.go deleted file mode 100644 index 40460280..00000000 --- a/lnclient/lnd/wrapper/interface.go +++ /dev/null @@ -1,41 +0,0 @@ -package wrapper - -import ( - "context" - - "github.com/lightningnetwork/lnd/lnrpc" - "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" - "github.com/lightningnetwork/lnd/lnrpc/routerrpc" - "google.golang.org/grpc" -) - -type LightningClientWrapper interface { - ListChannels(ctx context.Context, req *lnrpc.ListChannelsRequest, options ...grpc.CallOption) (*lnrpc.ListChannelsResponse, error) - SendPaymentSync(req *lnrpc.SendRequest, options ...grpc.CallOption) (*lnrpc.SendResponse, error) - ChannelBalance(ctx context.Context, req *lnrpc.ChannelBalanceRequest, options ...grpc.CallOption) (*lnrpc.ChannelBalanceResponse, error) - AddInvoice(ctx context.Context, req *lnrpc.Invoice, options ...grpc.CallOption) (*lnrpc.AddInvoiceResponse, error) - AddHoldInvoice(ctx context.Context, req *invoicesrpc.AddHoldInvoiceRequest, options ...grpc.CallOption) (*invoicesrpc.AddHoldInvoiceResp, error) - SettleInvoice(ctx context.Context, req *invoicesrpc.SettleInvoiceMsg, options ...grpc.CallOption) (*invoicesrpc.SettleInvoiceResp, error) - CancelInvoice(ctx context.Context, req *invoicesrpc.CancelInvoiceMsg, options ...grpc.CallOption) (*invoicesrpc.CancelInvoiceResp, error) - SubscribeInvoices(ctx context.Context, req *lnrpc.InvoiceSubscription, options ...grpc.CallOption) (SubscribeInvoicesWrapper, error) - SubscribeSingleInvoice(ctx context.Context, req *invoicesrpc.SubscribeSingleInvoiceRequest, options ...grpc.CallOption) (SubscribeSingleInvoiceWrapper, error) // Added - SubscribePayment(ctx context.Context, req *routerrpc.TrackPaymentRequest, options ...grpc.CallOption) (SubscribePaymentWrapper, error) - LookupInvoice(ctx context.Context, req *lnrpc.PaymentHash, options ...grpc.CallOption) (*lnrpc.Invoice, error) - GetInfo(ctx context.Context, req *lnrpc.GetInfoRequest, options ...grpc.CallOption) (*lnrpc.GetInfoResponse, error) - DecodeBolt11(ctx context.Context, bolt11 string, options ...grpc.CallOption) (*lnrpc.PayReq, error) - IsIdentityPubkey(pubkey string) (isOurPubkey bool) - GetMainPubkey() (pubkey string) - SignMessage(ctx context.Context, req *lnrpc.SignMessageRequest, options ...grpc.CallOption) (*lnrpc.SignMessageResponse, error) -} - -type SubscribeInvoicesWrapper interface { - Recv() (*lnrpc.Invoice, error) -} - -type SubscribeSingleInvoiceWrapper interface { - Recv() (*lnrpc.Invoice, error) -} - -type SubscribePaymentWrapper interface { - Recv() (*lnrpc.Payment, error) -} diff --git a/lnclient/lnd/wrapper/lnd.go b/lnclient/lnd/wrapper/lnd.go index 4efd0f12..4c76904d 100644 --- a/lnclient/lnd/wrapper/lnd.go +++ b/lnclient/lnd/wrapper/lnd.go @@ -16,9 +16,16 @@ import ( "gopkg.in/macaroon.v2" ) -type LNPayReq struct { - PayReq *lnrpc.PayReq - Keysend bool +type SubscribeInvoicesWrapper interface { + Recv() (*lnrpc.Invoice, error) +} + +type SubscribeSingleInvoiceWrapper interface { + Recv() (*lnrpc.Invoice, error) +} + +type SubscribePaymentWrapper interface { + Recv() (*lnrpc.Payment, error) } // LNDoptions are the options for the connection to the lnd node. @@ -110,10 +117,6 @@ func (wrapper *LNDWrapper) SendPayment(ctx context.Context, req *routerrpc.SendP return wrapper.routerClient.SendPaymentV2(ctx, req, options...) } -func (wrapper *LNDWrapper) ChannelBalance(ctx context.Context, req *lnrpc.ChannelBalanceRequest, options ...grpc.CallOption) (*lnrpc.ChannelBalanceResponse, error) { - return wrapper.client.ChannelBalance(ctx, req, options...) -} - func (wrapper *LNDWrapper) AddInvoice(ctx context.Context, req *lnrpc.Invoice, options ...grpc.CallOption) (*lnrpc.AddInvoiceResponse, error) { return wrapper.client.AddInvoice(ctx, req, options...) } @@ -146,10 +149,6 @@ func (wrapper *LNDWrapper) ListInvoices(ctx context.Context, req *lnrpc.ListInvo return wrapper.client.ListInvoices(ctx, req, options...) } -func (wrapper *LNDWrapper) ListPayments(ctx context.Context, req *lnrpc.ListPaymentsRequest, options ...grpc.CallOption) (*lnrpc.ListPaymentsResponse, error) { - return wrapper.client.ListPayments(ctx, req, options...) -} - func (wrapper *LNDWrapper) LookupInvoice(ctx context.Context, req *lnrpc.PaymentHash, options ...grpc.CallOption) (*lnrpc.Invoice, error) { return wrapper.client.LookupInvoice(ctx, req, options...) } @@ -162,10 +161,6 @@ func (wrapper *LNDWrapper) GetInfo(ctx context.Context, req *lnrpc.GetInfoReques return wrapper.client.GetInfo(ctx, req, options...) } -func (wrapper *LNDWrapper) GetNetworkInfo(ctx context.Context, req *lnrpc.NetworkInfoRequest, options ...grpc.CallOption) (*lnrpc.NetworkInfo, error) { - return wrapper.client.GetNetworkInfo(ctx, req, options...) -} - func (wrapper *LNDWrapper) DescribeGraph(ctx context.Context, req *lnrpc.ChannelGraphRequest, options ...grpc.CallOption) (*lnrpc.ChannelGraph, error) { return wrapper.client.DescribeGraph(ctx, req, options...) } @@ -178,24 +173,6 @@ func (wrapper *LNDWrapper) GetNodeInfo(ctx context.Context, req *lnrpc.NodeInfoR return wrapper.client.GetNodeInfo(ctx, req, options...) } -func (wrapper *LNDWrapper) DecodeBolt11(ctx context.Context, bolt11 string, options ...grpc.CallOption) (*lnrpc.PayReq, error) { - return wrapper.client.DecodePayReq(ctx, &lnrpc.PayReqString{ - PayReq: bolt11, - }) -} - -func (wrapper *LNDWrapper) SubscribePayment(ctx context.Context, req *routerrpc.TrackPaymentRequest, options ...grpc.CallOption) (SubscribePaymentWrapper, error) { - return wrapper.routerClient.TrackPaymentV2(ctx, req, options...) -} - -func (wrapper *LNDWrapper) IsIdentityPubkey(pubkey string) (isOurPubkey bool) { - return pubkey == wrapper.IdentityPubkey -} - -func (wrapper *LNDWrapper) GetMainPubkey() (pubkey string) { - return wrapper.IdentityPubkey -} - func (wrapper *LNDWrapper) SignMessage(ctx context.Context, req *lnrpc.SignMessageRequest, options ...grpc.CallOption) (*lnrpc.SignMessageResponse, error) { return wrapper.client.SignMessage(ctx, req, options...) } From 754acfc9df07b671eced8e01c35a7185ed63e2e8 Mon Sep 17 00:00:00 2001 From: Anshuman <109489361+Anshumancanrock@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:21:07 +0530 Subject: [PATCH 076/136] fix: update wave.space affiliate URL (#2475) --- frontend/src/components/connections/SuggestedAppData.tsx | 7 +++---- frontend/src/screens/cards/Cards.tsx | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/connections/SuggestedAppData.tsx b/frontend/src/components/connections/SuggestedAppData.tsx index f514d129..3647a71e 100644 --- a/frontend/src/components/connections/SuggestedAppData.tsx +++ b/frontend/src/components/connections/SuggestedAppData.tsx @@ -1268,8 +1268,7 @@ export const appStoreApps: AppStoreApp[] = ( title: "wavecard® by wave.space", description: "Spend Bitcoin from your AlbyHub at 150M+ merchants worldwide", - webLink: - "https://app.wave.space/spend/?utm_source=albyhub&affiliate=AlbyHub", + webLink: "https://app.wave.space/?utm_source=albyhub&affiliate=AlbyHub", logo: wavespace, extendedDescription: "The world's first Bitcoin VISA Debit Card that allows you to spend BTC globally, anywhere VISA is accepted – straight from the safety of your own NWC-enabled wallet. ✨ EXCLUSIVE ALBYHUB SPECIAL🐝 → Get 21% cashback on your wavecard transactions (up to 10,000 sats) using code »AlbyHub«", @@ -1281,10 +1280,10 @@ export const appStoreApps: AppStoreApp[] = (
  • Open{" "} - wave.space/spend + wave.space {" "} in your browser and{" "} diff --git a/frontend/src/screens/cards/Cards.tsx b/frontend/src/screens/cards/Cards.tsx index caae21e9..20921d3a 100644 --- a/frontend/src/screens/cards/Cards.tsx +++ b/frontend/src/screens/cards/Cards.tsx @@ -198,7 +198,7 @@ const providers: Provider[] = [ { id: "wavespace", name: "wavecard by wave.space", - url: "https://app.wave.space/spend/?utm_source=albyhub&affiliate=AlbyHub", + url: "https://app.wave.space/?utm_source=albyhub&affiliate=AlbyHub", logo: wavespaceLogo, initials: "WS", networks: ["Visa"], From afdb842a5470d552cadaec1e3aaf5ea32c48cf57 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:52:09 +0700 Subject: [PATCH 077/136] 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package.json | 2 +- frontend/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 6a0b25cf..9d50b921 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -57,7 +57,7 @@ "@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.2.4", "@types/node": "^25.9.3", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 40cb3a19..f5140f25 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2597,10 +2597,10 @@ resolved "https://registry.yarnpkg.com/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz#9ffd52fee8e3c8b20623ff0dcb29e5c21fb0a9ba" integrity sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ== -"@tailwindcss/forms@^0.5.7": - version "0.5.10" - resolved "https://registry.yarnpkg.com/@tailwindcss/forms/-/forms-0.5.10.tgz#0a1cd67b6933402f1985a04595bd24f9785aa302" - integrity sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw== +"@tailwindcss/forms@^0.5.11": + version "0.5.11" + resolved "https://registry.yarnpkg.com/@tailwindcss/forms/-/forms-0.5.11.tgz#e77039e96fa7b87c3d001a991f77f9418e666700" + integrity sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA== dependencies: mini-svg-data-uri "^1.2.3" From be17bc4e261c5b96877c44be51bbdc6208e9bc43 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:54:37 +0700 Subject: [PATCH 078/136] chore: replace alby mutinynet lsps2 with megalith mutinynet lsp (#2469) --- lnclient/ldk/ldk.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lnclient/ldk/ldk.go b/lnclient/ldk/ldk.go index 28c0767c..9131aed3 100644 --- a/lnclient/ldk/ldk.go +++ b/lnclient/ldk/ldk.go @@ -172,8 +172,8 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events if liquiditySourceLsps2 == "" { switch network { case "signet": - // Alby LSP (Mutinynet) - liquiditySourceLsps2 = "025010bd608771bc13f08f696e3dd226bf3a9ae6ea461e3922ed9bdca7bb0edfe5@141.95.84.44:9735" + // Megalith LSP 2 (Mutinynet) + liquiditySourceLsps2 = "03e30fda71887a916ef5548a4d02b06fe04aaa1a8de9e24134ce7f139cf79d7579@64.23.192.68:9736" case "bitcoin": // Megalith LSP 2 liquiditySourceLsps2 = "034066e29e402d9cf55af1ae1026cc5adf92eed1e0e421785442f53717ad1453b0@64.23.159.177:9735" From ce46a0f8a0038ecdc362c8795de56b8b410524c8 Mon Sep 17 00:00:00 2001 From: Peter <130258664+PeteClubSeven@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:25:25 +0100 Subject: [PATCH 079/136] 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. --- go.mod | 2 +- go.sum | 4 +- lnclient/bark/bark.go | 136 +++++++++++++++++++++++------------------- 3 files changed, 78 insertions(+), 64 deletions(-) diff --git a/go.mod b/go.mod index 1872de88..a3a8e665 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/tyler-smith/go-bip39 v1.1.0 github.com/wailsapp/wails/v2 v2.12.0 - gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.8.0 + gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.12.1 golang.org/x/crypto v0.52.0 golang.org/x/oauth2 v0.36.0 google.golang.org/grpc v1.79.3 diff --git a/go.sum b/go.sum index f16c9462..a3095caa 100644 --- a/go.sum +++ b/go.sum @@ -666,8 +666,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.8.0 h1:YKHSM8iNFMmTQ/QPUxC5U9KVim10F7sIGt3ou2wet+o= -gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.8.0/go.mod h1:1jAwB/XR4i3D72fz3qWAd41tQLYcOCGfWZHMagn5fNg= +gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.12.1 h1:jEnl0leC9n7EsT9Rn339nC9e/9GWCMPXzmzowzxmY24= +gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.12.1/go.mod h1:1jAwB/XR4i3D72fz3qWAd41tQLYcOCGfWZHMagn5fNg= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.etcd.io/etcd/api/v3 v3.5.16 h1:WvmyJVbjWqK4R1E+B12RRHz3bRGy9XVfh++MgbN+6n0= diff --git a/lnclient/bark/bark.go b/lnclient/bark/bark.go index 23947b6e..c73e8f02 100644 --- a/lnclient/bark/bark.go +++ b/lnclient/bark/bark.go @@ -4,10 +4,8 @@ package bark import ( "context" - "encoding/json" "errors" "fmt" - "os" "strconv" "strings" "sync" @@ -31,11 +29,14 @@ const ( // Subsystem name reported on movements produced for outgoing lightning // payments (see bark's Subsystem::LIGHTNING_SEND). lightningSendSubsystem = "lightning_send" + // The status a movement is created with; every other status is terminal. + movementStatusPending = "pending" // Movement status reported once a movement has settled. A movement first // appears as "pending" and is updated to this once complete. movementStatusSuccessful = "successful" - // Movement status reported when a send was definitively not paid. - movementStatusFailed = "failed" + // LightningReceive.State values in which we hold the preimage. + receiveStatePreimageRevealed = "preimage-revealed" + receiveStateSettled = "settled" // Grace period to allow the notification loop to unwind on shutdown. shutdownGracePeriod = 10 * time.Second ) @@ -123,7 +124,6 @@ func NewBarkService(ctx context.Context, eventPublisher events.EventPublisher, w cfg := bark.Config{ ServerAddress: config.ServerAddress, - Network: network, RoundTxRequiredConfirmations: &roundTxRequiredConfirmations, } esploraAddress := config.EsploraAddress @@ -135,23 +135,7 @@ func NewBarkService(ctx context.Context, eventPublisher events.EventPublisher, w cfg.ServerAccessToken = &token } - _, statErr := os.Stat(workDir) - isFirstSetup := statErr != nil && errors.Is(statErr, os.ErrNotExist) - - logger.Logger.WithFields(logrus.Fields{ - "workDir": workDir, - "isFirstSetup": isFirstSetup, - }).Info("Opening Bark wallet") - - var wallet *bark.Wallet - if isFirstSetup { - wallet, err = bark.WalletCreate(mnemonic, cfg, workDir, false) - } else { - wallet, err = bark.WalletOpen(mnemonic, cfg, workDir) - } - if err != nil { - return nil, fmt.Errorf("failed to open bark wallet: %w", err) - } + logger.Logger.WithField("workDir", workDir).Info("Opening Bark wallet") // Bark provides a built-in background daemon that periodically syncs with // the Ark server and blockchain, participates in rounds, and — crucially for @@ -159,8 +143,13 @@ func NewBarkService(ctx context.Context, eventPublisher events.EventPublisher, w // payment notifications and reveals the preimage, crediting the balance). We // don't poll for receives ourselves; instead we observe the resulting wallet // notifications (see runNotificationLoop) to emit payment-received events. - if err := wallet.RunDaemon(nil); err != nil { - logger.Logger.WithError(err).Warn("Bark daemon failed to start") + wallet, err := bark.WalletOpen(network, mnemonic, cfg, bark.WalletOpenArgs{ + Datadir: workDir, + RunDaemon: true, + CreateIfNotExists: true, + }) + if err != nil { + return nil, fmt.Errorf("failed to open bark wallet: %w", err) } loopCtx, cancelFn := context.WithCancel(context.Background()) @@ -260,6 +249,8 @@ func (bs *BarkService) handleLightningReceiveMovement(movement bark.Movement) { // A receive is only credited once its movement settles. We always hold the // preimage for our own receives, so PreimageRevealed isn't a useful signal; // the balance is credited when the movement status reaches "successful". + // An abandoned receive finishes as "canceled": no funds arrived, so there is + // nothing to report. if movement.Status != movementStatusSuccessful { return } @@ -269,13 +260,13 @@ func (bs *BarkService) handleLightningReceiveMovement(movement bark.Movement) { return } - receive, err := bs.wallet.LightningReceiveStatus(paymentHash) - if err != nil || receive == nil { + receive, err := bs.wallet.LightningReceiveState(paymentHash) + if err != nil { logger.Logger.WithError(err).WithField("paymentHash", paymentHash).Warn("Failed to look up claimed Bark receive") return } - tx, err := bs.lightningReceiveToTransaction(receive) + tx, err := bs.lightningReceiveToTransaction(&receive) if err != nil { logger.Logger.WithError(err).WithField("paymentHash", receive.PaymentHash).Warn("Failed to convert claimed Bark receive to transaction") return @@ -296,55 +287,60 @@ func (bs *BarkService) handleLightningReceiveMovement(movement bark.Movement) { // goroutine is gone) it falls back to publishing nwc_lnclient_payment_sent / // _failed so the transactions service can recover the db transaction state. func (bs *BarkService) handleLightningSendMovement(movement bark.Movement) { - if movement.Status != movementStatusSuccessful && movement.Status != movementStatusFailed { + if movement.Status == movementStatusPending { return } - var meta struct { - PaymentHash string `json:"payment_hash"` - PaymentPreimage string `json:"payment_preimage"` - } - if err := json.Unmarshal([]byte(movement.MetadataJson), &meta); err != nil || meta.PaymentHash == "" { - logger.Logger.WithError(err).WithField("movementId", movement.Id).Debug("Bark lightning send movement missing payment_hash") + paymentHash, ok := paymentHashFromMovement(movement) + if !ok { return } - if movement.Status == movementStatusFailed { - bs.deliverSendResult(meta.PaymentHash, sendResult{err: errors.New("bark lightning send failed")}, func() { + // The movement can be canceled or failed so we should just check if it + // wasn't successful. + if movement.Status != movementStatusSuccessful { + reason := fmt.Sprintf("bark lightning send %s", movement.Status) + logger.Logger.WithFields(logrus.Fields{ + "paymentHash": paymentHash, + "status": movement.Status, + "reason": reason, + }).Warn("Bark lightning send did not succeed") + bs.deliverSendResult(paymentHash, sendResult{err: errors.New(reason)}, func() { bs.eventPublisher.Publish(&events.Event{ Event: "nwc_lnclient_payment_failed", Properties: &lnclient.PaymentFailedEventProperties{ Transaction: &lnclient.Transaction{ Type: constants.TRANSACTION_TYPE_OUTGOING, - PaymentHash: meta.PaymentHash, + PaymentHash: paymentHash, }, - Reason: "bark lightning send failed", + Reason: reason, }, }) }) return } - if meta.PaymentPreimage == "" { - logger.Logger.WithField("paymentHash", meta.PaymentHash).Error("Bark lightning send reported successful but preimage is missing from movement metadata") - bs.deliverSendResult(meta.PaymentHash, sendResult{err: errors.New("bark lightning send completed without a preimage")}, nil) + preimage, err := bs.getSettledSendPreimage(paymentHash) + if err != nil { + logger.Logger.WithError(err).WithField("paymentHash", paymentHash).Error("Bark lightning send reported successful but no preimage is available") + bs.deliverSendResult(paymentHash, sendResult{err: fmt.Errorf("bark lightning send completed without a preimage: %w", err)}, nil) return } feeMsat := movement.OffchainFeeSats * 1000 logger.Logger.WithFields(logrus.Fields{ - "paymentHash": meta.PaymentHash, + "paymentHash": paymentHash, "feeMsat": feeMsat, }).Info("Bark lightning send completed") - bs.deliverSendResult(meta.PaymentHash, sendResult{preimage: meta.PaymentPreimage, feeMsat: feeMsat}, func() { + bs.deliverSendResult(paymentHash, sendResult{preimage: preimage, feeMsat: feeMsat}, func() { settledAt := time.Now().Unix() bs.eventPublisher.Publish(&events.Event{ Event: "nwc_lnclient_payment_sent", Properties: &lnclient.Transaction{ Type: constants.TRANSACTION_TYPE_OUTGOING, - PaymentHash: meta.PaymentHash, - Preimage: meta.PaymentPreimage, + PaymentHash: paymentHash, + Preimage: preimage, FeesPaidMsat: int64(feeMsat), SettledAt: &settledAt, }, @@ -352,6 +348,24 @@ func (bs *BarkService) handleLightningSendMovement(movement bark.Movement) { }) } +// Reads the preimage from the lightning-send's own state. Bark records the paid +// invoice before finishing the movement, so it is always persisted by the time +// the successful movement is observed. +func (bs *BarkService) getSettledSendPreimage(paymentHash string) (string, error) { + status, err := bs.wallet.LightningSendState(paymentHash) + if err != nil { + return "", fmt.Errorf("failed to look up bark lightning send state: %w", err) + } + paid, ok := status.(bark.LightningSendStatusPaid) + if !ok { + return "", fmt.Errorf("send is in state %T, expected settled", status) + } + if paid.Preimage == "" { + return "", errors.New("settled send has an empty preimage") + } + return paid.Preimage, nil +} + // deliverSendResult delivers to the SendPaymentSync waiter if present, else // runs fallback (used to publish an event for the hub-restart recovery path). func (bs *BarkService) deliverSendResult(paymentHash string, res sendResult, fallback func()) { @@ -365,17 +379,14 @@ func (bs *BarkService) deliverSendResult(paymentHash string, res sendResult, fal } func paymentHashFromMovement(movement bark.Movement) (string, bool) { - var meta struct { - PaymentHash string `json:"payment_hash"` - } - if err := json.Unmarshal([]byte(movement.MetadataJson), &meta); err != nil || meta.PaymentHash == "" { - logger.Logger.WithError(err).WithFields(logrus.Fields{ + if movement.PaymentHash == nil || *movement.PaymentHash == "" { + logger.Logger.WithFields(logrus.Fields{ "movementId": movement.Id, "subsystemName": movement.SubsystemName, }).Debug("Bark lightning movement missing payment_hash") return "", false } - return meta.PaymentHash, true + return *movement.PaymentHash, true } // notificationLogFields turns a Bark wallet notification into structured log @@ -429,7 +440,8 @@ func (bs *BarkService) MakeInvoice(ctx context.Context, amountMsat int64, descri desc = &description } - invoice, err := bs.wallet.Bolt11Invoice(uint64(amountMsat/1000), desc) + // The nil argument is an optional anti-DoS token, which we don't use. + invoice, err := bs.wallet.Bolt11Invoice(uint64(amountMsat/1000), desc, nil) if err != nil { return nil, fmt.Errorf("bark Bolt11Invoice failed: %w", err) } @@ -443,18 +455,17 @@ func (bs *BarkService) MakeInvoice(ctx context.Context, amountMsat int64, descri expiresAtUnix := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix() // The preimage is generated alongside the invoice but is not returned by - // Bolt11Invoice. Fetch it via the receive status so consumers can rely on + // Bolt11Invoice. Fetch it via the receive state so consumers can rely on // lookup_invoice exposing the real preimage. - var preimage string - receive, err := bs.wallet.LightningReceiveStatus(paymentRequest.PaymentHash) + receive, err := bs.wallet.LightningReceiveState(paymentRequest.PaymentHash) if err != nil { - logger.Logger.WithError(err).WithField("paymentHash", paymentRequest.PaymentHash).Error("Failed to fetch bark receive status for preimage") - return nil, err + logger.Logger.WithError(err).WithField("paymentHash", paymentRequest.PaymentHash).Error("Failed to fetch bark receive state for preimage") + return nil, fmt.Errorf("failed to fetch bark receive state for preimage: %w", err) } - preimage = receive.PaymentPreimage - if preimage == "" { + if receive.PaymentPreimage == nil || *receive.PaymentPreimage == "" { return nil, errors.New("no preimage available") } + preimage := *receive.PaymentPreimage return &lnclient.Transaction{ Type: constants.TRANSACTION_TYPE_INCOMING, @@ -551,10 +562,13 @@ func (bs *BarkService) lightningReceiveToTransaction(receive *bark.LightningRece Description: paymentRequest.Description, DescriptionHash: paymentRequest.DescriptionHash, } - if receive.PreimageRevealed { + // "preimage-revealed" until the claim is recorded, "settled" after. + if receive.State == receiveStatePreimageRevealed || receive.State == receiveStateSettled { now := time.Now().Unix() tx.SettledAt = &now - tx.Preimage = receive.PaymentPreimage + if receive.PaymentPreimage != nil { + tx.Preimage = *receive.PaymentPreimage + } } return tx, nil } From 32af89bc8c6626d6b8cf35c53c1b2fcdc38950ec Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:31:18 +0700 Subject: [PATCH 080/136] chore: bump rebalance fees to ensure payment succeeds (#2470) --- api/rebalance.go | 2 +- frontend/src/components/RebalanceChannelDialogContent.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/rebalance.go b/api/rebalance.go index 9afce454..2854d5b3 100644 --- a/api/rebalance.go +++ b/api/rebalance.go @@ -122,7 +122,7 @@ func (api *api) RebalanceChannel(ctx context.Context, rebalanceChannelRequest *R return nil, err } - if paymentRequest.MSatoshi > int64(float64(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") } diff --git a/frontend/src/components/RebalanceChannelDialogContent.tsx b/frontend/src/components/RebalanceChannelDialogContent.tsx index 0e9f3da4..96a44efb 100644 --- a/frontend/src/components/RebalanceChannelDialogContent.tsx +++ b/frontend/src/components/RebalanceChannelDialogContent.tsx @@ -134,7 +134,7 @@ export function RebalanceChannelDialogContent({ }} />

    - Fee: 0.3% + Fee: 0.5% {!!amountSat && ( <>  ( From bf0ebe1f33e0d8ab67adb137528786d71b5fb79f Mon Sep 17 00:00:00 2001 From: hermes-alby Date: Tue, 4 Aug 2026 16:14:30 +0700 Subject: [PATCH 081/136] ci: support builds for fork pull requests (#2492) ci: skip macOS signing for untrusted PRs Co-authored-by: Hermes Agent --- .github/workflows/http.yml | 32 +++++++++++++++++++++++++++++--- .github/workflows/wails.yml | 30 +++++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/.github/workflows/http.yml b/.github/workflows/http.yml index 9d01e13d..6a7ef1b7 100644 --- a/.github/workflows/http.yml +++ b/.github/workflows/http.yml @@ -25,6 +25,7 @@ on: jobs: build: strategy: + fail-fast: false matrix: build: [ @@ -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 diff --git a/.github/workflows/wails.yml b/.github/workflows/wails.yml index 71079d7c..c11aadc9 100644 --- a/.github/workflows/wails.yml +++ b/.github/workflows/wails.yml @@ -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 From e0c173ae1e62029f8c7cf2d39b1571bccb977526 Mon Sep 17 00:00:00 2001 From: hermes-alby Date: Tue, 4 Aug 2026 16:14:54 +0700 Subject: [PATCH 082/136] docs: add security policy (#2491) * docs: add security policy * docs: link security policy from README --------- Co-authored-by: Hermes Agent --- README.md | 2 ++ SECURITY.md | 7 +++++++ 2 files changed, 9 insertions(+) create mode 100644 SECURITY.md diff --git a/README.md b/README.md index c92fc752..1572f1dc 100644 --- a/README.md +++ b/README.md @@ -396,6 +396,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? diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..790ba2be --- /dev/null +++ b/SECURITY.md @@ -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. From fa03a9ba7055dbd4ddece6a272c075c1e87074cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:25:24 +0700 Subject: [PATCH 083/136] 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] * chore: bump node version in workflows --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Roland Bewick --- .github/workflows/http.yml | 2 +- .github/workflows/linting.yml | 2 +- .github/workflows/wails.yml | 2 +- frontend/package.json | 2 +- frontend/yarn.lock | 33 ++++++++++++++------------------- 5 files changed, 18 insertions(+), 23 deletions(-) diff --git a/.github/workflows/http.yml b/.github/workflows/http.yml index 6a7ef1b7..8353152d 100644 --- a/.github/workflows/http.yml +++ b/.github/workflows/http.yml @@ -86,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 ./... diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 5bd519ad..ee2e75c7 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -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 diff --git a/.github/workflows/wails.yml b/.github/workflows/wails.yml index c11aadc9..b74faa5c 100644 --- a/.github/workflows/wails.yml +++ b/.github/workflows/wails.yml @@ -73,7 +73,7 @@ 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.12.0 diff --git a/frontend/package.json b/frontend/package.json index 9d50b921..90cae564 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,7 +24,7 @@ "@fontsource-variable/figtree": "^5.2.10", "@fontsource-variable/inter": "^5.2.8", "@getalby/lightning-tools": "^8.1.0", - "@getalby/sdk": "^7.0.0", + "@getalby/sdk": "^8.0.3", "@scure/bip39": "^2.2.0", "@stepperize/react": "^6.1.0", "argon2-wasm-esm": "^1.0.3", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index f5140f25..b8c499ee 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1326,23 +1326,18 @@ resolved "https://registry.yarnpkg.com/@fontsource-variable/inter/-/inter-5.2.8.tgz#29b11476f5149f6a443b4df6516e26002d87941a" integrity sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ== -"@getalby/lightning-tools@^6.0.0": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@getalby/lightning-tools/-/lightning-tools-6.0.0.tgz#07c19cdf29ed8e3d51f125fba780d31a29ba0864" - integrity sha512-jpTO+7o1N1KhV5qT6qetPK+et6ZQshCzUMCRV8+Ek1NVlVU4ITIqOWRQ3kOrb0PhSxkbGN5G3d60HCi535hbDw== +"@getalby/lightning-tools@^8.1.0", "@getalby/lightning-tools@^8.1.1": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@getalby/lightning-tools/-/lightning-tools-8.2.0.tgz#7bcd7c24149f5dfe59e4660aebcda22637d1f207" + integrity sha512-eL+cnHyzeUARKVzNRuFBRBppAU5RBJOLjhFVzSWt8hDibRzL5+e4hq8I79+RGHfrWWMUDv382Jx8ewOazrBj7w== -"@getalby/lightning-tools@^8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@getalby/lightning-tools/-/lightning-tools-8.1.0.tgz#8aefcfba90fd43ccf87e6ccf8b426d21149222fe" - integrity sha512-P+wM1zNNwiSxA071jl2sNyPq/jfX02OSAEXIre/dL7P4dj0yC3YeyByI1/nKvPNqaFvXzHQn+7D9+Yo85aocRQ== - -"@getalby/sdk@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@getalby/sdk/-/sdk-7.0.0.tgz#6ab17f27bd9e762d383b70cfabd0cdeeded6bd53" - integrity sha512-0c8gyvFbRDHZIgHmOD/dfyPukxZLeidx/hx7SXlMIS/hsx4mXpKpo9Gx1zW90buElnd3k9TVB/S/bnFSEZPE7w== +"@getalby/sdk@^8.0.3": + version "8.0.3" + resolved "https://registry.yarnpkg.com/@getalby/sdk/-/sdk-8.0.3.tgz#dd59ce94682c1c041fa90b42c16e959aa2d59f03" + integrity sha512-vPEogAWwLHbL55COeXrRN7yRBXMDGDxX1M2A+o3Lqw60FFng6fa9FK9sw8ptdILMD1dQZq8FzPXuabQ7seoDBA== dependencies: - "@getalby/lightning-tools" "^6.0.0" - nostr-tools "^2.17.0" + "@getalby/lightning-tools" "^8.1.1" + nostr-tools "^2.23.3" "@humanfs/core@^0.19.1": version "0.19.1" @@ -4861,10 +4856,10 @@ node-releases@^2.0.19: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== -nostr-tools@^2.17.0: - version "2.23.3" - resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.23.3.tgz#1a7501988b72499cf27c8f3951f00d11d9ac6025" - integrity sha512-AALyt9k8xPdF4UV2mlLJ2mgCn4kpTB0DZ8t2r6wjdUh6anfx2cTVBsHUlo9U0EY/cKC5wcNyiMAmRJV5OVEalA== +nostr-tools@^2.23.3: + version "2.24.1" + resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.24.1.tgz#dfba0b30310d7d22d4ae6b6233a038913ab99b46" + integrity sha512-KdrKjC74n/rr6J3eCSfZj8dcbZFvolHYe4S22SefNZ5YWbhHiB0KL/mmJjEZ0u6B9mZK0YcQtl+WQ46KzwapeQ== dependencies: "@noble/ciphers" "2.1.1" "@noble/curves" "2.0.1" From c99ca51ad50f811c4e36afdb6800a77a635af3b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:29:20 +0700 Subject: [PATCH 084/136] 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a3a8e665..7f7dc661 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/go-gormigrate/gormigrate/v2 v2.1.6 github.com/google/uuid v1.6.0 github.com/labstack/echo/v4 v4.15.2 - github.com/mattn/go-sqlite3 v1.14.46 + github.com/mattn/go-sqlite3 v1.14.48 github.com/nbd-wtf/ln-decodepay v1.13.0 github.com/orandin/lumberjackrus v1.0.1 github.com/peterldowns/pgtestdb v0.1.1 diff --git a/go.sum b/go.sum index a3095caa..00ace59f 100644 --- a/go.sum +++ b/go.sum @@ -459,8 +459,8 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= -github.com/mattn/go-sqlite3 v1.14.46 h1:ZfaNcYO/CGNMRxkN1vvG9qf+Y+uvXfgT9a6MlEw+HmU= -github.com/mattn/go-sqlite3 v1.14.46/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= +github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs= +github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= From 857f145784334303a8a624693d28a7b423cece17 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:31:20 +0700 Subject: [PATCH 085/136] 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package.json | 2 +- frontend/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 90cae564..9477c25e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,7 +22,7 @@ "dependencies": { "@base-ui/react": "^1.5.0", "@fontsource-variable/figtree": "^5.2.10", - "@fontsource-variable/inter": "^5.2.8", + "@fontsource-variable/inter": "^5.3.0", "@getalby/lightning-tools": "^8.1.0", "@getalby/sdk": "^8.0.3", "@scure/bip39": "^2.2.0", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index b8c499ee..c65dadca 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1321,10 +1321,10 @@ resolved "https://registry.yarnpkg.com/@fontsource-variable/figtree/-/figtree-5.2.10.tgz#ad72d1b91646918073108e06464974d4f132b878" integrity sha512-a5Gumbpy3mdd+Yg31g6Qb7CmjYbrfyutJa3bWfP5q8A4GclIOwX7mI+ZuSHsJnw/mHvW6r9oh1AHJcJTIxK4JA== -"@fontsource-variable/inter@^5.2.8": - version "5.2.8" - resolved "https://registry.yarnpkg.com/@fontsource-variable/inter/-/inter-5.2.8.tgz#29b11476f5149f6a443b4df6516e26002d87941a" - integrity sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ== +"@fontsource-variable/inter@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@fontsource-variable/inter/-/inter-5.3.0.tgz#351dd1e02dab63a6cf66d57ec36dcfd10c07f07b" + integrity sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA== "@getalby/lightning-tools@^8.1.0", "@getalby/lightning-tools@^8.1.1": version "8.2.0" From 98288463de0caad94839213394f2c22627036923 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:32:52 +0700 Subject: [PATCH 086/136] 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7f7dc661..b7f1bb5a 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( gopkg.in/macaroon.v2 v2.1.0 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 - gorm.io/gorm v1.31.1 + gorm.io/gorm v1.31.2 ) require ( diff --git a/go.sum b/go.sum index 00ace59f..aa973c13 100644 --- a/go.sum +++ b/go.sum @@ -961,8 +961,8 @@ gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwy gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc= gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= -gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo= +gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= From 5b49783478ec411449320c6f6a1a6553d86a13d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:33:30 +0700 Subject: [PATCH 087/136] 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 16 ++++++++-------- go.sum | 32 ++++++++++++++++---------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index b7f1bb5a..7b64611b 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/tyler-smith/go-bip39 v1.1.0 github.com/wailsapp/wails/v2 v2.12.0 gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.12.1 - golang.org/x/crypto v0.52.0 + golang.org/x/crypto v0.54.0 golang.org/x/oauth2 v0.36.0 google.golang.org/grpc v1.79.3 google.golang.org/protobuf v1.36.10 @@ -229,14 +229,14 @@ require ( go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.15.0 // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/net v0.54.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/term v0.43.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/term v0.45.0 // indirect + golang.org/x/text v0.40.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/tools v0.47.0 // indirect google.golang.org/genproto v0.0.0-20240930140551-af27646dc61f // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect diff --git a/go.sum b/go.sum index aa973c13..f8a7db8b 100644 --- a/go.sum +++ b/go.sum @@ -748,8 +748,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250811191247-51f88131bc50 h1:3yiSh9fhy5/RhCSntf4Sy0Tnx50DmMpQ4MQdKKk4yg4= golang.org/x/exp v0.0.0-20250811191247-51f88131bc50/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg= @@ -763,8 +763,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20150829230318-ea47fc708ee3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -790,8 +790,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= -golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= @@ -804,8 +804,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -844,16 +844,16 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -863,8 +863,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -886,8 +886,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 417cb16d970f3464bbdebbb2b782874995e8d220 Mon Sep 17 00:00:00 2001 From: saunter <68239231+stackingsaunter@users.noreply.github.com> Date: Tue, 4 Aug 2026 12:08:45 +0200 Subject: [PATCH 088/136] feat: refresh payment QR and status components (#2459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 --------- Co-authored-by: René Aaron Co-authored-by: Roland Bewick Co-authored-by: Claude Fable 5 --- frontend/package.json | 2 +- .../src/assets/lotties/success-check.json | 8588 +++++++++++++++++ .../src/components/FixedFloatSwapInFlow.tsx | 14 +- frontend/src/components/LottieSuccess.tsx | 21 + .../src/components/PayLightningInvoice.tsx | 28 +- frontend/src/components/QRCode.tsx | 124 +- .../src/components/ReceiveToLightning.tsx | 8 +- frontend/src/components/ReceiveToOnchain.tsx | 27 +- .../src/components/icons/BitcoinPayment.tsx | 25 + frontend/src/components/icons/Lightning.tsx | 2 +- .../screens/channels/CurrentChannelOrder.tsx | 2 +- .../src/screens/internal-apps/BuzzPay.tsx | 21 +- .../src/screens/onchain/DepositBitcoin.tsx | 28 +- .../screens/wallet/receive/ReceiveInvoice.tsx | 25 +- .../screens/wallet/receive/ReceiveOffer.tsx | 12 +- .../screens/wallet/send/OnchainSuccess.tsx | 4 +- .../screens/wallet/send/PaymentSuccess.tsx | 12 +- .../src/screens/wallet/swap/SwapInStatus.tsx | 9 +- frontend/src/themes/base.css | 10 + frontend/src/themes/index.css | 4 + frontend/yarn.lock | 25 +- 21 files changed, 8864 insertions(+), 127 deletions(-) create mode 100644 frontend/src/assets/lotties/success-check.json create mode 100644 frontend/src/components/LottieSuccess.tsx create mode 100644 frontend/src/components/icons/BitcoinPayment.tsx diff --git a/frontend/package.json b/frontend/package.json index 9477c25e..67d187dc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,12 +37,12 @@ "dayjs": "^1.11.20", "embla-carousel-react": "^8.6.0", "lucide-react": "^1.7.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-lottie": "^1.2.4", - "react-qr-code": "^2.0.12", "react-router": "^7.14.2", "sonner": "^2.0.7", "swr": "^2.4.1", diff --git a/frontend/src/assets/lotties/success-check.json b/frontend/src/assets/lotties/success-check.json new file mode 100644 index 00000000..9eee6036 --- /dev/null +++ b/frontend/src/assets/lotties/success-check.json @@ -0,0 +1,8588 @@ +{ + "h": 1000, + "w": 1000, + "meta": { + "a": "Muzahid Ahinger", + "k": "check done ok complete checked", + "d": "Check, done, complete animated signs and symbols", + "g": "@lottiefiles/toolkit-js 0.57.1-beta.0", + "tc": "#ffffff" + }, + "layers": [ + { + "ty": 4, + "sr": 1, + "st": 2, + "op": 71, + "ip": 2, + "ln": "14", + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [0, 0] }, + "s": { + "a": 1, + "k": [ + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [112.5, 112.5, 100], + "t": 2 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 3 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [164.167, 164.167, 100], + "t": 4 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [175.388, 175.388, 100], + "t": 5 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [183.898, 183.898, 100], + "t": 6 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [189.962, 189.962, 100], + "t": 7 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [193.86, 193.86, 100], + "t": 8 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [195.883, 195.883, 100], + "t": 9 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [196.315, 196.315, 100], + "t": 10 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [195.434, 195.434, 100], + "t": 11 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [193.5, 193.5, 100], + "t": 12 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [190.758, 190.758, 100], + "t": 13 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [187.425, 187.425, 100], + "t": 14 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [183.699, 183.699, 100], + "t": 15 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [179.749, 179.749, 100], + "t": 16 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [175.722, 175.722, 100], + "t": 17 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [171.739, 171.739, 100], + "t": 18 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [167.898, 167.898, 100], + "t": 19 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [164.277, 164.277, 100], + "t": 20 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [160.931, 160.931, 100], + "t": 21 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [157.9, 157.9, 100], + "t": 22 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [155.207, 155.207, 100], + "t": 23 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [152.864, 152.864, 100], + "t": 24 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.87, 150.87, 100], + "t": 25 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.213, 149.213, 100], + "t": 26 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [147.878, 147.878, 100], + "t": 27 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [146.842, 146.842, 100], + "t": 28 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [146.077, 146.077, 100], + "t": 29 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [145.556, 145.556, 100], + "t": 30 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [145.248, 145.248, 100], + "t": 31 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [145.122, 145.122, 100], + "t": 32 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [145.149, 145.149, 100], + "t": 33 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [145.301, 145.301, 100], + "t": 34 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [145.55, 145.55, 100], + "t": 35 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [145.873, 145.873, 100], + "t": 36 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [146.248, 146.248, 100], + "t": 37 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [146.654, 146.654, 100], + "t": 38 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [147.077, 147.077, 100], + "t": 39 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [147.5, 147.5, 100], + "t": 40 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [147.914, 147.914, 100], + "t": 41 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [148.308, 148.308, 100], + "t": 42 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [148.676, 148.676, 100], + "t": 43 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.012, 149.012, 100], + "t": 44 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.314, 149.314, 100], + "t": 45 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.579, 149.579, 100], + "t": 46 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.808, 149.808, 100], + "t": 47 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 48 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.157, 150.157, 100], + "t": 49 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.282, 150.282, 100], + "t": 50 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.377, 150.377, 100], + "t": 51 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.444, 150.444, 100], + "t": 52 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.487, 150.487, 100], + "t": 53 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.51, 150.51, 100], + "t": 54 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.515, 150.515, 100], + "t": 55 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.505, 150.505, 100], + "t": 56 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.483, 150.483, 100], + "t": 57 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.453, 150.453, 100], + "t": 58 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.416, 150.416, 100], + "t": 59 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.374, 150.374, 100], + "t": 60 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.33, 150.33, 100], + "t": 61 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.286, 150.286, 100], + "t": 62 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.241, 150.241, 100], + "t": 63 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.199, 150.199, 100], + "t": 64 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.159, 150.159, 100], + "t": 65 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.121, 150.121, 100], + "t": 66 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.088, 150.088, 100], + "t": 67 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.058, 150.058, 100], + "t": 68 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.032, 150.032, 100], + "t": 69 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.01, 150.01, 100], + "t": 70 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.991, 149.991, 100], + "t": 71 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.976, 149.976, 100], + "t": 72 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.965, 149.965, 100], + "t": 73 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.956, 149.956, 100], + "t": 74 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.951, 149.951, 100], + "t": 75 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.947, 149.947, 100], + "t": 76 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.946, 149.946, 100], + "t": 77 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.946, 149.946, 100], + "t": 78 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.948, 149.948, 100], + "t": 79 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.951, 149.951, 100], + "t": 80 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.954, 149.954, 100], + "t": 81 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.958, 149.958, 100], + "t": 82 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.963, 149.963, 100], + "t": 83 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.968, 149.968, 100], + "t": 84 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.972, 149.972, 100], + "t": 85 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.977, 149.977, 100], + "t": 86 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.981, 149.981, 100], + "t": 87 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.985, 149.985, 100], + "t": 88 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.989, 149.989, 100], + "t": 89 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.992, 149.992, 100], + "t": 90 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.995, 149.995, 100], + "t": 91 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.998, 149.998, 100], + "t": 92 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 93 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.002, 150.002, 100], + "t": 94 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.003, 150.003, 100], + "t": 95 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.004, 150.004, 100], + "t": 96 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.005, 150.005, 100], + "t": 97 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.005, 150.005, 100], + "t": 98 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.006, 150.006, 100], + "t": 99 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.006, 150.006, 100], + "t": 100 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.006, 150.006, 100], + "t": 101 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.005, 150.005, 100], + "t": 102 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.005, 150.005, 100], + "t": 103 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.005, 150.005, 100], + "t": 104 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.004, 150.004, 100], + "t": 105 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.004, 150.004, 100], + "t": 106 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.003, 150.003, 100], + "t": 107 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.003, 150.003, 100], + "t": 108 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.002, 150.002, 100], + "t": 109 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.002, 150.002, 100], + "t": 110 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.001, 150.001, 100], + "t": 111 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.001, 150.001, 100], + "t": 112 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.001, 150.001, 100], + "t": 113 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 114 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 115 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 116 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 117 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 118 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 119 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.999, 149.999, 100], + "t": 120 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.999, 149.999, 100], + "t": 121 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.999, 149.999, 100], + "t": 122 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.999, 149.999, 100], + "t": 123 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.999, 149.999, 100], + "t": 124 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.999, 149.999, 100], + "t": 125 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.999, 149.999, 100], + "t": 126 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 127 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 128 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 129 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 130 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 131 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 132 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 133 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 134 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 135 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 136 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 137 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 138 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 139 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 140 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 141 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 142 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 143 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 144 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 145 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 146 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 147 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 148 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 149 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 150 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 151 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 152 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 153 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 154 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 155 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 156 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 157 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 158 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 159 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 160 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 161 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 162 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 163 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 164 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 165 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 166 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 167 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 168 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 169 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 170 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 171 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 172 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 173 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 174 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 175 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 176 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 177 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 178 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 179 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 180 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 181 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 182 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 183 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 184 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 185 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 186 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 187 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 188 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 189 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 190 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 191 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 192 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 193 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 194 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 195 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 196 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 197 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 198 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 199 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 200 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 201 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 202 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 203 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 204 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 205 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 206 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 207 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 208 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 209 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 210 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 211 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 212 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 213 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 214 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 215 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 216 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 217 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 218 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 219 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 220 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 221 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 222 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 223 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 224 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 225 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 226 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 227 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 228 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 229 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 230 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 231 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 232 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 233 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 234 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 235 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 236 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 237 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 238 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 239 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 240 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 241 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 242 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 243 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 244 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 245 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 246 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 247 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 248 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 249 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 250 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 251 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 252 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 253 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 254 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 255 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 256 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 257 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 258 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 259 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 260 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 261 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 262 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 263 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 264 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 265 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 266 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 267 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 268 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 269 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 270 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 271 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 272 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 273 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 274 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 275 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 276 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 277 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 278 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 279 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 280 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 281 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 282 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 283 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 284 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 285 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 286 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 287 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 288 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 289 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 290 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 291 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 292 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 293 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 294 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 295 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 296 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 297 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 298 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 299 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 300 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 301 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 302 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 303 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 304 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 305 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 306 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 307 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 308 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 309 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 310 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 311 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 312 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 313 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 314 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 315 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 316 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 317 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 318 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 319 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 320 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 321 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 322 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 323 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 324 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 325 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 326 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 327 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 328 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 329 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 330 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 331 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 332 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 333 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 334 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 335 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 336 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 337 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 338 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 339 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 340 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 341 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 342 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 343 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 344 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 345 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 346 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 347 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 348 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 349 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 350 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 351 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 352 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 353 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 354 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 355 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 356 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 357 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 358 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 359 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 360 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 361 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 362 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 363 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 364 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 365 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 366 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 367 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 368 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 369 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 370 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 371 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 372 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 373 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 374 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 375 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 376 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 377 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 378 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 379 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 380 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 381 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 382 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 383 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 384 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 385 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 386 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 387 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 388 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 389 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 390 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 391 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 392 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 393 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 394 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 395 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 396 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 397 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 398 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 399 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 400 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 401 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 402 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 403 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 404 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 405 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 406 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 407 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 408 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 409 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 410 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 411 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 412 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 413 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 414 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 415 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 416 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 417 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 418 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 419 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 420 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 421 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 422 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 423 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 424 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 425 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 426 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 427 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 428 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 429 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 430 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 431 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 432 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 433 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 434 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 435 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 436 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 437 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 438 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 439 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 440 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 441 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 442 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 443 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 444 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 445 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 446 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 447 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 448 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 449 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 450 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 451 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 452 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 453 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 454 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 455 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 456 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 457 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 458 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 459 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 460 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 461 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 462 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 463 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 464 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 465 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 466 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 467 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 468 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 469 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 470 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 471 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 472 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 473 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 474 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 475 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 476 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 477 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 478 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 479 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 480 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 481 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 482 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 483 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 484 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 485 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 486 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 487 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 488 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 489 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 490 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 491 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 492 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 493 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 494 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 495 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 496 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 497 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 498 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 499 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 500 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 501 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 502 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 503 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 504 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 505 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 506 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 507 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 508 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 509 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 510 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 511 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 512 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 513 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 514 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 515 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 516 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 517 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 518 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 519 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 520 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 521 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 522 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 523 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 524 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 525 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 526 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 527 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 528 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 529 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 530 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 531 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 532 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 533 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 534 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 535 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 536 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 537 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 538 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 539 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 540 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 541 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 542 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 543 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 544 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 545 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 546 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 547 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 548 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 549 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 550 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 551 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 552 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 553 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 554 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 555 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 556 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 557 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 558 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 559 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 560 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 561 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 562 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 563 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 564 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 565 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 566 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 567 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 568 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 569 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 570 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 571 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 572 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 573 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 574 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 575 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 576 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 577 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 578 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 579 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 580 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 581 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 582 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 583 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 584 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 585 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 586 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 587 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 588 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 589 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 590 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 591 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 592 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 593 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 594 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 595 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 596 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 597 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 598 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 599 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 600 + }, + { "s": [150, 150, 100], "t": 601 } + ] + }, + "p": { "a": 0, "k": [491, 480.676, 0] }, + "r": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100 } + }, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": false, + "i": [ + [0, 0], + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0], + [0, 0] + ], + "v": [ + [-85.202, 28.7], + [-32.001, 83.001], + [90.251, -41.301] + ] + } + } + }, + { + "ty": "tr", + "a": { "a": 0, "k": [0, 0] }, + "s": { "a": 0, "k": [100, 100] }, + "p": { "a": 0, "k": [0, 0] }, + "r": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100 } + } + ] + }, + { + "ty": "tm", + "e": { + "a": 1, + "k": [ + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [0], + "t": 6 + }, + { "s": [100], "t": 16 } + ] + }, + "o": { "a": 0, "k": 0 }, + "s": { "a": 0, "k": 0 }, + "m": 1 + }, + { + "ty": "st", + "lc": 2, + "lj": 2, + "ml": 4, + "o": { "a": 0, "k": 100 }, + "w": { "a": 0, "k": 40 }, + "c": { "a": 0, "k": [1, 1, 1] } + } + ], + "ind": 1 + }, + { + "ty": 4, + "sr": 1, + "st": 1, + "op": 71, + "ip": 0, + "ln": "13", + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [0, 0] }, + "s": { + "a": 1, + "k": [ + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [90, 90, 100], + "t": 0 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [120, 120, 100], + "t": 1 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 2 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [161.334, 161.334, 100], + "t": 3 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [170.311, 170.311, 100], + "t": 4 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [177.119, 177.119, 100], + "t": 5 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [181.969, 181.969, 100], + "t": 6 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [185.088, 185.088, 100], + "t": 7 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [186.706, 186.706, 100], + "t": 8 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [187.052, 187.052, 100], + "t": 9 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [186.347, 186.347, 100], + "t": 10 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [184.8, 184.8, 100], + "t": 11 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [182.606, 182.606, 100], + "t": 12 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [179.94, 179.94, 100], + "t": 13 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [176.959, 176.959, 100], + "t": 14 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [173.799, 173.799, 100], + "t": 15 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [170.578, 170.578, 100], + "t": 16 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [167.391, 167.391, 100], + "t": 17 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [164.319, 164.319, 100], + "t": 18 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [161.421, 161.421, 100], + "t": 19 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [158.744, 158.744, 100], + "t": 20 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [156.32, 156.32, 100], + "t": 21 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [154.166, 154.166, 100], + "t": 22 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [152.291, 152.291, 100], + "t": 23 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.696, 150.696, 100], + "t": 24 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.371, 149.371, 100], + "t": 25 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [148.302, 148.302, 100], + "t": 26 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [147.473, 147.473, 100], + "t": 27 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [146.862, 146.862, 100], + "t": 28 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [146.445, 146.445, 100], + "t": 29 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [146.198, 146.198, 100], + "t": 30 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [146.098, 146.098, 100], + "t": 31 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [146.119, 146.119, 100], + "t": 32 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [146.241, 146.241, 100], + "t": 33 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [146.44, 146.44, 100], + "t": 34 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [146.699, 146.699, 100], + "t": 35 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [146.998, 146.998, 100], + "t": 36 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [147.324, 147.324, 100], + "t": 37 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [147.661, 147.661, 100], + "t": 38 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [148, 148, 100], + "t": 39 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [148.331, 148.331, 100], + "t": 40 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [148.646, 148.646, 100], + "t": 41 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [148.94, 148.94, 100], + "t": 42 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.21, 149.21, 100], + "t": 43 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.451, 149.451, 100], + "t": 44 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.663, 149.663, 100], + "t": 45 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.846, 149.846, 100], + "t": 46 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 47 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.126, 150.126, 100], + "t": 48 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.226, 150.226, 100], + "t": 49 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.301, 150.301, 100], + "t": 50 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.355, 150.355, 100], + "t": 51 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.39, 150.39, 100], + "t": 52 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.408, 150.408, 100], + "t": 53 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.412, 150.412, 100], + "t": 54 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.404, 150.404, 100], + "t": 55 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.387, 150.387, 100], + "t": 56 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.362, 150.362, 100], + "t": 57 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.333, 150.333, 100], + "t": 58 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.299, 150.299, 100], + "t": 59 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.264, 150.264, 100], + "t": 60 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.229, 150.229, 100], + "t": 61 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.193, 150.193, 100], + "t": 62 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.159, 150.159, 100], + "t": 63 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.127, 150.127, 100], + "t": 64 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.097, 150.097, 100], + "t": 65 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.07, 150.07, 100], + "t": 66 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.046, 150.046, 100], + "t": 67 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.025, 150.025, 100], + "t": 68 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.008, 150.008, 100], + "t": 69 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.993, 149.993, 100], + "t": 70 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.981, 149.981, 100], + "t": 71 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.972, 149.972, 100], + "t": 72 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.965, 149.965, 100], + "t": 73 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.961, 149.961, 100], + "t": 74 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.958, 149.958, 100], + "t": 75 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.957, 149.957, 100], + "t": 76 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.957, 149.957, 100], + "t": 77 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.958, 149.958, 100], + "t": 78 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.96, 149.96, 100], + "t": 79 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.963, 149.963, 100], + "t": 80 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.967, 149.967, 100], + "t": 81 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.97, 149.97, 100], + "t": 82 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.974, 149.974, 100], + "t": 83 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.978, 149.978, 100], + "t": 84 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.981, 149.981, 100], + "t": 85 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.985, 149.985, 100], + "t": 86 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.988, 149.988, 100], + "t": 87 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.991, 149.991, 100], + "t": 88 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.994, 149.994, 100], + "t": 89 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.996, 149.996, 100], + "t": 90 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [149.998, 149.998, 100], + "t": 91 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 92 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.001, 150.001, 100], + "t": 93 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.003, 150.003, 100], + "t": 94 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.003, 150.003, 100], + "t": 95 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.004, 150.004, 100], + "t": 96 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.004, 150.004, 100], + "t": 97 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.005, 150.005, 100], + "t": 98 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.005, 150.005, 100], + "t": 99 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.004, 150.004, 100], + "t": 100 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.004, 150.004, 100], + "t": 101 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.004, 150.004, 100], + "t": 102 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.004, 150.004, 100], + "t": 103 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.003, 150.003, 100], + "t": 104 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.003, 150.003, 100], + "t": 105 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.003, 150.003, 100], + "t": 106 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.002, 150.002, 100], + "t": 107 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.002, 150.002, 100], + "t": 108 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.001, 150.001, 100], + "t": 109 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.001, 150.001, 100], + "t": 110 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.001, 150.001, 100], + "t": 111 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150.001, 150.001, 100], + "t": 112 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 113 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 114 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 115 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 116 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 117 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 118 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 119 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 120 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 121 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 122 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 123 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 124 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 125 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 126 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 127 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 128 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 129 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 130 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 131 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 132 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 133 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 134 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 135 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 136 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 137 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 138 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 139 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 140 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 141 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 142 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 143 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 144 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 145 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 146 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 147 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 148 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 149 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 150 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 151 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 152 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 153 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 154 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 155 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 156 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 157 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 158 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 159 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 160 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 161 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 162 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 163 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 164 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 165 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 166 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 167 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 168 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 169 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 170 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 171 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 172 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 173 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 174 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 175 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 176 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 177 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 178 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 179 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 180 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 181 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 182 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 183 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 184 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 185 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 186 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 187 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 188 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 189 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 190 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 191 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 192 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 193 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 194 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 195 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 196 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 197 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 198 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 199 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 200 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 201 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 202 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 203 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 204 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 205 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 206 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 207 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 208 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 209 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 210 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 211 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 212 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 213 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 214 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 215 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 216 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 217 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 218 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 219 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 220 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 221 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 222 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 223 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 224 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 225 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 226 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 227 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 228 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 229 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 230 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 231 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 232 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 233 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 234 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 235 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 236 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 237 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 238 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 239 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 240 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 241 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 242 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 243 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 244 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 245 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 246 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 247 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 248 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 249 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 250 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 251 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 252 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 253 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 254 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 255 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 256 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 257 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 258 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 259 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 260 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 261 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 262 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 263 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 264 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 265 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 266 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 267 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 268 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 269 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 270 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 271 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 272 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 273 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 274 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 275 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 276 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 277 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 278 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 279 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 280 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 281 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 282 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 283 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 284 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 285 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 286 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 287 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 288 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 289 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 290 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 291 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 292 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 293 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 294 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 295 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 296 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 297 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 298 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 299 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 300 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 301 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 302 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 303 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 304 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 305 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 306 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 307 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 308 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 309 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 310 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 311 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 312 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 313 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 314 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 315 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 316 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 317 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 318 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 319 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 320 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 321 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 322 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 323 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 324 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 325 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 326 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 327 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 328 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 329 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 330 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 331 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 332 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 333 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 334 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 335 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 336 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 337 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 338 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 339 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 340 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 341 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 342 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 343 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 344 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 345 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 346 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 347 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 348 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 349 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 350 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 351 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 352 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 353 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 354 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 355 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 356 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 357 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 358 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 359 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 360 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 361 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 362 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 363 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 364 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 365 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 366 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 367 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 368 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 369 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 370 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 371 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 372 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 373 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 374 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 375 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 376 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 377 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 378 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 379 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 380 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 381 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 382 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 383 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 384 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 385 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 386 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 387 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 388 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 389 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 390 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 391 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 392 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 393 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 394 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 395 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 396 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 397 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 398 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 399 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 400 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 401 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 402 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 403 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 404 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 405 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 406 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 407 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 408 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 409 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 410 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 411 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 412 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 413 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 414 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 415 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 416 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 417 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 418 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 419 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 420 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 421 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 422 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 423 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 424 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 425 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 426 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 427 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 428 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 429 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 430 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 431 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 432 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 433 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 434 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 435 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 436 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 437 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 438 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 439 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 440 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 441 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 442 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 443 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 444 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 445 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 446 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 447 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 448 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 449 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 450 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 451 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 452 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 453 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 454 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 455 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 456 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 457 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 458 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 459 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 460 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 461 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 462 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 463 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 464 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 465 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 466 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 467 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 468 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 469 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 470 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 471 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 472 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 473 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 474 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 475 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 476 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 477 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 478 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 479 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 480 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 481 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 482 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 483 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 484 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 485 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 486 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 487 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 488 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 489 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 490 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 491 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 492 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 493 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 494 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 495 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 496 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 497 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 498 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 499 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 500 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 501 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 502 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 503 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 504 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 505 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 506 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 507 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 508 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 509 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 510 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 511 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 512 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 513 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 514 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 515 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 516 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 517 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 518 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 519 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 520 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 521 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 522 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 523 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 524 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 525 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 526 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 527 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 528 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 529 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 530 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 531 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 532 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 533 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 534 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 535 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 536 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 537 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 538 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 539 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 540 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 541 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 542 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 543 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 544 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 545 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 546 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 547 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 548 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 549 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 550 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 551 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 552 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 553 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 554 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 555 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 556 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 557 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 558 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 559 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 560 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 561 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 562 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 563 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 564 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 565 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 566 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 567 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 568 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 569 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 570 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 571 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 572 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 573 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 574 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 575 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 576 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 577 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 578 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 579 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 580 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 581 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 582 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 583 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 584 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 585 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 586 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 587 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 588 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 589 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 590 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 591 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 592 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 593 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 594 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 595 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 596 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 597 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 598 + }, + { + "h": 1, + "o": { "x": 0.167, "y": 0.167 }, + "i": { "x": 0.833, "y": 0.833 }, + "s": [150, 150, 100], + "t": 599 + }, + { "s": [150, 150, 100], "t": 600 } + ] + }, + "p": { "a": 0, "k": [500, 500] }, + "r": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100 } + }, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "i": [ + [-110.457, 0], + [0, -110.457], + [110.457, 0], + [0, 110.457] + ], + "o": [ + [110.457, 0], + [0, 110.457], + [-110.457, 0], + [0, -110.457] + ], + "v": [ + [0, -200], + [200, 0], + [0, 200], + [-200, 0] + ] + } + } + }, + { + "ty": "st", + "lc": 1, + "lj": 1, + "ml": 4, + "o": { "a": 0, "k": 100 }, + "w": { "a": 0, "k": 0 }, + "c": { "a": 0, "k": [0.596, 0.875, 0.839] } + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.296, 0.698, 0] }, + "r": 1, + "o": { "a": 0, "k": 0 } + }, + { + "ty": "tr", + "a": { "a": 0, "k": [0, 0] }, + "s": { "a": 0, "k": [100, 100] }, + "p": { "a": 0, "k": [0, 0] }, + "r": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100 } + } + ] + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.296, 0.698, 0] }, + "r": 1, + "o": { "a": 0, "k": 100 } + } + ], + "ind": 2 + } + ], + "v": "5.7.0", + "fr": 60, + "op": 71, + "ip": 0, + "assets": [] +} diff --git a/frontend/src/components/FixedFloatSwapInFlow.tsx b/frontend/src/components/FixedFloatSwapInFlow.tsx index 04502a83..ff4bbd48 100644 --- a/frontend/src/components/FixedFloatSwapInFlow.tsx +++ b/frontend/src/components/FixedFloatSwapInFlow.tsx @@ -4,12 +4,12 @@ import { ExternalLinkIcon, HandCoinsIcon, } from "lucide-react"; -import TickSVG from "public/images/illustrations/tick.svg"; import { useEffect, useState } from "react"; import { FixedFloatButton } from "src/components/FixedFloatButton"; import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; import FormattedFiatAmount from "src/components/FormattedFiatAmount"; import LottieLoading from "src/components/LottieLoading"; +import LottieSuccess from "src/components/LottieSuccess"; import { Button } from "src/components/ui/button"; import { Card, @@ -77,7 +77,7 @@ export function FixedFloatSwapInFlow({ return ( - Waiting for Payment + Waiting for Payment... @@ -91,7 +91,7 @@ export function FixedFloatSwapInFlow({ />

  • - + - + Back to Wallet diff --git a/frontend/src/components/LottieSuccess.tsx b/frontend/src/components/LottieSuccess.tsx new file mode 100644 index 00000000..cdda9281 --- /dev/null +++ b/frontend/src/components/LottieSuccess.tsx @@ -0,0 +1,21 @@ +import { useMemo } from "react"; +import Lottie from "react-lottie"; +import animationData from "src/assets/lotties/success-check.json"; + +export default function LottieSuccess({ size = 288 }: { size?: number }) { + const options = useMemo( + () => ({ + loop: false, + autoplay: true, + animationData, + rendererSettings: { preserveAspectRatio: "xMidYMid meet" }, + }), + [] + ); + + return ( +
    + +
    + ); +} diff --git a/frontend/src/components/PayLightningInvoice.tsx b/frontend/src/components/PayLightningInvoice.tsx index 374f4b6a..b9227e95 100644 --- a/frontend/src/components/PayLightningInvoice.tsx +++ b/frontend/src/components/PayLightningInvoice.tsx @@ -3,7 +3,6 @@ import { CopyIcon, ExternalLinkIcon } from "lucide-react"; import React from "react"; import { FixedFloatButton } from "src/components/FixedFloatButton"; import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; -import { LightningIcon } from "src/components/icons/Lightning"; import Loading from "src/components/Loading"; import QRCode from "src/components/QRCode"; import { Button } from "src/components/ui/button"; @@ -28,16 +27,13 @@ export function PayLightningInvoice({ invoice }: PayLightningInvoiceProps) { }; return ( -
    -
    +
    +
    -

    Waiting for lightning payment...

    +

    Waiting for Payment...

    -
    - -
    - -
    +
    +

    @@ -50,23 +46,19 @@ export function PayLightningInvoice({ invoice }: PayLightningInvoiceProps) { }).format(fiatAmount)}

    -
    - - Pay with other Cryptocurrency + Pay with Crypto
    diff --git a/frontend/src/components/QRCode.tsx b/frontend/src/components/QRCode.tsx index 6bc2af83..eb97b2cd 100644 --- a/frontend/src/components/QRCode.tsx +++ b/frontend/src/components/QRCode.tsx @@ -1,38 +1,116 @@ -import ReactQRCode from "react-qr-code"; +import { type ReactNode, useEffect, useMemo, useRef } from "react"; +import QRCodeStyling, { type Options } from "qr-code-styling"; +import { BitcoinPaymentIcon } from "src/components/icons/BitcoinPayment"; +import { LightningIcon } from "src/components/icons/Lightning"; import { cn } from "src/lib/utils"; export type Props = { value: string; size?: number; className?: string; + showAvatar?: boolean; + frameType?: "lightning" | "onchain"; + paymentType?: "lightning" | "onchain"; + centerContent?: ReactNode; - // set the level to Q if there are overlays - // Q will improve error correction (so we can add overlays covering up to 25% of the QR) - // at the price of decreased information density (meaning the QR codes "pixels" have to be - // smaller to encode the same information). - // While that isn't that much of a problem for lightning addresses (because they are usually quite short), - // for invoices that contain larger amount of data those QR codes can get "harder" to read. - // (meaning you have to aim your phone very precisely and have to wait longer for the reader - // to recognize the QR code) + // Use Q when an external overlay covers part of the QR code. level?: "Q" | undefined; }; -function QRCode({ value, size, level, className }: Props) { - // Do not use dark mode: some apps do not handle it well (e.g. Phoenix) - // const { isDarkMode } = useTheme(); - const fgColor = "#242424"; // isDarkMode ? "#FFFFFF" : "#242424"; - const bgColor = "#FFFFFF"; // isDarkMode ? "#242424" : "#FFFFFF"; +function QRCode({ + value, + size = 256, + level, + className, + showAvatar = false, + frameType, + paymentType, + centerContent, +}: Props) { + const resolvedFrameType = paymentType ?? frameType; + const hasCenterContent = Boolean(centerContent); + const containerRef = useRef(null); + const options = useMemo( + () => ({ + type: "svg", + width: size, + height: size, + data: value, + image: showAvatar ? "/icon-lightmode.svg" : undefined, + margin: 0, + qrOptions: { + errorCorrectionLevel: + level ?? (showAvatar || paymentType || hasCenterContent ? "Q" : "M"), + }, + imageOptions: { + crossOrigin: "anonymous", + hideBackgroundDots: true, + imageSize: 0.15, + margin: 4, + }, + dotsOptions: { + color: "var(--qr-foreground)", + type: "dots", + roundSize: false, + }, + cornersSquareOptions: { + color: "var(--qr-foreground)", + type: "extra-rounded", + }, + cornersDotOptions: { + color: "var(--qr-foreground)", + type: "dot", + }, + backgroundOptions: { + color: "var(--qr-background)", + }, + }), + [hasCenterContent, level, paymentType, showAvatar, size, value] + ); + + useEffect(() => { + const container = containerRef.current; + if (!container) { + return; + } + + const qrCode = new QRCodeStyling(options); + qrCode.append(container); + + return () => { + container.replaceChildren(); + }; + }, [options]); return ( -
    - +
    +
    +
    +
    + {(paymentType || centerContent) && ( +
    + {centerContent ?? + (paymentType === "lightning" ? ( + + ) : ( + + ))} +
    + )}
    ); } diff --git a/frontend/src/components/ReceiveToLightning.tsx b/frontend/src/components/ReceiveToLightning.tsx index 892e94a3..f85ea3ca 100644 --- a/frontend/src/components/ReceiveToLightning.tsx +++ b/frontend/src/components/ReceiveToLightning.tsx @@ -9,6 +9,7 @@ import { Link } from "react-router"; import FirstChannelJitAlert from "src/components/FirstChannelJitAlert"; import Loading from "src/components/Loading"; import QRCode from "src/components/QRCode"; +import UserAvatar from "src/components/UserAvatar"; import { Accordion, AccordionContent, @@ -34,7 +35,12 @@ export function ReceiveToLightning() { - + } + />

    {me.lightning_address} diff --git a/frontend/src/components/ReceiveToOnchain.tsx b/frontend/src/components/ReceiveToOnchain.tsx index 523ab57f..4b852946 100644 --- a/frontend/src/components/ReceiveToOnchain.tsx +++ b/frontend/src/components/ReceiveToOnchain.tsx @@ -5,13 +5,13 @@ import { HandCoinsIcon, RefreshCwIcon, } from "lucide-react"; -import TickSVG from "public/images/illustrations/tick.svg"; import { useEffect, useRef, useState } from "react"; import { FixedFloatButton } from "src/components/FixedFloatButton"; import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; import FormattedFiatAmount from "src/components/FormattedFiatAmount"; import Loading from "src/components/Loading"; import LottieLoading from "src/components/LottieLoading"; +import LottieSuccess from "src/components/LottieSuccess"; import OnchainAddressDisplay from "src/components/OnchainAddressDisplay"; import QRCode from "src/components/QRCode"; import { Button } from "src/components/ui/button"; @@ -120,13 +120,13 @@ export function ReceiveToOnchain() { target="_blank" className="flex justify-center" > - +

    - +
    )} - + - + View on Mempool @@ -221,7 +220,7 @@ function DepositSuccess({ Transaction Received! - +

    @@ -229,13 +228,13 @@ function DepositSuccess({

    - + - + View on Mempool - + Back to Wallet diff --git a/frontend/src/components/icons/BitcoinPayment.tsx b/frontend/src/components/icons/BitcoinPayment.tsx new file mode 100644 index 00000000..8dd49706 --- /dev/null +++ b/frontend/src/components/icons/BitcoinPayment.tsx @@ -0,0 +1,25 @@ +import { SVGAttributes } from "react"; + +export function BitcoinPaymentIcon(props: SVGAttributes) { + return ( + + + + + ); +} diff --git a/frontend/src/components/icons/Lightning.tsx b/frontend/src/components/icons/Lightning.tsx index 2977ae3e..9b19471d 100644 --- a/frontend/src/components/icons/Lightning.tsx +++ b/frontend/src/components/icons/Lightning.tsx @@ -29,7 +29,7 @@ export function LightningIcon(props: SVGAttributes) { /> diff --git a/frontend/src/screens/channels/CurrentChannelOrder.tsx b/frontend/src/screens/channels/CurrentChannelOrder.tsx index fbffb3f7..33f10caf 100644 --- a/frontend/src/screens/channels/CurrentChannelOrder.tsx +++ b/frontend/src/screens/channels/CurrentChannelOrder.tsx @@ -335,7 +335,7 @@ function PayBitcoinChannelOrderTopup({ order }: { order: NewChannelOrder }) {
    diff --git a/frontend/src/screens/internal-apps/BuzzPay.tsx b/frontend/src/screens/internal-apps/BuzzPay.tsx index 70591100..8168fe04 100644 --- a/frontend/src/screens/internal-apps/BuzzPay.tsx +++ b/frontend/src/screens/internal-apps/BuzzPay.tsx @@ -1,7 +1,6 @@ import { AlertTriangleIcon, CopyIcon, ExternalLinkIcon } from "lucide-react"; import React from "react"; import { toast } from "sonner"; -import buzzpay from "src/assets/suggested-apps/buzzpay.png"; import { AppDetailConnectedApps } from "src/components/connections/AppDetailConnectedApps"; import { AppStoreDetailHeader } from "src/components/connections/AppStoreDetailHeader"; import { appStoreApps } from "src/components/connections/SuggestedAppData"; @@ -82,20 +81,24 @@ export function BuzzPay() { -
    +
    -
    -
    +
    - - diff --git a/frontend/src/screens/onchain/DepositBitcoin.tsx b/frontend/src/screens/onchain/DepositBitcoin.tsx index 75b95405..dfc836ce 100644 --- a/frontend/src/screens/onchain/DepositBitcoin.tsx +++ b/frontend/src/screens/onchain/DepositBitcoin.tsx @@ -1,5 +1,4 @@ import { - CircleCheckIcon, CopyIcon, CreditCardIcon, ExternalLinkIcon, @@ -12,6 +11,7 @@ import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; import FormattedFiatAmount from "src/components/FormattedFiatAmount"; import Loading from "src/components/Loading"; import LottieLoading from "src/components/LottieLoading"; +import LottieSuccess from "src/components/LottieSuccess"; import { MempoolAlert } from "src/components/MempoolAlert"; import OnchainAddressDisplay from "src/components/OnchainAddressDisplay"; import QRCode from "src/components/QRCode"; @@ -129,7 +129,7 @@ export default function DepositBitcoin() { target="_blank" className="flex justify-center" > - +
    @@ -137,11 +137,11 @@ export default function DepositBitcoin() {
    -
    +
    {!loadingAddress && } @@ -149,7 +149,7 @@ export default function DepositBitcoin() {
    @@ -201,14 +201,14 @@ function DepositPending({
    )} -
    +
    View on Mempool - +
    @@ -232,21 +232,21 @@ function DepositSuccess({ Payment Received! - +

    -
    +
    View on Mempool - +
    diff --git a/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx b/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx index b3826b64..b5015d86 100644 --- a/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx +++ b/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx @@ -6,7 +6,6 @@ import { PlusIcon, ReceiptTextIcon, } from "lucide-react"; -import TickSVG from "public/images/illustrations/tick.svg"; import React from "react"; import { Link } from "react-router"; import { toast } from "sonner"; @@ -16,6 +15,7 @@ import ExternalLink from "src/components/ExternalLink"; import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; import FormattedFiatAmount from "src/components/FormattedFiatAmount"; import Loading from "src/components/Loading"; +import LottieSuccess from "src/components/LottieSuccess"; import LowReceivingCapacityAlert from "src/components/LowReceivingCapacityAlert"; import QRCode from "src/components/QRCode"; import { @@ -198,13 +198,16 @@ export default function ReceiveInvoice() { {!paymentDone ? ( <> - - -

    Waiting for payment

    + + +

    Waiting for Payment...

    - +

    {newChannelFeeAlert}

    )}
    - + @@ -239,7 +242,7 @@ export default function ReceiveInvoice() { - +

    - + - + Back to Wallet diff --git a/frontend/src/screens/wallet/receive/ReceiveOffer.tsx b/frontend/src/screens/wallet/receive/ReceiveOffer.tsx index 59fd08c2..8685fe65 100644 --- a/frontend/src/screens/wallet/receive/ReceiveOffer.tsx +++ b/frontend/src/screens/wallet/receive/ReceiveOffer.tsx @@ -91,14 +91,18 @@ export default function ReceiveOffer() { Lightning Offer - + {description && (

    {description}

    )}
    - + diff --git a/frontend/src/screens/wallet/send/OnchainSuccess.tsx b/frontend/src/screens/wallet/send/OnchainSuccess.tsx index 043cb29c..032044dd 100644 --- a/frontend/src/screens/wallet/send/OnchainSuccess.tsx +++ b/frontend/src/screens/wallet/send/OnchainSuccess.tsx @@ -10,10 +10,10 @@ import { CardTitle, } from "src/components/ui/card"; -import TickSVG from "public/images/illustrations/tick.svg"; import AppHeader from "src/components/AppHeader"; import Loading from "src/components/Loading"; import LottieLoading from "src/components/LottieLoading"; +import LottieSuccess from "src/components/LottieSuccess"; import { ExternalLinkButton } from "src/components/ui/custom/external-link-button"; import { LinkButton } from "src/components/ui/custom/link-button"; import { useInfo } from "src/hooks/useInfo"; @@ -50,7 +50,7 @@ export default function OnchainSuccess() { {mempoolTx?.status.confirmed ? ( - + ) : ( )} diff --git a/frontend/src/screens/wallet/send/PaymentSuccess.tsx b/frontend/src/screens/wallet/send/PaymentSuccess.tsx index c0560c2b..324bf21e 100644 --- a/frontend/src/screens/wallet/send/PaymentSuccess.tsx +++ b/frontend/src/screens/wallet/send/PaymentSuccess.tsx @@ -20,8 +20,8 @@ import { } from "src/components/ui/card"; import { copyToClipboard } from "src/lib/clipboard"; -import TickSVG from "public/images/illustrations/tick.svg"; import AppHeader from "src/components/AppHeader"; +import LottieSuccess from "src/components/LottieSuccess"; import { LinkButton } from "src/components/ui/custom/link-button"; export default function PaymentSuccess() { @@ -56,7 +56,7 @@ export default function PaymentSuccess() { Payment Successful - +

    )} - + - + Make Another Payment - + Back to Wallet diff --git a/frontend/src/screens/wallet/swap/SwapInStatus.tsx b/frontend/src/screens/wallet/swap/SwapInStatus.tsx index dbb302b1..12ea32bf 100644 --- a/frontend/src/screens/wallet/swap/SwapInStatus.tsx +++ b/frontend/src/screens/wallet/swap/SwapInStatus.tsx @@ -214,9 +214,13 @@ export default function SwapInStatus() {

    {!swap.lockupTxId && !isInternalSwap && ( -
    +
    {swap.state !== "FAILED" && ( - @@ -225,6 +229,7 @@ export default function SwapInStatus() { Open in External Wallet diff --git a/frontend/src/themes/base.css b/frontend/src/themes/base.css index 1be566ee..04e76d19 100644 --- a/frontend/src/themes/base.css +++ b/frontend/src/themes/base.css @@ -50,6 +50,10 @@ --positive-foreground: oklch(0.623 0.169 149.178); --warning: oklch(0.976 0.016 73.092); --warning-foreground: oklch(0.645 0.192 41.712); + --payment-lightning: #ffdf6f; + --payment-onchain: #fb923c; + --qr-foreground: #242424; + --qr-background: #ffffff; } .dark, @@ -96,4 +100,10 @@ --positive-foreground: oklch(0.859 0.14 152.167); --warning: oklch(0.318 0.035 71.647); --warning-foreground: oklch(0.848 0.117 72.544); + --payment-lightning: #ffdf6f; + --payment-onchain: #fb923c; + /* QR codes stay dark-on-light even in dark mode: many scanner apps + (e.g. Phoenix) cannot read inverted QR codes */ + --qr-foreground: #242424; + --qr-background: #ffffff; } diff --git a/frontend/src/themes/index.css b/frontend/src/themes/index.css index fefa3f55..b6b90012 100644 --- a/frontend/src/themes/index.css +++ b/frontend/src/themes/index.css @@ -54,6 +54,10 @@ --color-warning: var(--warning); --color-warning-foreground: var(--warning-foreground); + --color-payment-lightning: var(--payment-lightning); + --color-payment-onchain: var(--payment-onchain); + --color-qr-foreground: var(--qr-foreground); + --color-qr-background: var(--qr-background); --shadow-sm: none; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index c65dadca..098b7787 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -5069,7 +5069,7 @@ pretty-bytes@^6.1.1: resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.1.1.tgz#38cd6bb46f47afbf667c202cfc754bffd2016a3b" integrity sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ== -prop-types@^15.6.1, prop-types@^15.8.1: +prop-types@^15.6.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -5091,10 +5091,17 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -qr.js@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f" - integrity sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ== +qr-code-styling@^1.9.2: + version "1.9.2" + resolved "https://registry.yarnpkg.com/qr-code-styling/-/qr-code-styling-1.9.2.tgz#071714860a7e59829e8822c9575e989042139d2d" + integrity sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg== + dependencies: + qrcode-generator "^1.4.4" + +qrcode-generator@^1.4.4: + version "1.5.2" + resolved "https://registry.yarnpkg.com/qrcode-generator/-/qrcode-generator-1.5.2.tgz#43faea8061a60d2b4ca5e9b0c0c0fb99e2d93e3a" + integrity sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw== queue-microtask@^1.2.2: version "1.2.3" @@ -5193,14 +5200,6 @@ react-lottie@^1.2.4: lottie-web "^5.12.2" prop-types "^15.6.1" -react-qr-code@^2.0.12: - version "2.0.18" - resolved "https://registry.yarnpkg.com/react-qr-code/-/react-qr-code-2.0.18.tgz#237de8fbab537885d6b2b10f4fd5318b371e3b17" - integrity sha512-v1Jqz7urLMhkO6jkgJuBYhnqvXagzceg3qJUWayuCK/c6LTIonpWbwxR1f1APGd4xrW/QcQEovNrAojbUz65Tg== - dependencies: - prop-types "^15.8.1" - qr.js "0.0.0" - react-remove-scroll-bar@^2.3.7: version "2.3.8" resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223" From 816e0dda6e64a80c2a1e26d4b58ad96a97ee127a Mon Sep 17 00:00:00 2001 From: Alchemist Date: Tue, 4 Aug 2026 11:18:22 +0100 Subject: [PATCH 089/136] feat: receive invoices to apps (#2466) * feat: receive invoices to apps * fix: handle cleared receive selector --- api/models.go | 3 +- api/transactions.go | 9 +- api/transactions_test.go | 62 ++++++++ frontend/src/constants.ts | 1 + .../screens/wallet/receive/ReceiveInvoice.tsx | 4 + .../wallet/receive/ReceiveToSelect.tsx | 138 ++++++++++++++++++ frontend/src/types.ts | 1 + http/http_service.go | 2 +- wails/wails_handlers.go | 2 +- 9 files changed, 217 insertions(+), 5 deletions(-) create mode 100644 api/transactions_test.go create mode 100644 frontend/src/screens/wallet/receive/ReceiveToSelect.tsx diff --git a/api/models.go b/api/models.go index 5448465d..b39abff1 100644 --- a/api/models.go +++ b/api/models.go @@ -46,7 +46,7 @@ type API interface { ListTransactions(ctx context.Context, appId *uint, limit uint64, offset uint64) (*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) (*MakeInvoiceResponse, 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) @@ -586,6 +586,7 @@ type MakeInvoiceRequest struct { AmountSat *uint64 `json:"amountSat"` AmountMsat *uint64 `json:"amountMsat"` Description string `json:"description"` + ToAppID *uint `json:"toAppId"` } type ResetRouterRequest struct { diff --git a/api/transactions.go b/api/transactions.go index b1a530e9..2e4dc18b 100644 --- a/api/transactions.go +++ b/api/transactions.go @@ -12,12 +12,17 @@ import ( "github.com/sirupsen/logrus" ) -func (api *api) CreateInvoice(ctx context.Context, amountMsat uint64, description string) (*MakeInvoiceResponse, error) { +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, amountMsat, description, "", 0, nil, lnClient, 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 } diff --git a/api/transactions_test.go b/api/transactions_test.go new file mode 100644 index 00000000..9db68655 --- /dev/null +++ b/api/transactions_test.go @@ -0,0 +1,62 @@ +package api + +import ( + "context" + "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()) +} diff --git a/frontend/src/constants.ts b/frontend/src/constants.ts index b21bf26e..dac344d3 100644 --- a/frontend/src/constants.ts +++ b/frontend/src/constants.ts @@ -16,6 +16,7 @@ export const ALBY_MIN_HOSTED_BALANCE_FOR_FIRST_CHANNEL = 10_000; export const LIST_TRANSACTIONS_LIMIT = 20; export const LIST_APPS_LIMIT = 20; export const MAX_FREE_SUBWALLETS = 3; +export const APP_SELECT_APPS_LIMIT = 100; export const PAY_FROM_SELECT_APPS_LIMIT = 100; export const SUPPORT_ALBY_CONNECTION_NAME = `ZapPlanner - Alby Hub`; diff --git a/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx b/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx index b5015d86..f7577696 100644 --- a/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx +++ b/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx @@ -46,6 +46,7 @@ import { useInfo } from "src/hooks/useInfo"; import { useTransaction } from "src/hooks/useTransaction"; import { copyToClipboard } from "src/lib/clipboard"; import { cn } from "src/lib/utils"; +import ReceiveToSelect from "src/screens/wallet/receive/ReceiveToSelect"; import { CreateInvoiceRequest, Transaction } from "src/types"; import { request } from "src/utils/request"; @@ -60,6 +61,7 @@ export default function ReceiveInvoice() { React.useState(false); const [amountSat, setAmountSat] = React.useState(""); const [description, setDescription] = React.useState(""); + const [toAppId, setToAppId] = React.useState(); const [transaction, setTransaction] = React.useState( null ); @@ -125,6 +127,7 @@ export default function ReceiveInvoice() { body: JSON.stringify({ amountMsat: (parseInt(amountSat) || 0) * 1000, description, + toAppId, } as CreateInvoiceRequest), }); @@ -337,6 +340,7 @@ export default function ReceiveInvoice() { }} />
    + +
    + +
    +
    {LIGHTNING_BALANCE_LABEL}
    +
    + ); +} + +function AppOption({ app }: { app: App }) { + return ( +
    + +
    +
    {getAppDisplayName(app.name)}
    +
    +
    + ); +} + +export default function ReceiveToSelect({ appId, onChange }: Props) { + const anchorRef = useComboboxAnchor(); + const [search, setSearch] = React.useState(""); + const { data: appsData } = useApps(APP_SELECT_APPS_LIMIT, undefined, { + name: search, + }); + + const apps = React.useMemo( + () => + [...(appsData?.apps || [])].sort((a, b) => + getAppDisplayName(a.name).localeCompare(getAppDisplayName(b.name)) + ), + [appsData?.apps] + ); + + const options = React.useMemo( + () => [ + { value: LIGHTNING_BALANCE, label: LIGHTNING_BALANCE_LABEL }, + ...apps.map((app) => ({ + value: app.id.toString(), + label: getAppDisplayName(app.name), + app, + })), + ], + [apps] + ); + + const selectedOption = options.find((opt) => + appId ? opt.value === appId.toString() : undefined + ); + + return ( +
    + + option.value} + onInputValueChange={setSearch} + onValueChange={(option) => + onChange( + !option || option.value === LIGHTNING_BALANCE + ? undefined + : Number(option.value) + ) + } + > +
    + + + {selectedOption?.app ? ( + + ) : ( +
    + +
    + )} +
    +
    +
    + + No connections found. + + {(option: ReceiveToOption) => ( + + {option.app ? ( + + ) : ( + + )} + + )} + + +
    +
    + ); +} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index d0586790..2e67d98f 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -378,6 +378,7 @@ export type CreateInvoiceRequest = { amountSat?: number; amountMsat?: number; description: string; + toAppId?: number; }; export type PayInvoiceRequest = { diff --git a/http/http_service.go b/http/http_service.go index 9f270c24..b37dee76 100644 --- a/http/http_service.go +++ b/http/http_service.go @@ -687,7 +687,7 @@ func (httpSvc *HttpService) makeInvoiceHandler(c echo.Context) error { amountMsat = *resolvedAmountMsat } - invoice, err := httpSvc.api.CreateInvoice(c.Request().Context(), amountMsat, makeInvoiceRequest.Description) + invoice, err := httpSvc.api.CreateInvoice(c.Request().Context(), amountMsat, makeInvoiceRequest.Description, makeInvoiceRequest.ToAppID) if err != nil { return c.JSON(http.StatusInternalServerError, ErrorResponse{ diff --git a/wails/wails_handlers.go b/wails/wails_handlers.go index 193f3a53..fc3897fe 100644 --- a/wails/wails_handlers.go +++ b/wails/wails_handlers.go @@ -600,7 +600,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string if resolvedAmountMsat != nil { amountMsat = *resolvedAmountMsat } - invoice, err := app.api.CreateInvoice(ctx, amountMsat, makeInvoiceRequest.Description) + invoice, err := app.api.CreateInvoice(ctx, amountMsat, makeInvoiceRequest.Description, makeInvoiceRequest.ToAppID) if err != nil { return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } From ec2ec911be9ef30a51ea58a9b3cc4712ee3af056 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:19:59 +0700 Subject: [PATCH 090/136] 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package.json | 2 +- frontend/yarn.lock | 53 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 67d187dc..dda6eaaf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -53,7 +53,7 @@ }, "devDependencies": { "@commitlint/cli": "^20.5.3", - "@commitlint/config-conventional": "^20.5.0", + "@commitlint/config-conventional": "^21.2.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^10.0.1", "@tailwindcss/aspect-ratio": "^0.4.2", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 098b7787..36109ac5 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -933,13 +933,13 @@ tinyexec "^1.0.0" yargs "^17.0.0" -"@commitlint/config-conventional@^20.5.0": - version "20.5.0" - resolved "https://registry.yarnpkg.com/@commitlint/config-conventional/-/config-conventional-20.5.0.tgz#7a9e971b4d54dd3dd22114baf9a23c99515b7957" - integrity sha512-t3Ni88rFw1XMa4nZHgOKJ8fIAT9M2j5TnKyTqJzsxea7FUetlNdYFus9dz+MhIRZmc16P0PPyEfh6X2d/qw8SA== +"@commitlint/config-conventional@^21.2.0": + version "21.2.0" + resolved "https://registry.yarnpkg.com/@commitlint/config-conventional/-/config-conventional-21.2.0.tgz#9ce2b1f5a24e94883ed1e4c5530fb1b8c1d531eb" + integrity sha512-Qf8WRDVcyVd14if6VTWenebxFbKnVnbzPUJjlzjkyJGeHK2xCGd63Dr1XZzj0plXKQb9P0BfOxoc1HVeCo2BWQ== dependencies: - "@commitlint/types" "^20.5.0" - conventional-changelog-conventionalcommits "^9.2.0" + "@commitlint/types" "^21.2.0" + conventional-changelog-conventionalcommits "^10.0.0" "@commitlint/config-validator@^20.5.0": version "20.5.0" @@ -1070,6 +1070,14 @@ conventional-commits-parser "^6.3.0" picocolors "^1.1.1" +"@commitlint/types@^21.2.0": + version "21.2.0" + resolved "https://registry.yarnpkg.com/@commitlint/types/-/types-21.2.0.tgz#da93897b416f788323f639cce6045310aeb03096" + integrity sha512-7zVFCDB2reMvJH5dmbKnOQPjZEvjdJTH8jc0U/PIPU1r3/+vf5pD1HlfitV2MWsWXrvu7u39iY1lyLUPOaN0Gw== + dependencies: + conventional-commits-parser "^7.0.0" + picocolors "^1.1.1" + "@conventional-changelog/git-client@^2.6.0": version "2.6.0" resolved "https://registry.yarnpkg.com/@conventional-changelog/git-client/-/git-client-2.6.0.tgz#1c7a13681426a7bc4298d24c92cda3a6d6fba544" @@ -1079,6 +1087,11 @@ "@simple-libs/stream-utils" "^1.2.0" semver "^7.5.2" +"@conventional-changelog/template@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@conventional-changelog/template/-/template-1.2.1.tgz#8f673635d6ec8289bc5d5a1264629fd1e88b8d4f" + integrity sha512-TzlTVpKPjaqW6qOYjQcYUDuGsLCNsvFHVBXkYGTAnf5V37jCWrE5haKNXzz0WZUtVHjrpV76L1buANjwXMfT8w== + "@date-fns/tz@^1.4.1": version "1.4.1" resolved "https://registry.yarnpkg.com/@date-fns/tz/-/tz-1.4.1.tgz#2d905f282304630e07bef6d02d2e7dbf3f0cc4e4" @@ -2477,6 +2490,11 @@ resolved "https://registry.yarnpkg.com/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz#5af724b826f1ab4d7f2826d31d3efccec124102b" integrity sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA== +"@simple-libs/stream-utils@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@simple-libs/stream-utils/-/stream-utils-2.0.0.tgz#758d2a0876b4d672dac1eae212cdbab452670d3c" + integrity sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ== + "@stepperize/core@2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@stepperize/core/-/core-2.1.0.tgz#7d28d8d35780b065458214ba8f4c1d7424fc8f42" @@ -2974,6 +2992,11 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +argue-cli@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/argue-cli/-/argue-cli-3.1.0.tgz#5ab702af716dce3e30a95919bd1ac314e2d903f0" + integrity sha512-DhBpBfXL4SS2uC0N922MMajKR3CdrTG0u2or1PNYgXMsrSzViJrbtvT0nCLlLGUI0plam/ZZCs7aAauHtW9thw== + aria-hidden@^1.2.4: version "1.2.6" resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.6.tgz#73051c9b088114c795b1ea414e9c0fff874ffc1a" @@ -3273,12 +3296,12 @@ conventional-changelog-angular@^8.2.0: dependencies: compare-func "^2.0.0" -conventional-changelog-conventionalcommits@^9.2.0: - version "9.3.1" - resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-9.3.1.tgz#14f2dd65ccc5de09322a7eb0159f3e0259d7399c" - integrity sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw== +conventional-changelog-conventionalcommits@^10.0.0: + version "10.2.1" + resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-10.2.1.tgz#dfa7cd9b3c6762bc65b2e7c97636097a3b426217" + integrity sha512-n4Kr1HFMTf3iMbES0TMxKIcYtUUv4rKqyQQp2JwfOEfFCOfGT3Tq4mCyJ8S9/YPyWhydjfKrrvnyl+gCjA+mJQ== dependencies: - compare-func "^2.0.0" + "@conventional-changelog/template" "^1.2.1" conventional-commits-parser@^6.3.0: version "6.3.0" @@ -3288,6 +3311,14 @@ conventional-commits-parser@^6.3.0: "@simple-libs/stream-utils" "^1.2.0" meow "^13.0.0" +conventional-commits-parser@^7.0.0: + version "7.1.2" + resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-7.1.2.tgz#4dca0779b338bab96fe994e864dfedf3c1908b03" + integrity sha512-O+x4N2yH+ijvqWlIyTHsXTAP+algNWgGbjY2duCe8w2vUMvUB95cLRslCPfTMQyLAKlet3bhZTdu6ozn4M+QJQ== + dependencies: + "@simple-libs/stream-utils" "^2.0.0" + argue-cli "^3.1.0" + convert-source-map@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" From 8f9d6f73f33546b1d786e1ae25c757744468ce97 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:21:05 +0700 Subject: [PATCH 091/136] 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 7b64611b..4dc0000a 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/getAlby/ldk-node-go v0.0.0-20260608130949-5ba22268f000 github.com/go-gormigrate/gormigrate/v2 v2.1.6 github.com/google/uuid v1.6.0 - github.com/labstack/echo/v4 v4.15.2 + github.com/labstack/echo/v4 v4.15.4 github.com/mattn/go-sqlite3 v1.14.48 github.com/nbd-wtf/ln-decodepay v1.13.0 github.com/orandin/lumberjackrus v1.0.1 @@ -151,7 +151,7 @@ require ( github.com/ltcsuite/ltcd v0.23.5 // indirect github.com/ltcsuite/ltcd/chaincfg/chainhash v1.0.2 // indirect github.com/mailru/easyjson v0.9.0 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.22 // indirect github.com/miekg/dns v1.1.62 // indirect github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect diff --git a/go.sum b/go.sum index f8a7db8b..71a5cfb0 100644 --- a/go.sum +++ b/go.sum @@ -384,8 +384,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labstack/echo-jwt/v4 v4.4.0 h1:nrXaEnJupfc2R4XChcLRDyghhMZup77F8nIzHnBK19U= github.com/labstack/echo-jwt/v4 v4.4.0/go.mod h1:kYXWgWms9iFqI3ldR+HAEj/Zfg5rZtR7ePOgktG4Hjg= -github.com/labstack/echo/v4 v4.15.2 h1:nnh2sCzGCVYnU+wCisMPiYapEg/QVo/gcI9ePKg5/T4= -github.com/labstack/echo/v4 v4.15.2/go.mod h1:Xzp1Ns1RA2c9fY7nSgUJkpkUZGNbEIVHZbtbOMPktBI= +github.com/labstack/echo/v4 v4.15.4 h1:DL45vVYa+BWE+XuW+zZNd9H0YEdZ80UAWJGcTVW4EVs= +github.com/labstack/echo/v4 v4.15.4/go.mod h1:CuMetKIRwsuO/qlAgMq+KTAalwGoB/h4tC+yPdrTj1g= github.com/labstack/gommon v0.5.0 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c= github.com/labstack/gommon v0.5.0/go.mod h1:Rzlg7HHy1maLfzBYGg9NZcVuz1sA68HHhLjhcEllYE0= github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= @@ -452,8 +452,8 @@ github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= +github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= From 2bafad7a6c235420c3a01dafe970a5313d94136e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 20:39:06 +0700 Subject: [PATCH 092/136] 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 23 +++----- go.sum | 178 +++++++-------------------------------------------------- 2 files changed, 28 insertions(+), 173 deletions(-) diff --git a/go.mod b/go.mod index 4dc0000a..d754cd0c 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,6 @@ require ( github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect - github.com/Masterminds/semver/v3 v3.3.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect @@ -89,10 +88,9 @@ require ( github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect - github.com/golang-migrate/migrate/v4 v4.18.1 // indirect + github.com/golang-migrate/migrate/v4 v4.19.1 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.1.3 // indirect @@ -103,8 +101,6 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgconn v1.14.3 // indirect @@ -113,9 +109,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgtype v1.14.4 // indirect - github.com/jackc/pgx/v4 v4.18.3 // indirect - github.com/jackc/pgx/v5 v5.7.4 // indirect + github.com/jackc/pgx/v5 v5.9.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect github.com/jessevdk/go-flags v1.6.1 // indirect @@ -142,7 +136,7 @@ require ( github.com/lightningnetwork/lnd/clock v1.1.1 // indirect github.com/lightningnetwork/lnd/fn/v2 v2.0.9 // indirect github.com/lightningnetwork/lnd/healthcheck v1.2.6 // indirect - github.com/lightningnetwork/lnd/kvdb v1.4.16 // indirect + github.com/lightningnetwork/lnd/kvdb v1.5.1 // indirect github.com/lightningnetwork/lnd/queue v1.2.0 // indirect github.com/lightningnetwork/lnd/sqldb v1.0.13 // indirect github.com/lightningnetwork/lnd/ticker v1.1.1 // indirect @@ -168,7 +162,7 @@ require ( github.com/onsi/gomega v1.36.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/opencontainers/runc v1.2.8 // indirect + github.com/opencontainers/runc v1.3.6 // indirect github.com/ory/dockertest/v3 v3.11.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect @@ -215,8 +209,8 @@ require ( go.etcd.io/etcd/raft/v3 v3.5.16 // indirect go.etcd.io/etcd/server/v3 v3.5.16 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0 // indirect @@ -224,7 +218,6 @@ require ( go.opentelemetry.io/otel/sdk v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect - go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect @@ -237,7 +230,7 @@ require ( golang.org/x/text v0.40.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.47.0 // indirect - google.golang.org/genproto v0.0.0-20240930140551-af27646dc61f // indirect + google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect gopkg.in/errgo.v1 v1.0.1 // indirect @@ -258,7 +251,7 @@ require ( ) require ( - github.com/BoltzExchange/boltz-client/v2 v2.12.0 + github.com/BoltzExchange/boltz-client/v2 v2.12.5 github.com/btcsuite/btcd/btcec/v2 v2.5.0 github.com/btcsuite/btcd/chaincfg/chainhash v1.2.0 github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect diff --git a/go.sum b/go.sum index 71a5cfb0..3066accf 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/BoltzExchange/boltz-client/v2 v2.12.0 h1:2iBpnjLtBSm0LSfoKecF0pQq0AbAgHC3x5dvCwh669o= -github.com/BoltzExchange/boltz-client/v2 v2.12.0/go.mod h1:sPmIoNuQyFRtz06VV2QrOPAMOIIY99fkG1yMoUdxzq8= +github.com/BoltzExchange/boltz-client/v2 v2.12.5 h1:q8v6w39mc5OyFYtFJ8biqjIEnWYiihTcGKSFe/NHQDY= +github.com/BoltzExchange/boltz-client/v2 v2.12.5/go.mod h1:Q/qOvQBtB9sz6gTnOQukH3e/NXvS0FBficKgscckMOQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e h1:ahyvB3q25YnZWly5Gq1ekg6jcmWaGj/vG/MhF4aisoc= github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:kGUqhHd//musdITWjFvNTHn90WG9bMLBEPQZ17Cmlpw= @@ -17,9 +17,6 @@ github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec h1:1Qb69m github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec/go.mod h1:CD8UlnlLDiqb36L110uqiP2iSflVjx9g/3U9hCI4q2U= github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg= github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= -github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= @@ -92,8 +89,6 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e h1:0XBUw73chJ1VYSsfvcPvVT7auykAJce9FpRr10L6Qhw= github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:P13beTBKr5Q18lJe1rIoLUqjM+CB1zYrRg44ZqGuQSA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= @@ -110,13 +105,10 @@ github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpS github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= @@ -130,14 +122,14 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvw github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/decred/dcrd/lru v1.1.3 h1:w9EAbvGLyzm6jTjF83UKuqZEiUtJmvRhQDOCEIvSuE0= github.com/decred/dcrd/lru v1.1.3/go.mod h1:Tw0i0pJyiLEx/oZdHLe1Wdv/Y7EGzAX+sYftnmxBR4o= -github.com/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0= -github.com/dhui/dktest v0.4.3/go.mod h1:zNK8IwktWzQRm6I/l2Wjp7MakiyaFWv4G1hjmodmMTs= +github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= +github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM= github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= -github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -201,17 +193,14 @@ github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= -github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y= -github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks= +github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= +github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= @@ -245,7 +234,6 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -264,70 +252,29 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= -github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8= -github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= -github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= -github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= -github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= -github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= @@ -369,14 +316,12 @@ github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2 github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= @@ -399,10 +344,7 @@ github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNqu github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc= @@ -425,8 +367,8 @@ github.com/lightningnetwork/lnd/fn/v2 v2.0.9 h1:ZytG4ltPac/sCyg1EJDn10RGzPIDJeye github.com/lightningnetwork/lnd/fn/v2 v2.0.9/go.mod h1:aPUJHJ31S+Lgoo8I5SxDIjnmeCifqujaiTXKZqpav3w= github.com/lightningnetwork/lnd/healthcheck v1.2.6 h1:1sWhqr93GdkWy4+6U7JxBfcyZIE78MhIHTJZfPx7qqI= github.com/lightningnetwork/lnd/healthcheck v1.2.6/go.mod h1:Mu02um4CWY/zdTOvFje7WJgJcHyX2zq/FG3MhOAiGaQ= -github.com/lightningnetwork/lnd/kvdb v1.4.16 h1:9BZgWdDfjmHRHLS97cz39bVuBAqMc4/p3HX1xtUdbDI= -github.com/lightningnetwork/lnd/kvdb v1.4.16/go.mod h1:HW+bvwkxNaopkz3oIgBV6NEnV4jCEZCACFUcNg4xSjM= +github.com/lightningnetwork/lnd/kvdb v1.5.1 h1:OG5cDbqggxiCFKAJbSPw0PfQovi+0odCZAAU5r5b+ho= +github.com/lightningnetwork/lnd/kvdb v1.5.1/go.mod h1:5lubXYoXHDBWBYKmC+2we7qgkjjz0cEZ/5QSQkxQSec= github.com/lightningnetwork/lnd/queue v1.2.0 h1:sSrn+u84OLuOT/F+xGxgg8VfknXeIZEAFQoMH6BL60s= github.com/lightningnetwork/lnd/queue v1.2.0/go.mod h1:qLNP0L3B7piRGvDyhAyJKic4xTt+Mw4D7mWrQeuAwxY= github.com/lightningnetwork/lnd/sqldb v1.0.13 h1:CcG9mrHNW/hIuZnqgosdiNmS7QhjSyfR/XkSFJB7EC8= @@ -450,13 +392,8 @@ github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUt github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs= @@ -517,8 +454,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/opencontainers/runc v1.2.8 h1:RnEICeDReapbZ5lZEgHvj7E9Q3Eex9toYmaGBsbvU5Q= -github.com/opencontainers/runc v1.2.8/go.mod h1:cC0YkmZcuvr+rtBZ6T7NBoVbMGNAdLa/21vIElJDOzI= +github.com/opencontainers/runc v1.3.6 h1:SLGIymCtsk80iNPWgbc8dtjI30r+5mTVV+4dN8/17Sk= +github.com/opencontainers/runc v1.3.6/go.mod h1:o1wyv76EDlTkcf0KTFgN8bMWLPvgF/HfX709lDv+rr4= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/orandin/lumberjackrus v1.0.1 h1:7ysDQ0MHD79zIFN9/EiDHjUcgopNi5ehtxFDy8rUkWo= github.com/orandin/lumberjackrus v1.0.1/go.mod h1:xYLt6H8W93pKnQgUQaxsApS0Eb4BwHLOkxk5DVzf5H0= @@ -557,25 +494,16 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU= github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= @@ -586,7 +514,6 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -662,10 +589,8 @@ github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chq github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.12.1 h1:jEnl0leC9n7EsT9Rn339nC9e/9GWCMPXzmzowzxmY24= gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.12.1/go.mod h1:1jAwB/XR4i3D72fz3qWAd41tQLYcOCGfWZHMagn5fNg= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= @@ -686,10 +611,10 @@ go.etcd.io/etcd/server/v3 v3.5.16 h1:d0/SAdJ3vVsZvF8IFVb1k8zqMZ+heGcNfft71ul9GWE go.etcd.io/etcd/server/v3 v3.5.16/go.mod h1:ynhyZZpdDp1Gq49jkUg5mfkDWZwXnn3eIqCqtJnrD/s= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 h1:hCq2hNMwsegUvPzI7sPOvtO9cqyy5GbWt/Ybp2xrx8Q= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0/go.mod h1:LqaApwGx/oUmzsbqxkzuBvyoPpkxk3JQWnqfVrJ3wCA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4= @@ -706,26 +631,13 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= @@ -737,17 +649,8 @@ golang.org/x/crypto v0.0.0-20170613210332-850760c427c5/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -757,12 +660,8 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20150829230318-ea47fc708ee3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -774,22 +673,16 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -802,27 +695,20 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -837,32 +723,20 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= @@ -872,24 +746,14 @@ golang.org/x/tools v0.0.0-20181008205924-a2b3f7f249e9/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -903,8 +767,8 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20240930140551-af27646dc61f h1:mCJ6SGikSxVlt9scCayUl2dMq0msUgmBArqRY6umieI= -google.golang.org/genproto v0.0.0-20240930140551-af27646dc61f/go.mod h1:xtVODtPkMQRUZ4kqOTgp6JrXQrPevvfCSdk4mJtHUbM= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= @@ -927,7 +791,6 @@ gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/httprequest.v1 v1.2.0/go.mod h1:T61ZUaJLpMnzvoJDO03ZD8yRXD4nZzBeDoW5e9sffjg= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/juju/environschema.v1 v1.0.0/go.mod h1:WTgU3KXKCVoO9bMmG/4KHzoaRvLeoxfjArpgd1MGWFA= gopkg.in/macaroon-bakery.v2 v2.3.0 h1:b40knPgPTke1QLTE8BSYeH7+R/hiIozB1A8CTLYN0Ic= gopkg.in/macaroon-bakery.v2 v2.3.0/go.mod h1:/8YhtPARXeRzbpEPLmRB66+gQE8/pzBBkWwg7Vz/guc= @@ -969,7 +832,6 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= launchpad.net/gocheck v0.0.0-20140225173054-000000000087 h1:Izowp2XBH6Ya6rv+hqbceQyw/gSGoXfH/UPoTGduL54= launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= From b051e5eb5f38750e8a73aebed426997dd667e428 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Tue, 4 Aug 2026 20:40:36 +0700 Subject: [PATCH 093/136] fix: include payment hash when querying by payment request to ensure index is used (#2480) --- transactions/transactions_service.go | 38 +++++++++++++++++++++------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/transactions/transactions_service.go b/transactions/transactions_service.go index 11d85fd4..23eec12b 100644 --- a/transactions/transactions_service.go +++ b/transactions/transactions_service.go @@ -314,7 +314,11 @@ func (svc *transactionsService) SendPaymentSync(payReq string, amountMsat *uint6 selfPayment := false var incomingTransaction db.Transaction result := svc.db.Limit(1).Find(&incomingTransaction, &db.Transaction{ - Type: constants.TRANSACTION_TYPE_INCOMING, + Type: constants.TRANSACTION_TYPE_INCOMING, + // NOTE: filter by payment hash so the payment hash index is used, + // but also match the payment request as wrapped invoices share + // the same hash but have different payment requests + PaymentHash: paymentRequest.PaymentHash, PaymentRequest: payReq, }) if result.Error == nil && result.RowsAffected > 0 { @@ -335,18 +339,26 @@ func (svc *transactionsService) SendPaymentSync(payReq string, amountMsat *uint6 var existingSettledTransaction db.Transaction if tx.Limit(1).Find(&existingSettledTransaction, &db.Transaction{ Type: constants.TRANSACTION_TYPE_OUTGOING, + PaymentHash: paymentRequest.PaymentHash, PaymentRequest: payReq, State: constants.TRANSACTION_STATE_SETTLED, }).RowsAffected > 0 { - logger.Logger.WithField("payment_request", dbTransaction.PaymentRequest).Debug("this invoice has already been paid") + logger.Logger.WithFields(logrus.Fields{ + "payment_request": payReq, + "payment_hash": paymentRequest.PaymentHash, + }).Debug("this invoice has already been paid") return errors.New("this invoice has already been paid") } if tx.Limit(1).Find(&existingSettledTransaction, &db.Transaction{ Type: constants.TRANSACTION_TYPE_OUTGOING, + PaymentHash: paymentRequest.PaymentHash, PaymentRequest: payReq, State: constants.TRANSACTION_STATE_PENDING, }).RowsAffected > 0 { - logger.Logger.WithField("payment_request", dbTransaction.PaymentRequest).Debug("this invoice is already being paid") + logger.Logger.WithFields(logrus.Fields{ + "payment_request": payReq, + "payment_hash": paymentRequest.PaymentHash, + }).Debug("this invoice is already being paid") return errors.New("there is already a payment pending for this invoice") } @@ -827,7 +839,7 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events. logger.Logger.WithField("event", event).Error("Transaction has no settle deadline") return } - svc.markHoldInvoiceAccepted(lnClientTransaction.Invoice, *lnClientTransaction.SettleDeadline, false) + svc.markHoldInvoiceAccepted(lnClientTransaction.Invoice, lnClientTransaction.PaymentHash, *lnClientTransaction.SettleDeadline, false) case "nwc_lnclient_payment_sent": lnClientTransaction, ok := event.Properties.(*lnclient.Transaction) @@ -937,23 +949,28 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events. } } -func (svc *transactionsService) markHoldInvoiceAccepted(paymentRequest string, settleDeadline uint32, selfPayment bool) { +func (svc *transactionsService) markHoldInvoiceAccepted(paymentRequest string, paymentHash string, settleDeadline uint32, selfPayment bool) { logger.Logger.WithFields(logrus.Fields{ "payment_request": paymentRequest, + "payment_hash": paymentHash, "self_payment": selfPayment, }).Info("Processing hold invoice accepted event") var dbTransaction db.Transaction err := svc.db.Transaction(func(tx *gorm.DB) error { - result := tx.Where("payment_request = ? AND type = ? AND state = ?", paymentRequest, constants.TRANSACTION_TYPE_INCOMING, constants.TRANSACTION_STATE_PENDING).First(&dbTransaction) + // NOTE: filter by payment hash so the payment hash index is used, + // but also match the payment request as wrapped invoices share the same hash + result := tx.Where("payment_hash = ? AND payment_request = ? AND type = ? AND state = ?", paymentHash, paymentRequest, constants.TRANSACTION_TYPE_INCOMING, constants.TRANSACTION_STATE_PENDING).First(&dbTransaction) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { logger.Logger.WithFields(logrus.Fields{ "payment_request": paymentRequest, + "payment_hash": paymentHash, }).Warn("No corresponding pending incoming transaction found in DB for accepted hold invoice") } logger.Logger.WithFields(logrus.Fields{ "payment_request": paymentRequest, + "payment_hash": paymentHash, }).WithError(result.Error).Error("Failed to query DB for accepted hold invoice") return result.Error } @@ -966,6 +983,7 @@ func (svc *transactionsService) markHoldInvoiceAccepted(paymentRequest string, s if err != nil { logger.Logger.WithFields(logrus.Fields{ "payment_request": paymentRequest, + "payment_hash": paymentHash, "id": dbTransaction.ID, }).WithError(err).Error("Failed to update hold invoice state to accepted in DB") return err @@ -973,6 +991,7 @@ func (svc *transactionsService) markHoldInvoiceAccepted(paymentRequest string, s logger.Logger.WithFields(logrus.Fields{ "payment_request": paymentRequest, + "payment_hash": paymentHash, "id": dbTransaction.ID, }).Info("Updated hold invoice state to accepted in DB") @@ -981,6 +1000,7 @@ func (svc *transactionsService) markHoldInvoiceAccepted(paymentRequest string, s if err != nil { logger.Logger.WithFields(logrus.Fields{ "payment_request": paymentRequest, + "payment_hash": paymentHash, "id": dbTransaction.ID, }).WithError(err).Error("Failed DB transaction for hold invoice accepted event") } else { @@ -1013,7 +1033,7 @@ func (svc *transactionsService) interceptSelfPayment(paymentRequest string, paym } if incomingTransaction.Hold { - return svc.interceptSelfHoldPayment(paymentRequest, lnClient) + return svc.interceptSelfHoldPayment(paymentRequest, paymentHash, lnClient) } if incomingTransaction.Preimage == nil { @@ -1035,7 +1055,7 @@ func (svc *transactionsService) interceptSelfPayment(paymentRequest string, paym }, nil } -func (svc *transactionsService) interceptSelfHoldPayment(paymentRequest string, lnClient lnclient.LNClient) (*lnclient.PayInvoiceResponse, error) { +func (svc *transactionsService) interceptSelfHoldPayment(paymentRequest string, paymentHash string, lnClient lnclient.LNClient) (*lnclient.PayInvoiceResponse, error) { settledChannel := make(chan *db.Transaction) canceledChannel := make(chan *db.Transaction) @@ -1054,7 +1074,7 @@ func (svc *transactionsService) interceptSelfHoldPayment(paymentRequest string, fakeSettleDeadline := clientInfo.BlockHeight + 24 - svc.markHoldInvoiceAccepted(paymentRequest, fakeSettleDeadline, true) + svc.markHoldInvoiceAccepted(paymentRequest, paymentHash, fakeSettleDeadline, true) select { case settledTransaction := <-settledChannel: From 4484046ff717edcf99ba089e755b9b71d85c4611 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Tue, 4 Aug 2026 23:14:30 +0700 Subject: [PATCH 094/136] 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 --- go.mod | 2 +- go.sum | 2 ++ lnclient/ldk/ldk.go | 50 ++++++++++++++++++++++++++++++++++++--------- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index d754cd0c..12768518 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/btcsuite/btcd/btcutil v1.2.0 github.com/elnosh/gonuts v0.4.2 github.com/getAlby/go-nostr v0.0.0-20260513161014-22fb7840c7a4 - github.com/getAlby/ldk-node-go v0.0.0-20260608130949-5ba22268f000 + github.com/getAlby/ldk-node-go v0.0.0-20260804150503-a3db1213cce4 github.com/go-gormigrate/gormigrate/v2 v2.1.6 github.com/google/uuid v1.6.0 github.com/labstack/echo/v4 v4.15.4 diff --git a/go.sum b/go.sum index 3066accf..fcb71152 100644 --- a/go.sum +++ b/go.sum @@ -168,6 +168,8 @@ github.com/getAlby/go-nostr v0.0.0-20260513161014-22fb7840c7a4 h1:Z93wPXKIMY4Emr github.com/getAlby/go-nostr v0.0.0-20260513161014-22fb7840c7a4/go.mod h1:BtlkV9evCTjpY0YeFhoNgycp7XNFbnfVXJPoykp+NtM= github.com/getAlby/ldk-node-go v0.0.0-20260608130949-5ba22268f000 h1:rlW59HX0myVvttj1VKiyNPNP564ecEDlUHRxC2dIDYs= github.com/getAlby/ldk-node-go v0.0.0-20260608130949-5ba22268f000/go.mod h1:8BRjtKcz8E0RyYTPEbMS8VIdgredcGSLne8vHDtcRLg= +github.com/getAlby/ldk-node-go v0.0.0-20260804150503-a3db1213cce4 h1:cgefKtvNpBxdQT/taKoYC9C4BTF8tH+Q0uIBfOPLvgI= +github.com/getAlby/ldk-node-go v0.0.0-20260804150503-a3db1213cce4/go.mod h1:8BRjtKcz8E0RyYTPEbMS8VIdgredcGSLne8vHDtcRLg= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gormigrate/gormigrate/v2 v2.1.6 h1:VtX+l1Stj2v5RGubVQk0LS/8EPGXR+ldcOyCmlmKoyg= github.com/go-gormigrate/gormigrate/v2 v2.1.6/go.mod h1:PZpedQc4tWaxn6kvXicwhinh3L0seLpMc5ReKRX5id4= diff --git a/lnclient/ldk/ldk.go b/lnclient/ldk/ldk.go index 9131aed3..c83a0cd6 100644 --- a/lnclient/ldk/ldk.go +++ b/lnclient/ldk/ldk.go @@ -62,6 +62,7 @@ type LDKService struct { lsps2MinPaymentSizeMsat *uint64 lsps2MaxPaymentSizeMsat *uint64 shuttingDown bool + eventHandlingMutex sync.Mutex } const resetRouterKey = "ResetRouter" @@ -311,19 +312,41 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events case <-ldkCtx.Done(): return default: - // NOTE: currently do not use WaitNextEvent() as it can possibly block the LDK thread (to confirm) - event := node.NextEvent() - if event == nil { - // if there is no event, wait before polling again to avoid 100% CPU usage - // TODO: remove this and use WaitNextEvent() - time.Sleep(time.Duration(1000) * time.Millisecond) - continue + } + + // NextEventAsync parks this goroutine on a Go channel until the next event + // arrives - unlike WaitNextEvent it does not block an OS thread in FFI. + // NOTE: the call cannot be cancelled; after shutdown it stays parked until + // the node emits a final event or the process exits. + event := node.NextEventAsync() + + // eventHandlingMutex is held while handling so Shutdown() can wait + // for in-flight event handling to finish before stopping the node. + // Events dropped without EventHandled() are redelivered by LDK on + // the next startup. + ok := func() bool { + ls.eventHandlingMutex.Lock() + defer ls.eventHandlingMutex.Unlock() + + if ldkCtx.Err() != nil { + return false } - ls.handleLdkEvent(event) - ldkEventConsumer <- event + ls.handleLdkEvent(&event) - node.EventHandled() + select { + case ldkEventConsumer <- &event: + case <-ldkCtx.Done(): + return false + } + + if err := node.EventHandled(); err != nil { + logger.Logger.WithError(err).Error("Failed to mark LDK event as handled") + } + return true + }() + if !ok { + return } } }() @@ -487,6 +510,13 @@ func (ls *LDKService) Shutdown() error { logger.Logger.Info("cancelling LDK context") ls.cancel() + // wait for in-flight LDK event handling to finish - handleLdkEvent makes + // node calls which must not run once the node is stopped and destroyed. + // Held until the end of Shutdown; the event loop checks the cancelled + // context under this mutex before touching the node. + ls.eventHandlingMutex.Lock() + defer ls.eventHandlingMutex.Unlock() + maxAttempts := 40 for i := 0; ls.syncing; i++ { logger.Logger.WithField("attempt", i).Warn("Waiting for background sync to finish before stopping LDK node...") From 8a9ba498072ec0ee159ff633029d11c5a5b547a8 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Wed, 5 Aug 2026 14:29:40 +0700 Subject: [PATCH 095/136] chore: bump node version in dockerfile to 22 (#2495) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e5f0ded5..68cfcdae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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= e.g. --build-arg BASE_PATH=/hub From 43d37d75d1aa6bb7ea5893776dfe97d621ac0d17 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Wed, 5 Aug 2026 14:50:08 +0700 Subject: [PATCH 096/136] 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 --- go.mod | 2 +- go.sum | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 12768518..69995839 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/btcsuite/btcd v0.25.1-0.20260310163610-1c55c7c18179 github.com/btcsuite/btcd/btcutil v1.2.0 github.com/elnosh/gonuts v0.4.2 - github.com/getAlby/go-nostr v0.0.0-20260513161014-22fb7840c7a4 + github.com/getAlby/go-nostr v0.0.0-20260805072924-9844f892c3c8 github.com/getAlby/ldk-node-go v0.0.0-20260804150503-a3db1213cce4 github.com/go-gormigrate/gormigrate/v2 v2.1.6 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index fcb71152..b82673db 100644 --- a/go.sum +++ b/go.sum @@ -164,10 +164,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/getAlby/go-nostr v0.0.0-20260513161014-22fb7840c7a4 h1:Z93wPXKIMY4Emr+zDz0R0NrSg1FEVGrzcqqomuSrmko= -github.com/getAlby/go-nostr v0.0.0-20260513161014-22fb7840c7a4/go.mod h1:BtlkV9evCTjpY0YeFhoNgycp7XNFbnfVXJPoykp+NtM= -github.com/getAlby/ldk-node-go v0.0.0-20260608130949-5ba22268f000 h1:rlW59HX0myVvttj1VKiyNPNP564ecEDlUHRxC2dIDYs= -github.com/getAlby/ldk-node-go v0.0.0-20260608130949-5ba22268f000/go.mod h1:8BRjtKcz8E0RyYTPEbMS8VIdgredcGSLne8vHDtcRLg= +github.com/getAlby/go-nostr v0.0.0-20260805072924-9844f892c3c8 h1:r2Th/F2tHpQl+eHIiGxMlF7R8tsdLQxrPMoIOacabO0= +github.com/getAlby/go-nostr v0.0.0-20260805072924-9844f892c3c8/go.mod h1:UtZi+MvE7ayGS85zEr5RH2s96T2ok5Gotgp3n65sULU= github.com/getAlby/ldk-node-go v0.0.0-20260804150503-a3db1213cce4 h1:cgefKtvNpBxdQT/taKoYC9C4BTF8tH+Q0uIBfOPLvgI= github.com/getAlby/ldk-node-go v0.0.0-20260804150503-a3db1213cce4/go.mod h1:8BRjtKcz8E0RyYTPEbMS8VIdgredcGSLne8vHDtcRLg= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= From 9971aa1ac8ce686440150e7ead4d9a3ca858d8ca Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Wed, 5 Aug 2026 15:08:03 +0700 Subject: [PATCH 097/136] 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 --- frontend/src/assets/images/node/bark-dark.svg | 12 ++++++++++++ frontend/src/assets/images/node/bark-light.svg | 12 ++++++++++++ frontend/src/assets/images/node/bark.jpg | Bin 9953 -> 0 bytes frontend/src/screens/setup/SetupNode.tsx | 10 ++++++++-- 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 frontend/src/assets/images/node/bark-dark.svg create mode 100644 frontend/src/assets/images/node/bark-light.svg delete mode 100644 frontend/src/assets/images/node/bark.jpg diff --git a/frontend/src/assets/images/node/bark-dark.svg b/frontend/src/assets/images/node/bark-dark.svg new file mode 100644 index 00000000..8cc83e3a --- /dev/null +++ b/frontend/src/assets/images/node/bark-dark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/frontend/src/assets/images/node/bark-light.svg b/frontend/src/assets/images/node/bark-light.svg new file mode 100644 index 00000000..1707c3b5 --- /dev/null +++ b/frontend/src/assets/images/node/bark-light.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/frontend/src/assets/images/node/bark.jpg b/frontend/src/assets/images/node/bark.jpg deleted file mode 100644 index c9948cfdd7b8eacf6e23aaac36220c0406408fb3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9953 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, + icon: ( + <> + + + + ), }, }; From d3455eee7df1ab5042a0028f23fe02798c7ba954 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Wed, 5 Aug 2026 15:26:40 +0700 Subject: [PATCH 098/136] 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 --- frontend/src/screens/settings/DeveloperSettings.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/screens/settings/DeveloperSettings.tsx b/frontend/src/screens/settings/DeveloperSettings.tsx index 68fe28c8..2dfe1b98 100644 --- a/frontend/src/screens/settings/DeveloperSettings.tsx +++ b/frontend/src/screens/settings/DeveloperSettings.tsx @@ -13,10 +13,12 @@ import { Separator } from "src/components/ui/separator"; import { useAlbyMe } from "src/hooks/useAlbyMe"; import { copyToClipboard } from "src/lib/clipboard"; import { AuthTokenResponse } from "src/types"; +import { isHttpMode } from "src/utils/isHttpMode"; import { request } from "src/utils/request"; export default function DeveloperSettings() { const { data: albyMe } = useAlbyMe(); + const _isHttpMode = isHttpMode(); const [token, setToken] = React.useState(); const [tokenPermission, setTokenPermission] = React.useState(); const [expiryDays, setExpiryDays] = React.useState("365"); @@ -103,7 +105,14 @@ export default function DeveloperSettings() { be removed entirely in the future.
    - {!token && !showCreateTokenForm && ( + {!_isHttpMode && ( +
    + API access is not available in the desktop app because it does not + expose an HTTP API. To use the API, install the Alby Hub server + version instead. +
    + )} + {_isHttpMode && !token && !showCreateTokenForm && (

    Lightning Node Backend

    -

    - {info.backendType} -

    +
    +
    + {backendTypeConfigs[info.backendType].icon} +
    +

    {backendTypeConfigs[info.backendType].title}

    +
    {info.chainDataSourceType && (
    diff --git a/frontend/src/screens/setup/SetupNode.tsx b/frontend/src/screens/setup/SetupNode.tsx index 8f229bea..0cd5349e 100644 --- a/frontend/src/screens/setup/SetupNode.tsx +++ b/frontend/src/screens/setup/SetupNode.tsx @@ -1,66 +1,20 @@ -import React, { ReactElement } from "react"; +import React from "react"; import { useNavigate } from "react-router"; import Container from "src/components/Container"; import TwoColumnLayoutHeader from "src/components/TwoColumnLayoutHeader"; -import { LDKIcon } from "src/components/icons/LDK"; -import { PhoenixdIcon } from "src/components/icons/Phoenixd"; import { Button } from "src/components/ui/button"; import { cn } from "src/lib/utils"; import { BackendType } from "src/types"; -import barkDark from "src/assets/images/node/bark-dark.svg"; -import barkLight from "src/assets/images/node/bark-light.svg"; -import cashu from "src/assets/images/node/cashu.png"; -import cln from "src/assets/images/node/cln.png"; -import lnd from "src/assets/images/node/lnd.png"; import { backendTypeConfigs } from "src/lib/backendType"; import useSetupStore from "src/state/SetupStore"; -type BackendTypeDisplayConfig = { - title: string; - icon: ReactElement; -}; - -const backendTypeDisplayConfigs: Partial< - Record -> = { - LDK: { - title: "LDK", - icon: , - }, - PHOENIX: { - title: "phoenixd", - icon: , - }, - LND: { - title: "LND", - icon: , - }, - CASHU: { - title: "Cashu Mint", - icon: , - }, - CLN: { - title: "CLN", - icon: , - }, - BARK: { - title: "Bark", - icon: ( - <> - - - - ), - }, -}; - -const backendTypeDisplayConfigList = Object.entries( - backendTypeDisplayConfigs -).map((entry) => ({ - ...entry[1], - backendType: entry[0] as BackendType, -})); +const backendTypeDisplayConfigList = Object.entries(backendTypeConfigs).map( + (entry) => ({ + ...entry[1], + backendType: entry[0] as BackendType, + }) +); export function SetupNode() { const navigate = useNavigate(); From 3b3e784fa6d6fe650635d2d20ddbd57f0b3b6ce4 Mon Sep 17 00:00:00 2001 From: Josip Date: Thu, 6 Aug 2026 12:24:10 +0200 Subject: [PATCH 102/136] 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 --- lnclient/phoenixd/phoenixd.go | 109 ++++++++++++++++++++++++++++++---- 1 file changed, 98 insertions(+), 11 deletions(-) diff --git a/lnclient/phoenixd/phoenixd.go b/lnclient/phoenixd/phoenixd.go index b3684f04..a36ab0a0 100644 --- a/lnclient/phoenixd/phoenixd.go +++ b/lnclient/phoenixd/phoenixd.go @@ -21,6 +21,9 @@ import ( "github.com/sirupsen/logrus" ) +// errNotFound indicates that a payment was not found at the queried endpoint. +var errNotFound = errors.New("phoenixd: payment not found") + type InvoiceResponse struct { PaymentHash string `json:"paymentHash"` Preimage string `json:"preimage"` @@ -259,42 +262,85 @@ func (svc *PhoenixService) CancelHoldInvoice(ctx context.Context, paymentHash st return errors.New("not implemented") } +// LookupInvoice looks up a transaction by payment hash. It first checks +// incoming payments, then falls back to outgoing payments if the incoming +// payment is not found (HTTP 404). func (svc *PhoenixService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *lnclient.Transaction, err error) { + transaction, err = svc.lookupIncomingPayment(ctx, paymentHash) + if err == nil { + return transaction, nil + } + + // Only fall back to outgoing lookup when incoming returns not-found. + if !errors.Is(err, errNotFound) { + return nil, err + } + + return svc.lookupOutgoingPayment(ctx, paymentHash) +} + +// lookupIncomingPayment fetches an incoming payment from Phoenixd by payment hash. +func (svc *PhoenixService) lookupIncomingPayment(ctx context.Context, paymentHash string) (*lnclient.Transaction, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, svc.Address+"/payments/incoming/"+paymentHash, nil) if err != nil { - return nil, err + return nil, fmt.Errorf("create phoenixd incoming payment request: %w", err) } req.Header.Add("Authorization", "Basic "+svc.Authorization) client := &http.Client{Timeout: 5 * time.Second} resp, err := client.Do(req) if err != nil { - return nil, err + return nil, fmt.Errorf("call phoenixd incoming payment endpoint: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - return nil, err + return nil, fmt.Errorf("read phoenixd incoming payment response: %w", err) + } + if resp.StatusCode == http.StatusNotFound { + return nil, errNotFound } if resp.StatusCode != http.StatusOK { - logger.Logger.WithFields(logrus.Fields{ - "body": string(body), - "status_code": resp.StatusCode, - }).Error("phoenixd incoming payments endpoint returned non-success code") return nil, fmt.Errorf("phoenixd incoming payments endpoint returned non-success code: %d %s", resp.StatusCode, string(body)) } var invoiceRes InvoiceResponse if err := json.Unmarshal(body, &invoiceRes); err != nil { - return nil, err + return nil, fmt.Errorf("decode phoenixd incoming payment response: %w", err) } - transaction, err = phoenixInvoiceToTransaction(&invoiceRes) + return phoenixInvoiceToTransaction(&invoiceRes) +} + +// lookupOutgoingPayment fetches an outgoing payment from Phoenixd using the +// /payments/outgoingbyhash/{paymentHash} endpoint. +func (svc *PhoenixService) lookupOutgoingPayment(ctx context.Context, paymentHash string) (*lnclient.Transaction, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, svc.Address+"/payments/outgoingbyhash/"+paymentHash, nil) if err != nil { - return nil, err + return nil, fmt.Errorf("create phoenixd outgoing payment request: %w", err) + } + req.Header.Add("Authorization", "Basic "+svc.Authorization) + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("call phoenixd outgoing payment endpoint: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("read phoenixd outgoing payment response: %w", err) + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("phoenixd outgoing payment lookup returned non-success code: %d %s", resp.StatusCode, string(body)) } - return transaction, nil + var paymentRes OutgoingPaymentResponse + if err := json.Unmarshal(body, &paymentRes); err != nil { + return nil, fmt.Errorf("decode phoenixd outgoing payment response: %w", err) + } + + return outgoingPaymentToTransaction(&paymentRes) } func (svc *PhoenixService) SendPaymentSync(payReq string, amountMsat *uint64) (*lnclient.PayInvoiceResponse, error) { @@ -488,6 +534,47 @@ func phoenixInvoiceToTransaction(invoiceRes *InvoiceResponse) (*lnclient.Transac }, nil } +// outgoingPaymentToTransaction converts a Phoenixd OutgoingPaymentResponse +// to an lnclient.Transaction. +func outgoingPaymentToTransaction(payment *OutgoingPaymentResponse) (*lnclient.Transaction, error) { + var settledAt *int64 + if payment.CompletedAt != 0 { + settledAtUnix := time.UnixMilli(payment.CompletedAt).Unix() + settledAt = &settledAtUnix + } + + paymentRequest, err := decodepay.Decodepay(payment.Invoice) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "bolt11": payment.Invoice, + }).Errorf("Failed to decode bolt11 invoice: %v", err) + return nil, fmt.Errorf("decode phoenixd outgoing payment bolt11: %w", err) + } + + expiresAt := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix() + + // unlike incoming payments, "fees" on outgoing payments is in millisats, + // and "sent" (in sats) includes the fees + amountMsat := paymentRequest.MSatoshi + if amountMsat == 0 { + amountMsat = payment.Sent*1000 - payment.Fees + } + + return &lnclient.Transaction{ + Type: "outgoing", + Invoice: payment.Invoice, + Preimage: payment.Preimage, + PaymentHash: payment.PaymentHash, + AmountMsat: amountMsat, + FeesPaidMsat: payment.Fees, + CreatedAt: time.UnixMilli(payment.CreatedAt).Unix(), + Description: paymentRequest.Description, + SettledAt: settledAt, + ExpiresAt: &expiresAt, + DescriptionHash: paymentRequest.DescriptionHash, + }, nil +} + func (svc *PhoenixService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef { return nil } From 35d666d46975ec8d4d8c7c338aa0191fe0e7c1ef Mon Sep 17 00:00:00 2001 From: Alchemist Date: Fri, 7 Aug 2026 04:52:14 +0100 Subject: [PATCH 103/136] 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 * feat: set transaction filters in a dialog from wallet actions menu Co-Authored-By: Claude Fable 5 * feat: filter transactions by search term and type Co-Authored-By: Claude Fable 5 * fix: reject invalid transaction filters and reset page synchronously Co-Authored-By: Claude Fable 5 * fix: parse complete minimum amount value in transactions filter dialog Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Roland Bewick Co-authored-by: Claude Fable 5 --- api/models.go | 73 +++--- api/transactions.go | 55 ++++- api/transactions_test.go | 36 +++ .../components/TransactionsFilterDialog.tsx | 140 ++++++++++++ frontend/src/components/TransactionsList.tsx | 35 ++- .../src/components/TransactionsListMenu.tsx | 47 ++-- frontend/src/components/WalletActionsMenu.tsx | 108 ++++++--- .../src/components/layouts/WalletLayout.tsx | 8 +- frontend/src/hooks/useTransactions.ts | 52 ++++- frontend/src/state/TransactionFiltersStore.ts | 17 ++ http/http_service.go | 9 +- .../list_transactions_controller.go | 5 +- transactions/list_transactions_test.go | 216 +++++++++++++++++- transactions/notifications_test.go | 8 +- transactions/transactions_service.go | 62 ++++- wails/wails_handlers.go | 47 ++-- 16 files changed, 780 insertions(+), 138 deletions(-) create mode 100644 frontend/src/components/TransactionsFilterDialog.tsx create mode 100644 frontend/src/state/TransactionFiltersStore.ts diff --git a/api/models.go b/api/models.go index b39abff1..eda20676 100644 --- a/api/models.go +++ b/api/models.go @@ -43,7 +43,7 @@ type API interface { SignMessage(ctx context.Context, message string) (*SignMessageResponse, 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) + 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) @@ -304,38 +304,38 @@ 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"` - 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"` + 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"` + 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"` } type UpdateSettingsRequest struct { @@ -497,6 +497,13 @@ 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"` diff --git a/api/transactions.go b/api/transactions.go index 2e4dc18b..eb5109b4 100644 --- a/api/transactions.go +++ b/api/transactions.go @@ -4,9 +4,13 @@ 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" @@ -45,7 +49,47 @@ func (api *api) SetTransactionUserLabels(ctx context.Context, id uint, labels ma return api.svc.GetTransactionsService().SetTransactionUserLabels(ctx, id, labels) } -func (api *api) ListTransactions(ctx context.Context, appId *uint, limit uint64, offset uint64) (*ListTransactionsResponse, error) { +// 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 @@ -56,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, lnClient, 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)) } diff --git a/api/transactions_test.go b/api/transactions_test.go index 9db68655..934ef46f 100644 --- a/api/transactions_test.go +++ b/api/transactions_test.go @@ -2,6 +2,7 @@ package api import ( "context" + "net/url" "testing" "github.com/stretchr/testify/assert" @@ -60,3 +61,38 @@ func TestCreateInvoice_ToAppNotFound(t *testing.T) { 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) + } +} diff --git a/frontend/src/components/TransactionsFilterDialog.tsx b/frontend/src/components/TransactionsFilterDialog.tsx new file mode 100644 index 00000000..4b72ee44 --- /dev/null +++ b/frontend/src/components/TransactionsFilterDialog.tsx @@ -0,0 +1,140 @@ +import React from "react"; +import { Button } from "src/components/ui/button"; +import { Checkbox } from "src/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "src/components/ui/dialog"; +import { Input } from "src/components/ui/input"; +import { Label } from "src/components/ui/label"; +import { ToggleGroup, ToggleGroupItem } from "src/components/ui/toggle-group"; +import { + defaultTransactionFilters, + type TransactionFilters, +} from "src/hooks/useTransactions"; + +const TYPE_OPTIONS: { label: string; value: string }[] = [ + { label: "All", value: "all" }, + { label: "Sent", value: "outgoing" }, + { label: "Received", value: "incoming" }, +]; + +type TransactionsFilterDialogProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + filters: TransactionFilters; + onFiltersChange: (filters: TransactionFilters) => void; +}; + +export function TransactionsFilterDialog({ + open, + onOpenChange, + filters, + onFiltersChange, +}: TransactionsFilterDialogProps) { + const [searchTerm, setSearchTerm] = React.useState(""); + const [type, setType] = React.useState("all"); + const [minAmountSat, setMinAmountSat] = React.useState(""); + const [hideFailed, setHideFailed] = React.useState(false); + + React.useEffect(() => { + if (open) { + setSearchTerm(filters.searchTerm ?? ""); + setType(filters.type ?? "all"); + setMinAmountSat(filters.minAmountSat ? String(filters.minAmountSat) : ""); + setHideFailed(!!filters.hideFailed); + } + }, [open, filters]); + + function onSubmit(e: React.FormEvent) { + e.preventDefault(); + const parsedMinAmountSat = Number(minAmountSat); + onFiltersChange({ + searchTerm: searchTerm.trim() || undefined, + type: type === "incoming" || type === "outgoing" ? type : undefined, + minAmountSat: + Number.isSafeInteger(parsedMinAmountSat) && parsedMinAmountSat > 0 + ? parsedMinAmountSat + : undefined, + hideFailed, + }); + onOpenChange(false); + } + + function onReset() { + onFiltersChange({ ...defaultTransactionFilters }); + onOpenChange(false); + } + + return ( + + +
    + + Filter Transactions + + Choose which payments appear in your transaction list. + + +
    + + setSearchTerm(e.target.value)} + /> +
    +
    + + value && setType(value)} + > + {TYPE_OPTIONS.map((option) => ( + + {option.label} + + ))} + +
    +
    + + setMinAmountSat(e.target.value.trim())} + /> +
    +
    + setHideFailed(checked === true)} + /> + +
    + + + + +
    +
    +
    + ); +} diff --git a/frontend/src/components/TransactionsList.tsx b/frontend/src/components/TransactionsList.tsx index 83ff8fdf..bf2d5ff1 100644 --- a/frontend/src/components/TransactionsList.tsx +++ b/frontend/src/components/TransactionsList.tsx @@ -5,7 +5,12 @@ import EmptyState from "src/components/EmptyState"; import Loading from "src/components/Loading"; import TransactionItem from "src/components/TransactionItem"; import { LIST_TRANSACTIONS_LIMIT } from "src/constants"; -import { getTransactionsUrl, useTransactions } from "src/hooks/useTransactions"; +import { + getTransactionsUrl, + hasActiveTransactionFilters, + useTransactions, +} from "src/hooks/useTransactions"; +import useTransactionFiltersStore from "src/state/TransactionFiltersStore"; type TransactionsListProps = { appId?: number; @@ -23,20 +28,36 @@ function TransactionsList({ emptyVariant, }: TransactionsListProps) { const [page, setPage] = useState(1); + const { filters } = useTransactionFiltersStore(); + + // Reset pagination during render when the filters or app change, so no + // request is made for a page that may not exist under the new list. + const [prevListIdentity, setPrevListIdentity] = useState({ appId, filters }); + if ( + prevListIdentity.appId !== appId || + prevListIdentity.filters !== filters + ) { + setPrevListIdentity({ appId, filters }); + setPage(1); + } + const transactionListRef = useRef(null); const transactionListKey = getTransactionsUrl( appId, LIST_TRANSACTIONS_LIMIT, - page + page, + filters ); const { data: transactionData, isLoading } = useTransactions( appId, false, LIST_TRANSACTIONS_LIMIT, - page + page, + filters ); const transactions = transactionData?.transactions || []; const totalCount = transactionData?.totalCount || 0; + const hasActiveFilters = hasActiveTransactionFilters(filters); const handlePageChange = (page: number) => { setPage(page); @@ -55,8 +76,12 @@ function TransactionsList({ {!transactions.length ? ( ) : ( diff --git a/frontend/src/components/TransactionsListMenu.tsx b/frontend/src/components/TransactionsListMenu.tsx index 190599b4..7aa4cb4b 100644 --- a/frontend/src/components/TransactionsListMenu.tsx +++ b/frontend/src/components/TransactionsListMenu.tsx @@ -1,27 +1,46 @@ -import { DownloadIcon, EllipsisVerticalIcon } from "lucide-react"; +import { DownloadIcon, EllipsisVerticalIcon, FunnelIcon } from "lucide-react"; +import { useState } from "react"; +import { TransactionsFilterDialog } from "src/components/TransactionsFilterDialog"; import { Button } from "src/components/ui/button"; import { DropdownMenu, DropdownMenuContent, + DropdownMenuItem, DropdownMenuTrigger, } from "src/components/ui/dropdown-menu"; import { ProDropdownMenuItem } from "src/components/UpgradeDialog"; +import useTransactionFiltersStore from "src/state/TransactionFiltersStore"; import { handleExportTransactions } from "./transactions-utils"; export const TransactionsListMenu = ({ appId }: { appId?: number }) => { + const [filterDialogOpen, setFilterDialogOpen] = useState(false); + const { filters, setFilters } = useTransactionFiltersStore(); + return ( - - - - handleExportTransactions(appId)}> - - Export Transactions - - - + <> + + + + setFilterDialogOpen(true)}> + + Filter Transactions + + handleExportTransactions(appId)}> + + Export Transactions + + + + + ); }; diff --git a/frontend/src/components/WalletActionsMenu.tsx b/frontend/src/components/WalletActionsMenu.tsx index a7e264e3..54b997e8 100644 --- a/frontend/src/components/WalletActionsMenu.tsx +++ b/frontend/src/components/WalletActionsMenu.tsx @@ -4,9 +4,12 @@ import { CreditCardIcon, DownloadIcon, EllipsisVerticalIcon, + FunnelIcon, } from "lucide-react"; +import { useState } from "react"; import { Link } from "react-router"; import ExternalLink from "src/components/ExternalLink"; +import { TransactionsFilterDialog } from "src/components/TransactionsFilterDialog"; import { Button } from "src/components/ui/button"; import { DropdownMenu, @@ -16,55 +19,84 @@ import { DropdownMenuTrigger, } from "src/components/ui/dropdown-menu"; import { ProDropdownMenuItem } from "src/components/UpgradeDialog"; +import useTransactionFiltersStore from "src/state/TransactionFiltersStore"; import { handleExportTransactions } from "./transactions-utils"; export function WalletActionsMenu({ hasChannelManagement, + isOnchain, }: { hasChannelManagement: boolean; + isOnchain: boolean; }) { + const [filterDialogOpen, setFilterDialogOpen] = useState(false); + const { filters, setFilters } = useTransactionFiltersStore(); + return ( - - - -
    - {hasChannelManagement && ( + <> + + + +
    + {hasChannelManagement && ( + + + + Swap + + + )} - - - Swap + + + Recurring + + + + Buy + + + {!isOnchain && } +
    + {!isOnchain && ( + <> + setFilterDialogOpen(true)}> + + Filter Transactions + + handleExportTransactions()}> + + Export Transactions + + )} - - - - Recurring - - - - - - Buy - - - -
    - handleExportTransactions()}> - - Export Transactions - -
    -
    + + + {!isOnchain && ( + + )} + ); } diff --git a/frontend/src/components/layouts/WalletLayout.tsx b/frontend/src/components/layouts/WalletLayout.tsx index 9b015d43..89f26457 100644 --- a/frontend/src/components/layouts/WalletLayout.tsx +++ b/frontend/src/components/layouts/WalletLayout.tsx @@ -3,7 +3,7 @@ import { CalendarSyncIcon, CreditCardIcon, } from "lucide-react"; -import { Outlet } from "react-router"; +import { Outlet, useMatch } from "react-router"; import AppHeader from "src/components/AppHeader"; import Loading from "src/components/Loading"; import { ExternalLinkButton } from "src/components/ui/custom/external-link-button"; @@ -17,6 +17,7 @@ export default function WalletLayout() { useSyncWallet(); const { data: info, hasChannelManagement } = useInfo(); const { data: balances } = useBalances(true); + const isOnchain = !!useMatch("/wallet/onchain"); if (!info || !balances) { return ; @@ -59,7 +60,10 @@ export default function WalletLayout() { Buy - +
    } /> diff --git a/frontend/src/hooks/useTransactions.ts b/frontend/src/hooks/useTransactions.ts index 492e4e65..babb552e 100644 --- a/frontend/src/hooks/useTransactions.ts +++ b/frontend/src/hooks/useTransactions.ts @@ -7,12 +7,53 @@ const pollConfiguration: SWRConfiguration = { refreshInterval: 10000, }; -export function getTransactionsUrl(appId?: number, limit = 100, page = 1) { +export type TransactionFilters = { + searchTerm?: string; + type?: "incoming" | "outgoing"; + minAmountSat?: number; + hideFailed?: boolean; +}; + +export const defaultTransactionFilters: TransactionFilters = {}; + +export function hasActiveTransactionFilters(filters: TransactionFilters) { + return ( + !!filters.searchTerm || + !!filters.type || + (filters.minAmountSat ?? 0) > 0 || + !!filters.hideFailed + ); +} + +export function getTransactionsUrl( + appId?: number, + limit = 100, + page = 1, + filters?: TransactionFilters +) { const offset = (page - 1) * limit; - let url = `/api/transactions?limit=${limit}&offset=${offset}`; + const searchParams = new URLSearchParams({ + limit: String(limit), + offset: String(offset), + }); + if (appId) { - url += `&appId=${appId}`; + searchParams.set("appId", String(appId)); } + if (filters?.searchTerm) { + searchParams.set("search", filters.searchTerm); + } + if (filters?.type) { + searchParams.set("type", filters.type); + } + if (filters?.minAmountSat && filters.minAmountSat > 0) { + searchParams.set("minAmountSat", String(filters.minAmountSat)); + } + if (filters?.hideFailed) { + searchParams.set("hideFailed", "true"); + } + + const url = `/api/transactions?${searchParams.toString()}`; return url; } @@ -21,9 +62,10 @@ export function useTransactions( appId?: number, poll = false, limit = 100, - page = 1 + page = 1, + filters?: TransactionFilters ) { - const url = getTransactionsUrl(appId, limit, page); + const url = getTransactionsUrl(appId, limit, page, filters); return useSWR( url, diff --git a/frontend/src/state/TransactionFiltersStore.ts b/frontend/src/state/TransactionFiltersStore.ts new file mode 100644 index 00000000..c9fa896c --- /dev/null +++ b/frontend/src/state/TransactionFiltersStore.ts @@ -0,0 +1,17 @@ +import { + defaultTransactionFilters, + type TransactionFilters, +} from "src/hooks/useTransactions"; +import { create } from "zustand"; + +interface TransactionFiltersStore { + readonly filters: TransactionFilters; + setFilters(filters: TransactionFilters): void; +} + +const useTransactionFiltersStore = create((set) => ({ + filters: defaultTransactionFilters, + setFilters: (filters) => set({ filters }), +})); + +export default useTransactionFiltersStore; diff --git a/http/http_service.go b/http/http_service.go index b37dee76..53bc1e0f 100644 --- a/http/http_service.go +++ b/http/http_service.go @@ -765,7 +765,14 @@ func (httpSvc *HttpService) listTransactionsHandler(c echo.Context) error { } } - transactions, err := httpSvc.api.ListTransactions(ctx, appId, limit, offset) + filters, err := api.ParseListTransactionsFilters(c.QueryParams()) + if err != nil { + return c.JSON(http.StatusBadRequest, ErrorResponse{ + Message: err.Error(), + }) + } + + transactions, err := httpSvc.api.ListTransactions(ctx, appId, limit, offset, filters) if err != nil { return c.JSON(http.StatusInternalServerError, ErrorResponse{ diff --git a/nip47/controllers/list_transactions_controller.go b/nip47/controllers/list_transactions_controller.go index 450a55ea..cea716d0 100644 --- a/nip47/controllers/list_transactions_controller.go +++ b/nip47/controllers/list_transactions_controller.go @@ -6,6 +6,7 @@ import ( "github.com/getAlby/go-nostr" "github.com/getAlby/hub/logger" "github.com/getAlby/hub/nip47/models" + "github.com/getAlby/hub/transactions" "github.com/sirupsen/logrus" ) @@ -50,7 +51,9 @@ func (controller *nip47Controller) HandleListTransactionsEvent(ctx context.Conte transactionType = &listParams.Type } - dbTransactions, totalCount, err := controller.transactionsService.ListTransactions(ctx, listParams.From, listParams.Until, limit, listParams.Offset, listParams.Unpaid || listParams.UnpaidOutgoing, listParams.Unpaid || listParams.UnpaidIncoming, transactionType, controller.lnClient, &appId, false) + dbTransactions, totalCount, err := controller.transactionsService.ListTransactions(ctx, listParams.From, listParams.Until, limit, listParams.Offset, listParams.Unpaid || listParams.UnpaidOutgoing, listParams.Unpaid || listParams.UnpaidIncoming, controller.lnClient, &appId, false, &transactions.ListTransactionsFilters{ + Type: transactionType, + }) if err != nil { logger.Logger.WithFields(logrus.Fields{ "params": listParams, diff --git a/transactions/list_transactions_test.go b/transactions/list_transactions_test.go index 276acb35..2709cad5 100644 --- a/transactions/list_transactions_test.go +++ b/transactions/list_transactions_test.go @@ -11,6 +11,7 @@ import ( "github.com/getAlby/hub/constants" "github.com/getAlby/hub/db" "github.com/getAlby/hub/tests" + "gorm.io/datatypes" ) func TestListTransactions_Paid(t *testing.T) { @@ -48,7 +49,7 @@ func TestListTransactions_Paid(t *testing.T) { transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) - incomingTransactions, totalCount, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, false, false, nil, svc.LNClient, nil, false) + incomingTransactions, totalCount, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, false, false, svc.LNClient, nil, false, nil) assert.NoError(t, err) assert.Equal(t, uint64(1), totalCount) assert.Equal(t, 1, len(incomingTransactions)) @@ -114,7 +115,7 @@ func TestListTransactions_UnpaidIncoming(t *testing.T) { transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) - incomingTransactions, totalCount, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, false, true, nil, svc.LNClient, nil, false) + incomingTransactions, totalCount, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, false, true, svc.LNClient, nil, false, nil) assert.NoError(t, err) assert.Equal(t, uint64(3), totalCount) assert.Equal(t, 3, len(incomingTransactions)) @@ -182,7 +183,7 @@ func TestListTransactions_UnpaidOutgoing(t *testing.T) { transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) - outgoingTransactions, totalCount, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, true, false, nil, svc.LNClient, nil, false) + outgoingTransactions, totalCount, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, true, false, svc.LNClient, nil, false, nil) assert.NoError(t, err) assert.Equal(t, uint64(3), totalCount) assert.Equal(t, 3, len(outgoingTransactions)) @@ -250,7 +251,7 @@ func TestListTransactions_Unpaid(t *testing.T) { transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) - outgoingTransactions, totalCount, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, true, true, nil, svc.LNClient, nil, false) + outgoingTransactions, totalCount, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, true, true, svc.LNClient, nil, false, nil) assert.NoError(t, err) assert.Equal(t, uint64(5), totalCount) assert.Equal(t, 5, len(outgoingTransactions)) @@ -286,7 +287,7 @@ func TestListTransactions_Limit(t *testing.T) { transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) - incomingTransactions, totalCount, err := transactionsService.ListTransactions(ctx, 0, 0, 1, 0, false, false, nil, svc.LNClient, nil, false) + incomingTransactions, totalCount, err := transactionsService.ListTransactions(ctx, 0, 0, 1, 0, false, false, svc.LNClient, nil, false, nil) assert.NoError(t, err) assert.Equal(t, uint64(2), totalCount) assert.Equal(t, 1, len(incomingTransactions)) @@ -343,13 +344,210 @@ func TestListTransactions_Offset(t *testing.T) { transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) - incomingTransactions, totalCount, err := transactionsService.ListTransactions(ctx, 0, 0, 1, 2, false, false, nil, svc.LNClient, nil, false) + incomingTransactions, totalCount, err := transactionsService.ListTransactions(ctx, 0, 0, 1, 2, false, false, svc.LNClient, nil, false, nil) assert.NoError(t, err) assert.Equal(t, uint64(4), totalCount) assert.Equal(t, 1, len(incomingTransactions)) assert.Equal(t, "third", incomingTransactions[0].Description) } +func TestListTransactions_MinAmount(t *testing.T) { + ctx := context.TODO() + + svc, err := tests.CreateTestService(t) + require.NoError(t, err) + defer svc.Remove() + + mockPreimage := tests.MockLNClientTransaction.Preimage + svc.DB.Create(&db.Transaction{ + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_INCOMING, + PaymentRequest: tests.MockLNClientTransaction.Invoice, + PaymentHash: tests.MockLNClientTransaction.PaymentHash, + Preimage: &mockPreimage, + AmountMsat: 1000, + Description: "small", + }) + svc.DB.Create(&db.Transaction{ + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_INCOMING, + PaymentRequest: tests.MockLNClientTransaction.Invoice, + PaymentHash: tests.MockLNClientTransaction.PaymentHash, + Preimage: &mockPreimage, + AmountMsat: 10000, + Description: "large", + }) + + transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) + minAmountMsat := uint64(5000) + + filteredTransactions, totalCount, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, false, false, svc.LNClient, nil, false, &ListTransactionsFilters{ + MinAmountMsat: &minAmountMsat, + }) + assert.NoError(t, err) + assert.Equal(t, uint64(1), totalCount) + require.Len(t, filteredTransactions, 1) + assert.Equal(t, "large", filteredTransactions[0].Description) +} + +func TestListTransactions_HideFailed(t *testing.T) { + ctx := context.TODO() + + svc, err := tests.CreateTestService(t) + require.NoError(t, err) + defer svc.Remove() + + mockPreimage := tests.MockLNClientTransaction.Preimage + svc.DB.Create(&db.Transaction{ + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_OUTGOING, + PaymentRequest: tests.MockLNClientTransaction.Invoice, + PaymentHash: tests.MockLNClientTransaction.PaymentHash, + Preimage: &mockPreimage, + AmountMsat: 123000, + Description: "settled", + UpdatedAt: time.Now().Add(2 * time.Minute), + }) + svc.DB.Create(&db.Transaction{ + State: constants.TRANSACTION_STATE_FAILED, + Type: constants.TRANSACTION_TYPE_OUTGOING, + PaymentRequest: tests.MockLNClientTransaction.Invoice, + PaymentHash: tests.MockLNClientTransaction.PaymentHash, + Preimage: &mockPreimage, + AmountMsat: 123000, + Description: "failed", + UpdatedAt: time.Now().Add(1 * time.Minute), + }) + svc.DB.Create(&db.Transaction{ + State: constants.TRANSACTION_STATE_PENDING, + Type: constants.TRANSACTION_TYPE_OUTGOING, + PaymentRequest: tests.MockLNClientTransaction.Invoice, + PaymentHash: tests.MockLNClientTransaction.PaymentHash, + Preimage: &mockPreimage, + AmountMsat: 123000, + Description: "pending", + }) + + transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) + + filteredTransactions, totalCount, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, true, false, svc.LNClient, nil, false, &ListTransactionsFilters{ + HideFailed: true, + }) + assert.NoError(t, err) + assert.Equal(t, uint64(2), totalCount) + require.Len(t, filteredTransactions, 2) + assert.Equal(t, "settled", filteredTransactions[0].Description) + assert.Equal(t, "pending", filteredTransactions[1].Description) +} + +func TestListTransactions_Type(t *testing.T) { + ctx := context.TODO() + + svc, err := tests.CreateTestService(t) + require.NoError(t, err) + defer svc.Remove() + + mockPreimage := tests.MockLNClientTransaction.Preimage + svc.DB.Create(&db.Transaction{ + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_INCOMING, + PaymentRequest: tests.MockLNClientTransaction.Invoice, + PaymentHash: tests.MockLNClientTransaction.PaymentHash, + Preimage: &mockPreimage, + AmountMsat: 123000, + Description: "received", + }) + svc.DB.Create(&db.Transaction{ + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_OUTGOING, + PaymentRequest: tests.MockLNClientTransaction.Invoice, + PaymentHash: tests.MockLNClientTransaction.PaymentHash, + Preimage: &mockPreimage, + AmountMsat: 123000, + Description: "sent", + }) + + transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) + transactionType := constants.TRANSACTION_TYPE_OUTGOING + + filteredTransactions, totalCount, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, false, false, svc.LNClient, nil, false, &ListTransactionsFilters{ + Type: &transactionType, + }) + assert.NoError(t, err) + assert.Equal(t, uint64(1), totalCount) + require.Len(t, filteredTransactions, 1) + assert.Equal(t, "sent", filteredTransactions[0].Description) +} + +func TestListTransactions_Search(t *testing.T) { + ctx := context.TODO() + + svc, err := tests.CreateTestService(t) + require.NoError(t, err) + defer svc.Remove() + + mockPreimage := tests.MockLNClientTransaction.Preimage + svc.DB.Create(&db.Transaction{ + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_OUTGOING, + PaymentRequest: "lnbc1coffee", + PaymentHash: "3086c621ecbef1ba99446fca8f484e2dbef77b28ee76a94ab8bb8b0e7f60a0f1", + Preimage: &mockPreimage, + AmountMsat: 123000, + Description: "Coffee shop", + }) + svc.DB.Create(&db.Transaction{ + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_INCOMING, + PaymentRequest: tests.MockLNClientTransaction.Invoice, + PaymentHash: tests.MockLNClientTransaction.PaymentHash, + Preimage: &mockPreimage, + AmountMsat: 123000, + Description: "Zap", + Metadata: datatypes.JSON(`{"user_labels":{"category":"Drinks"}}`), + }) + svc.DB.Create(&db.Transaction{ + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_OUTGOING, + PaymentRequest: "lnbc1discount", + PaymentHash: "af88b1571c1a0b2b1e8c05bf74e6a2f6b3a4f4a2be27077b1c5f5e2e4f6a8b9c", + Preimage: &mockPreimage, + AmountMsat: 123000, + Description: "50% discount", + }) + + transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) + + for _, testCase := range []struct { + searchTerm string + expectedDescriptions []string + }{ + {"COFFEE", []string{"Coffee shop"}}, + // exact payment hash + {tests.MockLNClientTransaction.PaymentHash, []string{"Zap"}}, + // full invoice is decoded and matched by its payment hash + {tests.MockLNClientTransaction.Invoice, []string{"Zap"}}, + // invoices are not matched by substring + {"lnbc1disc", []string{}}, + // partial payment hashes are not matched + {tests.MockLNClientTransaction.PaymentHash[:32], []string{}}, + {"drinks", []string{"Zap"}}, + {"category", []string{"Zap"}}, + {"50%", []string{"50% discount"}}, + {"nonexistent", []string{}}, + } { + filteredTransactions, totalCount, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, false, false, svc.LNClient, nil, false, &ListTransactionsFilters{ + SearchTerm: testCase.searchTerm, + }) + assert.NoError(t, err, "search: %s", testCase.searchTerm) + assert.Equal(t, uint64(len(testCase.expectedDescriptions)), totalCount, "search: %s", testCase.searchTerm) + require.Len(t, filteredTransactions, len(testCase.expectedDescriptions), "search: %s", testCase.searchTerm) + for i, description := range testCase.expectedDescriptions { + assert.Equal(t, description, filteredTransactions[i].Description, "search: %s", testCase.searchTerm) + } + } +} + func TestListTransactions_FromUntil(t *testing.T) { ctx := context.TODO() @@ -394,7 +592,7 @@ func TestListTransactions_FromUntil(t *testing.T) { transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) - incomingTransactions, totalCount, err := transactionsService.ListTransactions(ctx, uint64(time.Now().Add(4*time.Minute).Unix()), uint64(time.Now().Add(6*time.Minute).Unix()), 0, 0, false, false, nil, svc.LNClient, nil, false) + incomingTransactions, totalCount, err := transactionsService.ListTransactions(ctx, uint64(time.Now().Add(4*time.Minute).Unix()), uint64(time.Now().Add(6*time.Minute).Unix()), 0, 0, false, false, svc.LNClient, nil, false, nil) assert.NoError(t, err) assert.Equal(t, uint64(1), totalCount) assert.Equal(t, 1, len(incomingTransactions)) @@ -456,7 +654,7 @@ func TestListTransactions_FromUntilUnpaidOutgoing(t *testing.T) { transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) - incomingTransactions, totalCount, err := transactionsService.ListTransactions(ctx, uint64(time.Now().Add(4*time.Minute).Unix()), uint64(time.Now().Add(6*time.Minute).Unix()), 0, 0, true, false, nil, svc.LNClient, nil, false) + incomingTransactions, totalCount, err := transactionsService.ListTransactions(ctx, uint64(time.Now().Add(4*time.Minute).Unix()), uint64(time.Now().Add(6*time.Minute).Unix()), 0, 0, true, false, svc.LNClient, nil, false, nil) assert.NoError(t, err) assert.Equal(t, uint64(1), totalCount) assert.Equal(t, "second", incomingTransactions[0].Description) @@ -518,7 +716,7 @@ func TestListTransactions_FromUntilUnpaidIncoming(t *testing.T) { transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) - incomingTransactions, totalCount, err := transactionsService.ListTransactions(ctx, uint64(time.Now().Add(4*time.Minute).Unix()), uint64(time.Now().Add(6*time.Minute).Unix()), 0, 0, false, true, nil, svc.LNClient, nil, false) + incomingTransactions, totalCount, err := transactionsService.ListTransactions(ctx, uint64(time.Now().Add(4*time.Minute).Unix()), uint64(time.Now().Add(6*time.Minute).Unix()), 0, 0, false, true, svc.LNClient, nil, false, nil) assert.NoError(t, err) assert.Equal(t, uint64(1), totalCount) assert.Equal(t, "second", incomingTransactions[0].Description) diff --git a/transactions/notifications_test.go b/transactions/notifications_test.go index 44215c45..3f7d8e7f 100644 --- a/transactions/notifications_test.go +++ b/transactions/notifications_test.go @@ -314,7 +314,7 @@ func TestNotifications_FailedKnownPendingAndExistingFailedPayment(t *testing.T) }, }, map[string]interface{}{}) - transactions, totalCount, err := transactionsService.ListTransactions(ctx, uint64(0), uint64(0), uint64(0), uint64(0), true, false, nil, svc.LNClient, nil, false) + transactions, totalCount, err := transactionsService.ListTransactions(ctx, uint64(0), uint64(0), uint64(0), uint64(0), true, false, svc.LNClient, nil, false, nil) assert.NoError(t, err) assert.Equal(t, uint64(2), totalCount) for _, transaction := range transactions { @@ -348,7 +348,7 @@ func TestNotifications_SentAfterMarkedPaymentFailed(t *testing.T) { Properties: tests.MockLNClientTransaction, }, map[string]interface{}{}) - transactions, totalCount, err := transactionsService.ListTransactions(ctx, uint64(0), uint64(0), uint64(0), uint64(0), true, false, nil, svc.LNClient, nil, false) + transactions, totalCount, err := transactionsService.ListTransactions(ctx, uint64(0), uint64(0), uint64(0), uint64(0), true, false, svc.LNClient, nil, false, nil) assert.NoError(t, err) assert.Equal(t, uint64(1), totalCount) assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transactions[0].State) @@ -392,7 +392,7 @@ func TestNotifications_SentAfterMarkedTwoPaymentsFailed(t *testing.T) { Properties: tests.MockLNClientTransaction, }, map[string]interface{}{}) - transactions, totalCount, err := transactionsService.ListTransactions(ctx, uint64(0), uint64(0), uint64(0), uint64(0), true, false, nil, svc.LNClient, nil, false) + transactions, totalCount, err := transactionsService.ListTransactions(ctx, uint64(0), uint64(0), uint64(0), uint64(0), true, false, svc.LNClient, nil, false, nil) assert.NoError(t, err) assert.Equal(t, uint64(2), totalCount) assert.Equal(t, latestFailedTransaction.ID, transactions[0].ID) @@ -436,7 +436,7 @@ func TestNotifications_SentWithFailedAndPendingPayment(t *testing.T) { Properties: tests.MockLNClientTransaction, }, map[string]interface{}{}) - transactions, totalCount, err := transactionsService.ListTransactions(ctx, uint64(0), uint64(0), uint64(0), uint64(0), true, false, nil, svc.LNClient, nil, false) + transactions, totalCount, err := transactionsService.ListTransactions(ctx, uint64(0), uint64(0), uint64(0), uint64(0), true, false, svc.LNClient, nil, false, nil) assert.NoError(t, err) assert.Equal(t, uint64(2), totalCount) assert.Equal(t, pendingTransaction.ID, transactions[0].ID) diff --git a/transactions/transactions_service.go b/transactions/transactions_service.go index 23eec12b..407d07d7 100644 --- a/transactions/transactions_service.go +++ b/transactions/transactions_service.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "math" + "regexp" "slices" "strconv" "strings" @@ -38,7 +39,7 @@ type TransactionsService interface { events.EventSubscriber MakeInvoice(ctx context.Context, amountMsat uint64, description string, descriptionHash string, expiry uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint, throughNodePubkey *string) (*Transaction, error) LookupTransaction(ctx context.Context, paymentHash string, transactionType *string, lnClient lnclient.LNClient, appId *uint) (*Transaction, error) - ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaidOutgoing bool, unpaidIncoming bool, transactionType *string, lnClient lnclient.LNClient, appId *uint, forceFilterByAppId bool) (transactions []Transaction, totalCount uint64, err error) + ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaidOutgoing bool, unpaidIncoming bool, lnClient lnclient.LNClient, appId *uint, forceFilterByAppId bool, filters *ListTransactionsFilters) (transactions []Transaction, totalCount uint64, err error) SendPaymentSync(payReq string, amountMsat *uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) SendKeysend(amountMsat uint64, destination string, customRecords []lnclient.TLVRecord, preimage string, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) MakeHoldInvoice(ctx context.Context, amountMsat uint64, description string, descriptionHash string, expiry uint64, paymentHash string, minCltvExpiryDelta *uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) @@ -60,6 +61,27 @@ var balanceValidationLock = &sync.Mutex{} type Transaction = db.Transaction +type ListTransactionsFilters struct { + Type *string + MinAmountMsat *uint64 + HideFailed bool + SearchTerm string +} + +var paymentHashRegex = regexp.MustCompile("^[0-9a-f]{64}$") + +// escapeLikePattern makes a string match literally in a LIKE ... ESCAPE '\' +// clause by escaping the wildcard characters % and _. This is not an SQL +// injection concern (search terms are always passed as bound parameters); +// without it a term like "50%" would behave as a wildcard pattern. +// The backslash must be escaped first. +func escapeLikePattern(s string) string { + s = strings.ReplaceAll(s, `\`, `\\`) + s = strings.ReplaceAll(s, "%", `\%`) + s = strings.ReplaceAll(s, "_", `\_`) + return s +} + type Boostagram struct { AppName string `json:"app_name"` Name string `json:"name"` @@ -640,7 +662,7 @@ func (svc *transactionsService) LookupTransaction(ctx context.Context, paymentHa return &transaction, nil } -func (svc *transactionsService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaidOutgoing bool, unpaidIncoming bool, transactionType *string, lnClient lnclient.LNClient, appId *uint, forceFilterByAppId bool) (transactions []Transaction, totalCount uint64, err error) { +func (svc *transactionsService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaidOutgoing bool, unpaidIncoming bool, lnClient lnclient.LNClient, appId *uint, forceFilterByAppId bool, filters *ListTransactionsFilters) (transactions []Transaction, totalCount uint64, err error) { svc.checkUnsettledTransactions(ctx, lnClient) var isIsolatedApp bool @@ -672,8 +694,40 @@ func (svc *transactionsService) ListTransactions(ctx context.Context, from, unti tx = tx.Where("state = ? OR type = ?", constants.TRANSACTION_STATE_SETTLED, constants.TRANSACTION_TYPE_INCOMING) } - if transactionType != nil { - tx = tx.Where("type = ?", *transactionType) + if filters != nil { + if filters.Type != nil { + tx = tx.Where("type = ?", *filters.Type) + } + if filters.MinAmountMsat != nil { + tx = tx.Where("amount_msat >= ?", *filters.MinAmountMsat) + } + if filters.HideFailed { + tx = tx.Where("state != ?", constants.TRANSACTION_STATE_FAILED) + } + if searchTerm := strings.ToLower(strings.TrimSpace(filters.SearchTerm)); searchTerm != "" { + likePattern := "%" + escapeLikePattern(searchTerm) + "%" + labelsCondition := `EXISTS (SELECT 1 FROM json_each(transactions.metadata, '$.user_labels') AS user_labels WHERE LOWER(user_labels.key) LIKE ? ESCAPE '\' OR LOWER(user_labels.value) LIKE ? ESCAPE '\')` + if svc.db.Dialector.Name() == "postgres" { + labelsCondition = `EXISTS (SELECT 1 FROM jsonb_each_text((transactions.metadata->'user_labels')::jsonb) AS user_labels WHERE LOWER(user_labels.key) LIKE ? ESCAPE '\' OR LOWER(user_labels.value) LIKE ? ESCAPE '\')` + } + conditions := `LOWER(description) LIKE ? ESCAPE '\' OR ` + labelsCondition + args := []interface{}{likePattern, likePattern, likePattern} + + paymentHash := "" + if paymentHashRegex.MatchString(searchTerm) { + paymentHash = searchTerm + } else if strings.HasPrefix(searchTerm, "ln") { + if paymentRequest, err := decodepay.Decodepay(searchTerm); err == nil { + paymentHash = strings.ToLower(paymentRequest.PaymentHash) + } + } + if paymentHash != "" { + conditions += " OR payment_hash = ?" + args = append(args, paymentHash) + } + + tx = tx.Where(conditions, args...) + } } if from > 0 { diff --git a/wails/wails_handlers.go b/wails/wails_handlers.go index fc3897fe..5462a229 100644 --- a/wails/wails_handlers.go +++ b/wails/wails_handlers.go @@ -327,28 +327,37 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string offset := uint64(0) var appId *uint - // Extract limit and offset parameters - paramRegex := regexp.MustCompile(`[?&](limit|offset|appId)=([^&]+)`) - paramMatches := paramRegex.FindAllStringSubmatch(route, -1) - for _, match := range paramMatches { - switch match[1] { - case "limit": - if parsedLimit, err := strconv.ParseUint(match[2], 10, 64); err == nil { - limit = parsedLimit - } - case "offset": - if parsedOffset, err := strconv.ParseUint(match[2], 10, 64); err == nil { - offset = parsedOffset - } - case "appId": - if parsedAppId, err := strconv.ParseUint(match[2], 10, 64); err == nil { - var unsignedAppId = uint(parsedAppId) - appId = &unsignedAppId - } + parsedUrl, err := url.Parse(route) + if err != nil { + return WailsRequestRouterResponse{Body: nil, Error: "invalid route"} + } + query := parsedUrl.Query() + + if limitParam := query.Get("limit"); limitParam != "" { + if parsedLimit, err := strconv.ParseUint(limitParam, 10, 64); err == nil { + limit = parsedLimit } } - transactions, err := app.api.ListTransactions(ctx, appId, limit, offset) + if offsetParam := query.Get("offset"); offsetParam != "" { + if parsedOffset, err := strconv.ParseUint(offsetParam, 10, 64); err == nil { + offset = parsedOffset + } + } + + if appIdParam := query.Get("appId"); appIdParam != "" { + if parsedAppId, err := strconv.ParseUint(appIdParam, 10, 64); err == nil { + var unsignedAppId = uint(parsedAppId) + appId = &unsignedAppId + } + } + + filters, err := api.ParseListTransactionsFilters(query) + if err != nil { + return WailsRequestRouterResponse{Body: nil, Error: err.Error()} + } + + transactions, err := app.api.ListTransactions(ctx, appId, limit, offset, filters) if err != nil { return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } From d94f6933f5f603615bca9c39d44c7227d913f192 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:20:01 +0700 Subject: [PATCH 104/136] 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 --- frontend/src/components/ReceiveToLightning.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/components/ReceiveToLightning.tsx b/frontend/src/components/ReceiveToLightning.tsx index f85ea3ca..6a4b8761 100644 --- a/frontend/src/components/ReceiveToLightning.tsx +++ b/frontend/src/components/ReceiveToLightning.tsx @@ -9,7 +9,6 @@ import { Link } from "react-router"; import FirstChannelJitAlert from "src/components/FirstChannelJitAlert"; import Loading from "src/components/Loading"; import QRCode from "src/components/QRCode"; -import UserAvatar from "src/components/UserAvatar"; import { Accordion, AccordionContent, @@ -39,7 +38,6 @@ export function ReceiveToLightning() { value={me.lightning_address} className="h-auto w-full" frameType="lightning" - centerContent={} />

    From bdce8fe8d25f70eee5369afab4f84063abf5adcc Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Fri, 7 Aug 2026 12:39:07 +0700 Subject: [PATCH 105/136] fix: use scope constant in get_budget permission query (#2510) * 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 * fix: return error from get_budget on unexpected permission query failure Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- nip47/controllers/get_budget_controller.go | 18 +++++++++- .../controllers/get_budget_controller_test.go | 35 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/nip47/controllers/get_budget_controller.go b/nip47/controllers/get_budget_controller.go index ccf0c4d3..818546fd 100644 --- a/nip47/controllers/get_budget_controller.go +++ b/nip47/controllers/get_budget_controller.go @@ -2,10 +2,13 @@ package controllers import ( "context" + "errors" "github.com/getAlby/go-nostr" "github.com/getAlby/hub/db/queries" + "gorm.io/gorm" + "github.com/getAlby/hub/constants" "github.com/getAlby/hub/db" "github.com/getAlby/hub/logger" "github.com/getAlby/hub/nip47/models" @@ -26,8 +29,21 @@ func (controller *nip47Controller) HandleGetBudgetEvent(ctx context.Context, nip }).Debug("Getting budget") appPermission := db.AppPermission{} - controller.db.Where("app_id = ? AND scope = ?", app.ID, models.PAY_INVOICE_METHOD).First(&appPermission) + result := controller.db.Where("app_id = ? AND scope = ?", app.ID, constants.PAY_INVOICE_SCOPE).First(&appPermission) + if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) { + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + }).WithError(result.Error).Error("Failed to fetch pay_invoice permission") + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: mapNip47Error(result.Error), + }, nostr.Tags{}) + return + } + // On ErrRecordNotFound appPermission stays zero-valued and maxAmountSat == 0, + // which returns the same empty "no budget" response as a permission with no + // budget set. maxAmountSat := appPermission.MaxAmountSat if maxAmountSat == 0 { publishResponse(&models.Response{ diff --git a/nip47/controllers/get_budget_controller_test.go b/nip47/controllers/get_budget_controller_test.go index b39b7194..8a6e6d72 100644 --- a/nip47/controllers/get_budget_controller_test.go +++ b/nip47/controllers/get_budget_controller_test.go @@ -207,6 +207,41 @@ func TestHandleGetBudgetEvent_NoBudget(t *testing.T) { assert.Nil(t, publishedResponse.Error) } +func TestHandleGetBudgetEvent_DatabaseError(t *testing.T) { + ctx := context.TODO() + svc, err := tests.CreateTestService(t) + require.NoError(t, err) + defer svc.Remove() + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47GetBudgetJson), nip47Request) + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + // simulate a database failure that is not a record-not-found error + err = svc.DB.Exec("DROP TABLE app_permissions").Error + assert.NoError(t, err) + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + NewTestNip47Controller(svc). + HandleGetBudgetEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) + + assert.Nil(t, publishedResponse.Result) + require.NotNil(t, publishedResponse.Error) + assert.Equal(t, constants.ERROR_INTERNAL, publishedResponse.Error.Code) +} + func TestHandleGetBudgetEvent_NoPayInvoicePermission(t *testing.T) { ctx := context.TODO() svc, err := tests.CreateTestService(t) From 6175489cb0dc8513b00e59f50e69fcd18bf233b4 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Fri, 7 Aug 2026 17:55:38 +0700 Subject: [PATCH 106/136] chore: remove unused argon2-wasm-esm dependency (#2508) Co-authored-by: Claude Fable 5 --- frontend/package.json | 1 - frontend/tsconfig.json | 2 +- frontend/types/argon2-wasm-esm.d.ts | 19 ------------------- frontend/yarn.lock | 5 ----- 4 files changed, 1 insertion(+), 26 deletions(-) delete mode 100644 frontend/types/argon2-wasm-esm.d.ts diff --git a/frontend/package.json b/frontend/package.json index dda6eaaf..b46ba73b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,7 +27,6 @@ "@getalby/sdk": "^8.0.3", "@scure/bip39": "^2.2.0", "@stepperize/react": "^6.1.0", - "argon2-wasm-esm": "^1.0.3", "bitcoin-address-validation": "^3.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 1ce5430a..94765c8d 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -26,6 +26,6 @@ "react": ["./node_modules/@types/react"] } }, - "include": ["src", "types"], + "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/frontend/types/argon2-wasm-esm.d.ts b/frontend/types/argon2-wasm-esm.d.ts deleted file mode 100644 index 87bc1c92..00000000 --- a/frontend/types/argon2-wasm-esm.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -declare module "argon2-wasm-esm" { - export interface Argon2Options { - pass: string; - salt: Uint8Array; - time: number; - mem: number; - hashLen: number; - parallelism: number; - type: number; - } - - export interface Argon2Result { - hash: Uint8Array; - hashHex: string; - encoded: string; - } - - export function hash(options: Argon2Options): Promise; -} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 36109ac5..8dddf83d 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2982,11 +2982,6 @@ ansi-styles@^6.2.1: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== -argon2-wasm-esm@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/argon2-wasm-esm/-/argon2-wasm-esm-1.0.3.tgz#cdd6602b00b78b6d4fe8bf3c966c4c705e683e21" - integrity sha512-tRwVl0LO0Tl4rJRwTAMy+PfA7cqZb6jGB2o5lIrPqF2PFJVeNILeo4zAdFgow+MvggDWsujrrbYo14+CWF0dmQ== - argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" From 5f4e52bd884cd5f6e356e9ce0585c77cd64f591d Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Sat, 8 Aug 2026 13:38:28 +0700 Subject: [PATCH 107/136] 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 * 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 * 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 --------- Co-authored-by: Claude Fable 5 --- tests/mock_ln_client.go | 9 + transactions/app_payments_test.go | 43 ++ transactions/keysend_test.go | 29 ++ transactions/payments_test.go | 84 ++-- transactions/transactions_service.go | 617 ++++++++++++++++----------- 5 files changed, 501 insertions(+), 281 deletions(-) diff --git a/tests/mock_ln_client.go b/tests/mock_ln_client.go index 35c775d2..e935b385 100644 --- a/tests/mock_ln_client.go +++ b/tests/mock_ln_client.go @@ -81,6 +81,8 @@ type MockLn struct { MakeInvoiceErrors []error PayInvoiceResponses []*lnclient.PayInvoiceResponse PayInvoiceErrors []error + PayKeysendResponses []*lnclient.PayKeysendResponse + PayKeysendErrors []error PaymentDelay *time.Duration Pubkey string MockTransaction *lnclient.Transaction @@ -110,6 +112,13 @@ func (mln *MockLn) SendPaymentSync(payReq string, amountMsat *uint64) (*lnclient } func (mln *MockLn) SendKeysend(amountMsat uint64, destination string, custom_records []lnclient.TLVRecord, preimage string) (*lnclient.PayKeysendResponse, error) { + if len(mln.PayKeysendResponses) > 0 { + response := mln.PayKeysendResponses[0] + err := mln.PayKeysendErrors[0] + mln.PayKeysendResponses = mln.PayKeysendResponses[1:] + mln.PayKeysendErrors = mln.PayKeysendErrors[1:] + return response, err + } return &lnclient.PayKeysendResponse{ FeeMsat: 1, }, nil diff --git a/transactions/app_payments_test.go b/transactions/app_payments_test.go index 552ef6dc..b989407a 100644 --- a/transactions/app_payments_test.go +++ b/transactions/app_payments_test.go @@ -62,6 +62,49 @@ func TestSendPaymentSync_App_WithPermission(t *testing.T) { assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId) } +func TestMarkSettled_App_BudgetWarning(t *testing.T) { + svc, err := tests.CreateTestService(t) + require.NoError(t, err) + defer svc.Remove() + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + Scope: constants.PAY_INVOICE_SCOPE, + MaxAmountSat: 100, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + // settling this payment pushes the app over 80% of its 100 sat budget + dbTransaction := db.Transaction{ + AppId: &app.ID, + State: constants.TRANSACTION_STATE_PENDING, + Type: constants.TRANSACTION_TYPE_OUTGOING, + PaymentHash: tests.MockLNClientTransaction.PaymentHash, + AmountMsat: 90000, + } + svc.DB.Create(&dbTransaction) + + mockEventConsumer := tests.NewMockEventConsumer() + svc.EventPublisher.RegisterSubscriber(mockEventConsumer) + transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) + _, err = transactionsService.markTransactionSettled(&dbTransaction, "test", 0, false) + + assert.NoError(t, err) + consumedEvents := mockEventConsumer.GetConsumedEvents() + assert.Equal(t, 2, len(consumedEvents)) + eventNames := []string{} + for _, consumedEvent := range consumedEvents { + eventNames = append(eventNames, consumedEvent.Event) + } + assert.Contains(t, eventNames, "nwc_payment_sent") + assert.Contains(t, eventNames, "nwc_budget_warning") +} + func TestSendPaymentSync_App_BudgetExceeded(t *testing.T) { svc, err := tests.CreateTestService(t) require.NoError(t, err) diff --git a/transactions/keysend_test.go b/transactions/keysend_test.go index 42f21d86..ba50db49 100644 --- a/transactions/keysend_test.go +++ b/transactions/keysend_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "encoding/json" + "errors" "strconv" "testing" @@ -47,6 +48,34 @@ func TestSendKeysend(t *testing.T) { settledTransaction := mockEventConsumer.GetConsumedEvents()[0].Properties.(*db.Transaction) assert.Equal(t, transaction, settledTransaction) } +func TestSendKeysend_FailedRemovesFeeReserve(t *testing.T) { + svc, err := tests.CreateTestService(t) + require.NoError(t, err) + defer svc.Remove() + + svc.LNClient.(*tests.MockLn).PayKeysendErrors = append(svc.LNClient.(*tests.MockLn).PayKeysendErrors, errors.New("Some error")) + svc.LNClient.(*tests.MockLn).PayKeysendResponses = append(svc.LNClient.(*tests.MockLn).PayKeysendResponses, nil) + + mockEventConsumer := tests.NewMockEventConsumer() + svc.EventPublisher.RegisterSubscriber(mockEventConsumer) + + transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) + transaction, err := transactionsService.SendKeysend(uint64(1000), "fake destination", nil, "", svc.LNClient, nil, nil) + + assert.Error(t, err) + assert.Nil(t, transaction) + + failedTransaction := db.Transaction{} + require.NoError(t, svc.DB.Where("type = ? AND state = ?", constants.TRANSACTION_TYPE_OUTGOING, constants.TRANSACTION_STATE_FAILED).First(&failedTransaction).Error) + + assert.Equal(t, uint64(1000), failedTransaction.AmountMsat) + assert.Zero(t, failedTransaction.FeeReserveMsat) + assert.Equal(t, "Some error", failedTransaction.FailureReason) + + assert.Equal(t, 1, len(mockEventConsumer.GetConsumedEvents())) + assert.Equal(t, "nwc_payment_failed", mockEventConsumer.GetConsumedEvents()[0].Event) +} + func TestSendKeysend_CustomPreimage(t *testing.T) { svc, err := tests.CreateTestService(t) require.NoError(t, err) diff --git a/transactions/payments_test.go b/transactions/payments_test.go index ef2a675f..ef8a5037 100644 --- a/transactions/payments_test.go +++ b/transactions/payments_test.go @@ -12,7 +12,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gorm.io/gorm" "github.com/getAlby/hub/constants" "github.com/getAlby/hub/db" @@ -181,10 +180,7 @@ func TestMarkSettled_Sent(t *testing.T) { mockEventConsumer := tests.NewMockEventConsumer() svc.EventPublisher.RegisterSubscriber(mockEventConsumer) transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) - err = svc.DB.Transaction(func(tx *gorm.DB) error { - _, err = transactionsService.markTransactionSettled(tx, &dbTransaction, "test", 0, false) - return err - }) + _, err = transactionsService.markTransactionSettled(&dbTransaction, "test", 0, false) assert.NoError(t, err) assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, dbTransaction.State) @@ -212,29 +208,36 @@ func TestMarkSettled_Twice(t *testing.T) { transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) var wg sync.WaitGroup n := 10 + markErrors := make([]error, n) wg.Add(n) - for range n { + for i := range n { go func() { defer wg.Done() - err = svc.DB.Transaction(func(tx *gorm.DB) error { - time.Sleep(time.Duration(n) * 10 * time.Millisecond) - _, err = transactionsService.markTransactionSettled(tx, &dbTransaction, "test", 0, false) - time.Sleep(time.Duration(n) * 10 * time.Millisecond) - return err - }) - require.NoError(t, err) + // load an independent copy so goroutines don't share the struct + var transactionCopy db.Transaction + if err := svc.DB.First(&transactionCopy, dbTransaction.ID).Error; err != nil { + markErrors[i] = err + return + } + _, markErrors[i] = transactionsService.markTransactionSettled(&transactionCopy, "test", 0, false) }() } wg.Wait() + for _, markError := range markErrors { + assert.NoError(t, markError) + } + // ensure we only mark transaction settled once and only fire // settled notifications once - assert.NoError(t, err) - assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, dbTransaction.State) + var reloadedTransaction db.Transaction + require.NoError(t, svc.DB.First(&reloadedTransaction, dbTransaction.ID).Error) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, reloadedTransaction.State) assert.Equal(t, 1, len(mockEventConsumer.GetConsumedEvents())) assert.Equal(t, "nwc_payment_sent", mockEventConsumer.GetConsumedEvents()[0].Event) settledTransaction := mockEventConsumer.GetConsumedEvents()[0].Properties.(*db.Transaction) - assert.Equal(t, &dbTransaction, settledTransaction) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, settledTransaction.State) + assert.Equal(t, dbTransaction.PaymentHash, settledTransaction.PaymentHash) } func TestMarkSettled_Received(t *testing.T) { @@ -253,10 +256,7 @@ func TestMarkSettled_Received(t *testing.T) { mockEventConsumer := tests.NewMockEventConsumer() svc.EventPublisher.RegisterSubscriber(mockEventConsumer) transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) - err = svc.DB.Transaction(func(tx *gorm.DB) error { - _, err = transactionsService.markTransactionSettled(tx, &dbTransaction, "test", 0, false) - return err - }) + _, err = transactionsService.markTransactionSettled(&dbTransaction, "test", 0, false) assert.NoError(t, err) assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, dbTransaction.State) @@ -284,10 +284,7 @@ func TestDoNotMarkSettledTwice(t *testing.T) { mockEventConsumer := tests.NewMockEventConsumer() svc.EventPublisher.RegisterSubscriber(mockEventConsumer) transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) - err = svc.DB.Transaction(func(tx *gorm.DB) error { - _, err = transactionsService.markTransactionSettled(tx, &dbTransaction, "test", 0, false) - return err - }) + _, err = transactionsService.markTransactionSettled(&dbTransaction, "test", 0, false) assert.NoError(t, err) assert.Zero(t, len(mockEventConsumer.GetConsumedEvents())) @@ -309,11 +306,10 @@ func TestMarkFailed(t *testing.T) { mockEventConsumer := tests.NewMockEventConsumer() svc.EventPublisher.RegisterSubscriber(mockEventConsumer) transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) - err = svc.DB.Transaction(func(tx *gorm.DB) error { - return transactionsService.markPaymentFailed(tx, &dbTransaction, "some routing error") - }) + markedFailed, err := transactionsService.markPaymentFailed(&dbTransaction, "some routing error") assert.NoError(t, err) + assert.True(t, markedFailed) assert.Equal(t, constants.TRANSACTION_STATE_FAILED, dbTransaction.State) assert.Equal(t, 1, len(mockEventConsumer.GetConsumedEvents())) assert.Equal(t, "nwc_payment_failed", mockEventConsumer.GetConsumedEvents()[0].Event) @@ -340,15 +336,43 @@ func TestDoNotMarkFailedTwice(t *testing.T) { mockEventConsumer := tests.NewMockEventConsumer() svc.EventPublisher.RegisterSubscriber(mockEventConsumer) transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) - err = svc.DB.Transaction(func(tx *gorm.DB) error { - return transactionsService.markPaymentFailed(tx, &dbTransaction, "some routing error") - }) + markedFailed, err := transactionsService.markPaymentFailed(&dbTransaction, "some routing error") assert.NoError(t, err) + assert.False(t, markedFailed) assert.Equal(t, updatedAt, dbTransaction.UpdatedAt) assert.Zero(t, len(mockEventConsumer.GetConsumedEvents())) } +func TestDoNotMarkSettledPaymentFailed(t *testing.T) { + svc, err := tests.CreateTestService(t) + require.NoError(t, err) + defer svc.Remove() + + settledAt := time.Now() + dbTransaction := db.Transaction{ + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_OUTGOING, + PaymentHash: tests.MockLNClientTransaction.PaymentHash, + AmountMsat: 123000, + SettledAt: &settledAt, + } + svc.DB.Create(&dbTransaction) + + mockEventConsumer := tests.NewMockEventConsumer() + svc.EventPublisher.RegisterSubscriber(mockEventConsumer) + transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher) + markedFailed, err := transactionsService.markPaymentFailed(&dbTransaction, "some routing error") + + assert.Error(t, err) + assert.False(t, markedFailed) + + var reloadedTransaction db.Transaction + require.NoError(t, svc.DB.First(&reloadedTransaction, dbTransaction.ID).Error) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, reloadedTransaction.State) + assert.Zero(t, len(mockEventConsumer.GetConsumedEvents())) +} + func TestSendPaymentSync_FailedRemovesFeeReserve(t *testing.T) { svc, err := tests.CreateTestService(t) require.NoError(t, err) diff --git a/transactions/transactions_service.go b/transactions/transactions_service.go index 407d07d7..d0497676 100644 --- a/transactions/transactions_service.go +++ b/transactions/transactions_service.go @@ -444,19 +444,17 @@ func (svc *transactionsService) SendPaymentSync(payReq string, amountMsat *uint6 "bolt11": payReq, }).WithError(err).Error("Failed to send payment") - svc.db.Transaction(func(tx *gorm.DB) error { - return svc.markPaymentFailed(tx, &dbTransaction, err.Error()) - }) + if _, markFailedErr := svc.markPaymentFailed(&dbTransaction, err.Error()); markFailedErr != nil { + logger.Logger.WithFields(logrus.Fields{ + "bolt11": payReq, + }).WithError(markFailedErr).Error("Failed to mark payment as failed") + } return nil, err } // the payment definitely succeeded - var settledTransaction *db.Transaction - err = svc.db.Transaction(func(tx *gorm.DB) error { - settledTransaction, err = svc.markTransactionSettled(tx, &dbTransaction, response.Preimage, response.FeeMsat, selfPayment) - return err - }) + settledTransaction, err := svc.markTransactionSettled(&dbTransaction, response.Preimage, response.FeeMsat, selfPayment) if err != nil { return nil, err } @@ -579,27 +577,18 @@ func (svc *transactionsService) SendKeysend(amountMsat uint64, destination strin "amount_msat": amountMsat, }).WithError(err).Error("Failed to send payment") - dbErr := svc.db.Model(&dbTransaction).Updates(&db.Transaction{ - PaymentHash: paymentHash, - State: constants.TRANSACTION_STATE_FAILED, - }).Error - if dbErr != nil { + if _, markFailedErr := svc.markPaymentFailed(&dbTransaction, err.Error()); markFailedErr != nil { logger.Logger.WithFields(logrus.Fields{ "destination": destination, "amount_msat": amountMsat, - }).WithError(dbErr).Error("Failed to update DB transaction") + }).WithError(markFailedErr).Error("Failed to mark payment as failed") } return nil, err } // the payment definitely succeeded - var settledTransaction *db.Transaction - err = svc.db.Transaction(func(tx *gorm.DB) error { - settledTransaction, err = svc.markTransactionSettled(tx, &dbTransaction, preimage, payKeysendResponse.FeeMsat, selfPayment) - return err - }) - + settledTransaction, err := svc.markTransactionSettled(&dbTransaction, preimage, payKeysendResponse.FeeMsat, selfPayment) if err != nil { return nil, err } @@ -795,11 +784,7 @@ func (svc *transactionsService) checkUnsettledTransaction(ctx context.Context, t } // update transaction state if lnClientTransaction.SettledAt != nil { - err = svc.db.Transaction(func(tx *gorm.DB) error { - _, err = svc.markTransactionSettled(tx, transaction, lnClientTransaction.Preimage, uint64(lnClientTransaction.FeesPaidMsat), false) - return err - }) - + _, err = svc.markTransactionSettled(transaction, lnClientTransaction.Preimage, uint64(lnClientTransaction.FeesPaidMsat), false) if err != nil { logger.Logger.WithError(err).Error("Failed to mark payment sent when checking unsettled transaction") } @@ -816,73 +801,72 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events. } var dbTransaction db.Transaction - err := svc.db.Transaction(func(tx *gorm.DB) error { - - result := tx.Limit(1).Find(&dbTransaction, &db.Transaction{ - Type: constants.TRANSACTION_TYPE_INCOMING, - PaymentHash: lnClientTransaction.PaymentHash, - }) - - if result.RowsAffected == 0 { - var appId *uint - description := lnClientTransaction.Description - var metadataBytes []byte - var boostagramBytes []byte - if lnClientTransaction.Metadata != nil { - var err error - metadataBytes, err = json.Marshal(lnClientTransaction.Metadata) - if err != nil { - logger.Logger.WithError(err).Error("Failed to serialize transaction metadata") - return err - } - - var customRecords []lnclient.TLVRecord - customRecords, _ = lnClientTransaction.Metadata["tlv_records"].([]lnclient.TLVRecord) - boostagramBytes = svc.getBoostagramBytesFromCustomRecords(customRecords) - extractedDescription := svc.getDescriptionFromCustomRecords(customRecords) - if extractedDescription != "" { - description = extractedDescription - } - // find app by custom key/value records - appId = svc.getAppIdFromCustomRecords(customRecords, tx) - } - var expiresAt *time.Time - if lnClientTransaction.ExpiresAt != nil { - expiresAtValue := time.Unix(*lnClientTransaction.ExpiresAt, 0) - expiresAt = &expiresAtValue - } - dbTransaction = db.Transaction{ - Type: constants.TRANSACTION_TYPE_INCOMING, - AmountMsat: uint64(lnClientTransaction.AmountMsat), - PaymentRequest: lnClientTransaction.Invoice, - PaymentHash: lnClientTransaction.PaymentHash, - Description: description, - DescriptionHash: lnClientTransaction.DescriptionHash, - ExpiresAt: expiresAt, - Metadata: datatypes.JSON(metadataBytes), - Boostagram: datatypes.JSON(boostagramBytes), - AppId: appId, - } - err := tx.Create(&dbTransaction).Error - if err != nil { - logger.Logger.WithFields(logrus.Fields{ - "payment_hash": lnClientTransaction.PaymentHash, - }).WithError(err).Error("Failed to create transaction") - return err - } - } - - _, err := svc.markTransactionSettled(tx, &dbTransaction, lnClientTransaction.Preimage, uint64(lnClientTransaction.FeesPaidMsat), false) - return err + result := svc.db.Limit(1).Find(&dbTransaction, &db.Transaction{ + Type: constants.TRANSACTION_TYPE_INCOMING, + PaymentHash: lnClientTransaction.PaymentHash, }) - if err != nil { + if result.Error != nil { logger.Logger.WithFields(logrus.Fields{ "payment_hash": lnClientTransaction.PaymentHash, - }).WithError(err).Error("Failed to execute DB transaction") + }).WithError(result.Error).Error("Failed to find transaction") return } + if result.RowsAffected == 0 { + var appId *uint + description := lnClientTransaction.Description + var metadataBytes []byte + var boostagramBytes []byte + if lnClientTransaction.Metadata != nil { + var err error + metadataBytes, err = json.Marshal(lnClientTransaction.Metadata) + if err != nil { + logger.Logger.WithError(err).Error("Failed to serialize transaction metadata") + return + } + + var customRecords []lnclient.TLVRecord + customRecords, _ = lnClientTransaction.Metadata["tlv_records"].([]lnclient.TLVRecord) + boostagramBytes = svc.getBoostagramBytesFromCustomRecords(customRecords) + extractedDescription := svc.getDescriptionFromCustomRecords(customRecords) + if extractedDescription != "" { + description = extractedDescription + } + // find app by custom key/value records + appId = svc.getAppIdFromCustomRecords(customRecords, svc.db) + } + var expiresAt *time.Time + if lnClientTransaction.ExpiresAt != nil { + expiresAtValue := time.Unix(*lnClientTransaction.ExpiresAt, 0) + expiresAt = &expiresAtValue + } + dbTransaction = db.Transaction{ + Type: constants.TRANSACTION_TYPE_INCOMING, + AmountMsat: uint64(lnClientTransaction.AmountMsat), + PaymentRequest: lnClientTransaction.Invoice, + PaymentHash: lnClientTransaction.PaymentHash, + Description: description, + DescriptionHash: lnClientTransaction.DescriptionHash, + ExpiresAt: expiresAt, + Metadata: datatypes.JSON(metadataBytes), + Boostagram: datatypes.JSON(boostagramBytes), + AppId: appId, + } + if _, err := svc.createSettledTransactionFromNotification(&dbTransaction, lnClientTransaction.Preimage, uint64(lnClientTransaction.FeesPaidMsat), false); err != nil { + logger.Logger.WithFields(logrus.Fields{ + "payment_hash": lnClientTransaction.PaymentHash, + }).WithError(err).Error("Failed to create settled transaction") + } + return + } + + if _, err := svc.markTransactionSettled(&dbTransaction, lnClientTransaction.Preimage, uint64(lnClientTransaction.FeesPaidMsat), false); err != nil { + logger.Logger.WithFields(logrus.Fields{ + "payment_hash": lnClientTransaction.PaymentHash, + }).WithError(err).Error("Failed to mark transaction as settled") + } + case "nwc_lnclient_hold_invoice_accepted": lnClientTransaction, ok := event.Properties.(*lnclient.Transaction) if !ok { @@ -903,78 +887,79 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events. } var dbTransaction db.Transaction - err := svc.db.Transaction(func(tx *gorm.DB) error { - // first lookup by pending - result := tx.Limit(1).Find(&dbTransaction, &db.Transaction{ + // first lookup by pending + result := svc.db.Limit(1).Find(&dbTransaction, &db.Transaction{ + Type: constants.TRANSACTION_TYPE_OUTGOING, + State: constants.TRANSACTION_STATE_PENDING, + PaymentHash: lnClientTransaction.PaymentHash, + }) + + if result.Error != nil { + logger.Logger.WithFields(logrus.Fields{ + "payment_hash": lnClientTransaction.PaymentHash, + }).WithError(result.Error).Error("Failed to find transaction") + return + } + + if result.RowsAffected == 0 { + // if no pending payment was found, lookup by failed, latest updated first + result := svc.db.Limit(1).Order("updated_at DESC").Find(&dbTransaction, &db.Transaction{ Type: constants.TRANSACTION_TYPE_OUTGOING, - State: constants.TRANSACTION_STATE_PENDING, + State: constants.TRANSACTION_STATE_FAILED, PaymentHash: lnClientTransaction.PaymentHash, }) if result.Error != nil { - return result.Error + logger.Logger.WithFields(logrus.Fields{ + "payment_hash": lnClientTransaction.PaymentHash, + }).WithError(result.Error).Error("Failed to find transaction") + return } if result.RowsAffected == 0 { - // if no pending payment was found, lookup by failed, latest updated first - result := tx.Limit(1).Order("updated_at DESC").Find(&dbTransaction, &db.Transaction{ + result := svc.db.Limit(1).Find(&dbTransaction, &db.Transaction{ Type: constants.TRANSACTION_TYPE_OUTGOING, - State: constants.TRANSACTION_STATE_FAILED, PaymentHash: lnClientTransaction.PaymentHash, }) if result.Error != nil { - return result.Error + logger.Logger.WithFields(logrus.Fields{ + "payment_hash": lnClientTransaction.PaymentHash, + }).WithError(result.Error).Error("Failed to find transaction") + return } if result.RowsAffected == 0 { - result := tx.Limit(1).Find(&dbTransaction, &db.Transaction{ - Type: constants.TRANSACTION_TYPE_OUTGOING, - PaymentHash: lnClientTransaction.PaymentHash, - }) - - if result.Error != nil { - return result.Error + dbTransaction = db.Transaction{ + Type: constants.TRANSACTION_TYPE_OUTGOING, + AmountMsat: uint64(lnClientTransaction.AmountMsat), + FeeReserveMsat: 0, + PaymentRequest: lnClientTransaction.Invoice, + PaymentHash: lnClientTransaction.PaymentHash, + Description: lnClientTransaction.Description, + DescriptionHash: lnClientTransaction.DescriptionHash, } - if result.RowsAffected == 0 { - dbTransaction = db.Transaction{ - Type: constants.TRANSACTION_TYPE_OUTGOING, - State: constants.TRANSACTION_STATE_PENDING, - AmountMsat: uint64(lnClientTransaction.AmountMsat), - FeeReserveMsat: 0, - PaymentRequest: lnClientTransaction.Invoice, - PaymentHash: lnClientTransaction.PaymentHash, - Description: lnClientTransaction.Description, - DescriptionHash: lnClientTransaction.DescriptionHash, - } - - if lnClientTransaction.ExpiresAt != nil { - expiresAtValue := time.Unix(*lnClientTransaction.ExpiresAt, 0) - dbTransaction.ExpiresAt = &expiresAtValue - } - - err := tx.Create(&dbTransaction).Error - if err != nil { - logger.Logger.WithFields(logrus.Fields{ - "payment_hash": lnClientTransaction.PaymentHash, - }).WithError(err).Error("Failed to create outgoing transaction") - return err - } + if lnClientTransaction.ExpiresAt != nil { + expiresAtValue := time.Unix(*lnClientTransaction.ExpiresAt, 0) + dbTransaction.ExpiresAt = &expiresAtValue } + + if _, err := svc.createSettledTransactionFromNotification(&dbTransaction, lnClientTransaction.Preimage, uint64(lnClientTransaction.FeesPaidMsat), false); err != nil { + logger.Logger.WithFields(logrus.Fields{ + "payment_hash": lnClientTransaction.PaymentHash, + }).WithError(err).Error("Failed to create settled transaction") + } + return } } + } - _, err := svc.markTransactionSettled(tx, &dbTransaction, lnClientTransaction.Preimage, uint64(lnClientTransaction.FeesPaidMsat), false) - return err - }) - - if err != nil { + if _, err := svc.markTransactionSettled(&dbTransaction, lnClientTransaction.Preimage, uint64(lnClientTransaction.FeesPaidMsat), false); err != nil { logger.Logger.WithFields(logrus.Fields{ "payment_hash": lnClientTransaction.PaymentHash, }).WithError(err).Error("Failed to update transaction") - return } case "nwc_lnclient_payment_failed": paymentFailedAsyncProperties, ok := event.Properties.(*lnclient.PaymentFailedEventProperties) @@ -997,9 +982,11 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events. return } - svc.db.Transaction(func(tx *gorm.DB) error { - return svc.markPaymentFailed(tx, &dbTransaction, paymentFailedAsyncProperties.Reason) - }) + if _, err := svc.markPaymentFailed(&dbTransaction, paymentFailedAsyncProperties.Reason); err != nil { + logger.Logger.WithFields(logrus.Fields{ + "payment_hash": lnClientTransaction.PaymentHash, + }).WithError(err).Error("Failed to mark payment as failed") + } } } @@ -1094,11 +1081,7 @@ func (svc *transactionsService) interceptSelfPayment(paymentRequest string, paym return nil, errors.New("preimage is not set on transaction. Self payments not supported") } - err := svc.db.Transaction(func(tx *gorm.DB) error { - _, err := svc.markTransactionSettled(tx, &incomingTransaction, *incomingTransaction.Preimage, uint64(0), true) - return err - }) - + _, err := svc.markTransactionSettled(&incomingTransaction, *incomingTransaction.Preimage, uint64(0), true) if err != nil { return nil, err } @@ -1356,18 +1339,12 @@ func (svc *transactionsService) SettleHoldInvoice(ctx context.Context, preimage return nil, err } - var settledTransaction *db.Transaction - err = svc.db.Transaction(func(tx *gorm.DB) error { - var err error - settledTransaction, err = svc.markTransactionSettled(tx, &dbTransaction, preimage, 0, dbTransaction.SelfPayment) - return err - }) - + settledTransaction, err := svc.markTransactionSettled(&dbTransaction, preimage, 0, dbTransaction.SelfPayment) if err != nil { logger.Logger.WithFields(logrus.Fields{ "payment_hash": paymentHash, "preimage": preimage, - }).WithError(err).Error("Failed DB transaction while settling hold invoice") + }).WithError(err).Error("Failed to mark hold invoice as settled") return nil, err } @@ -1399,37 +1376,23 @@ func (svc *transactionsService) CancelHoldInvoice(ctx context.Context, paymentHa } } - err := svc.db.Transaction(func(tx *gorm.DB) error { - var dbTransaction db.Transaction - result := tx.Limit(1).Find(&dbTransaction, &db.Transaction{ - Type: constants.TRANSACTION_TYPE_INCOMING, - State: constants.TRANSACTION_STATE_ACCEPTED, - PaymentHash: paymentHash, - }) - - if result.Error != nil { - logger.Logger.WithFields(logrus.Fields{ - "payment_hash": paymentHash, - }).WithError(result.Error).Error("Failed to find accepted hold invoice in DB for cancellation") - return result.Error - } - if result.RowsAffected == 0 { - logger.Logger.WithFields(logrus.Fields{ - "payment_hash": paymentHash, - }).Warn("No accepted hold invoice found in DB to mark as failed due to cancellation") - return NewNotFoundError() - } - - return svc.markPaymentFailed(tx, &dbTransaction, "Hold invoice was cancelled") - }) - + markedFailed, err := svc.markPaymentFailed(&dbTransaction, "Hold invoice was cancelled") if err != nil { logger.Logger.WithFields(logrus.Fields{ "payment_hash": paymentHash, - }).WithError(err).Error("Failed DB transaction while canceling hold invoice") + }).WithError(err).Error("Failed to mark hold invoice as failed due to cancellation") return err } + if !markedFailed { + // a concurrent cancellation already marked the invoice as failed and + // published the canceled event + logger.Logger.WithFields(logrus.Fields{ + "payment_hash": paymentHash, + }).Info("Hold invoice was already marked as failed") + return nil + } + logger.Logger.WithFields(logrus.Fields{ "payment_hash": paymentHash, }).Info("Marked hold invoice as failed in DB due to cancellation") @@ -1501,61 +1464,171 @@ func (svc *transactionsService) SetTransactionUserLabels(ctx context.Context, id return svc.SetTransactionMetadata(ctx, id, metadata) } -func (svc *transactionsService) markTransactionSettled(tx *gorm.DB, dbTransaction *db.Transaction, preimage string, feeMsat uint64, selfPayment bool) (*db.Transaction, error) { +// markTransactionSettled marks an existing transaction as settled in its own +// database transaction and publishes the corresponding events after it +// commits, so subscribers never observe uncommitted state. +func (svc *transactionsService) markTransactionSettled(dbTransaction *db.Transaction, preimage string, feeMsat uint64, selfPayment bool) (*db.Transaction, error) { if preimage == "" { return nil, errors.New("no preimage in payment") } - if tx.Dialector.Name() == "postgres" { - // lock based on payment hash to ensure we only mark one transaction as settled - // (in sqlite transactions are serializable by default) - transactionsWithPaymentHash := []db.Transaction{} - tx.Where(&db.Transaction{ - PaymentHash: dbTransaction.PaymentHash, - }).Clauses(clause.Locking{Strength: "UPDATE"}).Find(&transactionsWithPaymentHash) + var settledTransaction *db.Transaction + var eventsToPublish []*events.Event + err := svc.db.Transaction(func(tx *gorm.DB) error { + existingSettledTransaction, err := svc.findSettledTransaction(tx, dbTransaction) + if err != nil { + return err + } + if existingSettledTransaction != nil { + logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Debug("payment already marked as sent") + settledTransaction = existingSettledTransaction + return nil + } + + settledAt := time.Now() + err = tx.Model(dbTransaction).Updates(map[string]interface{}{ + "State": constants.TRANSACTION_STATE_SETTLED, + "Preimage": &preimage, + "FeeMsat": feeMsat, + "FeeReserveMsat": 0, + "SettledAt": &settledAt, + "SelfPayment": selfPayment, + }).Error + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "payment_hash": dbTransaction.PaymentHash, + }).WithError(err).Error("Failed to update DB transaction") + return err + } + + logger.Logger.WithFields(logrus.Fields{ + "payment_hash": dbTransaction.PaymentHash, + "type": dbTransaction.Type, + }).Info("Marked transaction as settled") + + settledTransaction = dbTransaction + eventsToPublish = svc.afterTransactionSettled(tx, dbTransaction, &settledAt) + return nil + }) + if err != nil { + return nil, err + } + svc.publishEvents(eventsToPublish) + + return settledTransaction, nil +} + +// createSettledTransactionFromNotification inserts a transaction directly in +// its settled state, in its own database transaction, and publishes the +// corresponding events after it commits. It is for the case where the +// LNClient notifies us of a sent or received payment we didn't already know +// about (e.g. if the LNClient is an external node, and the payment was made +// or received outside of Alby Hub, or a received keysend, which has no +// invoice created upfront). +func (svc *transactionsService) createSettledTransactionFromNotification(dbTransaction *db.Transaction, preimage string, feeMsat uint64, selfPayment bool) (*db.Transaction, error) { + if preimage == "" { + return nil, errors.New("no preimage in payment") + } + + var settledTransaction *db.Transaction + var eventsToPublish []*events.Event + err := svc.db.Transaction(func(tx *gorm.DB) error { + existingSettledTransaction, err := svc.findSettledTransaction(tx, dbTransaction) + if err != nil { + return err + } + if existingSettledTransaction != nil { + logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Debug("payment already marked as settled") + settledTransaction = existingSettledTransaction + return nil + } + + settledAt := time.Now() + dbTransaction.State = constants.TRANSACTION_STATE_SETTLED + dbTransaction.Preimage = &preimage + dbTransaction.FeeMsat = feeMsat + dbTransaction.FeeReserveMsat = 0 + dbTransaction.SettledAt = &settledAt + dbTransaction.SelfPayment = selfPayment + if err := tx.Create(dbTransaction).Error; err != nil { + logger.Logger.WithFields(logrus.Fields{ + "payment_hash": dbTransaction.PaymentHash, + }).WithError(err).Error("Failed to create settled DB transaction") + return err + } + + logger.Logger.WithFields(logrus.Fields{ + "payment_hash": dbTransaction.PaymentHash, + "type": dbTransaction.Type, + }).Info("Created settled transaction") + + settledTransaction = dbTransaction + eventsToPublish = svc.afterTransactionSettled(tx, dbTransaction, &settledAt) + return nil + }) + if err != nil { + return nil, err + } + svc.publishEvents(eventsToPublish) + + return settledTransaction, nil +} + +// lockTransactionsByPaymentHash takes a row lock on all transactions with the +// given payment hash on postgres, so that concurrent state changes for the +// same payment serialize (in sqlite transactions are serializable by default). +func (svc *transactionsService) lockTransactionsByPaymentHash(tx *gorm.DB, paymentHash string) error { + if tx.Dialector.Name() != "postgres" { + return nil + } + transactionsWithPaymentHash := []db.Transaction{} + err := tx.Where(&db.Transaction{ + PaymentHash: paymentHash, + }).Clauses(clause.Locking{Strength: "UPDATE"}).Find(&transactionsWithPaymentHash).Error + if err != nil { + logger.Logger.WithField("payment_hash", paymentHash).WithError(err).Error("Failed to lock transactions by payment hash") + } + return err +} + +// findSettledTransaction returns the already-settled transaction matching +// dbTransaction if one exists, locking all transactions with the same payment +// hash to ensure only one transaction is settled per payment. +func (svc *transactionsService) findSettledTransaction(tx *gorm.DB, dbTransaction *db.Transaction) (*db.Transaction, error) { + if err := svc.lockTransactionsByPaymentHash(tx, dbTransaction.PaymentHash); err != nil { + return nil, err } var existingSettledTransaction db.Transaction - if tx.Limit(1).Find(&existingSettledTransaction, &db.Transaction{ + result := tx.Limit(1).Find(&existingSettledTransaction, &db.Transaction{ Type: dbTransaction.Type, PaymentRequest: dbTransaction.PaymentRequest, PaymentHash: dbTransaction.PaymentHash, State: constants.TRANSACTION_STATE_SETTLED, - }).RowsAffected > 0 { - logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Debug("payment already marked as sent") + }) + if result.Error != nil { + logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).WithError(result.Error).Error("Failed to check for existing settled transaction") + return nil, result.Error + } + if result.RowsAffected > 0 { return &existingSettledTransaction, nil } + return nil, nil +} - settledAt := time.Now() - err := tx.Model(dbTransaction).Updates(map[string]interface{}{ - "State": constants.TRANSACTION_STATE_SETTLED, - "Preimage": &preimage, - "FeeMsat": feeMsat, - "FeeReserveMsat": 0, - "SettledAt": &settledAt, - "SelfPayment": selfPayment, - }).Error - if err != nil { - logger.Logger.WithFields(logrus.Fields{ - "payment_hash": dbTransaction.PaymentHash, - }).WithError(err).Error("Failed to update DB transaction") - return nil, err - } - - logger.Logger.WithFields(logrus.Fields{ - "payment_hash": dbTransaction.PaymentHash, - "type": dbTransaction.Type, - }).Info("Marked transaction as settled") - +// afterTransactionSettled runs the post-settlement side effects within the +// caller's database transaction and returns the events to publish after it +// commits. +func (svc *transactionsService) afterTransactionSettled(tx *gorm.DB, dbTransaction *db.Transaction, settledAt *time.Time) []*events.Event { event := "nwc_payment_sent" if dbTransaction.Type == constants.TRANSACTION_TYPE_INCOMING { event = "nwc_payment_received" } - svc.eventPublisher.Publish(&events.Event{ + eventsToPublish := []*events.Event{{ Event: event, Properties: dbTransaction, - }) + }} if dbTransaction.AppId != nil { var app db.App @@ -1564,17 +1637,25 @@ func (svc *transactionsService) markTransactionSettled(tx *gorm.DB, dbTransactio }) if result.RowsAffected == 0 { logger.Logger.WithField("app_id", dbTransaction.AppId).Error("failed to find app by id") - return dbTransaction, nil + return eventsToPublish } - svc.updateAppLastSettledTransactionAt(&app, tx, &settledAt) + svc.updateAppLastSettledTransactionAt(&app, tx, settledAt) if dbTransaction.Type == constants.TRANSACTION_TYPE_OUTGOING { - svc.checkBudgetUsage(&app, dbTransaction, tx) + if budgetWarningEvent := svc.checkBudgetUsage(&app, dbTransaction, tx); budgetWarningEvent != nil { + eventsToPublish = append(eventsToPublish, budgetWarningEvent) + } } } - return dbTransaction, nil + return eventsToPublish +} + +func (svc *transactionsService) publishEvents(eventsToPublish []*events.Event) { + for _, event := range eventsToPublish { + svc.eventPublisher.Publish(event) + } } func (svc *transactionsService) updateAppLastSettledTransactionAt(app *db.App, gormTransaction *gorm.DB, settledAt *time.Time) { @@ -1584,9 +1665,11 @@ func (svc *transactionsService) updateAppLastSettledTransactionAt(app *db.App, g } } -func (svc *transactionsService) checkBudgetUsage(app *db.App, dbTransaction *db.Transaction, gormTransaction *gorm.DB) { +// checkBudgetUsage returns a budget warning event to publish after the +// caller's database transaction commits, or nil if no warning is due. +func (svc *transactionsService) checkBudgetUsage(app *db.App, dbTransaction *db.Transaction, gormTransaction *gorm.DB) *events.Event { if app.Isolated { - return + return nil } var appPermission db.AppPermission @@ -1596,59 +1679,91 @@ func (svc *transactionsService) checkBudgetUsage(app *db.App, dbTransaction *db. }) if result.RowsAffected == 0 { logger.Logger.WithField("app_id", dbTransaction.AppId).Error("failed to find pay_invoice scope") - return + return nil } budgetUsageMsat, err := queries.GetBudgetUsageMsat(gormTransaction, &appPermission) if err != nil { logger.Logger.WithField("app_id", dbTransaction.AppId).WithError(err).Error("failed to get budget usage") - return + return nil } budgetUsageSat := budgetUsageMsat / 1000 warningUsage := uint64(math.Floor(float64(appPermission.MaxAmountSat) * 0.8)) if budgetUsageSat >= warningUsage && budgetUsageSat-dbTransaction.AmountMsat/1000 < warningUsage { - svc.eventPublisher.Publish(&events.Event{ + return &events.Event{ Event: "nwc_budget_warning", Properties: map[string]interface{}{ "name": app.Name, "id": app.ID, }, - }) + } } -} - -func (svc *transactionsService) markPaymentFailed(tx *gorm.DB, dbTransaction *db.Transaction, reason string) error { - var existingTransaction db.Transaction - result := tx.Limit(1).Find(&existingTransaction, &db.Transaction{ - ID: dbTransaction.ID, - }) - - if result.Error != nil { - logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).WithError(result.Error).Error("could not find transaction to mark as failed") - return result.Error - } - - if existingTransaction.State == constants.TRANSACTION_STATE_FAILED { - logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Info("payment already marked as failed") - return nil - } - - err := tx.Model(dbTransaction).Updates(map[string]interface{}{ - "State": constants.TRANSACTION_STATE_FAILED, - "FeeReserveMsat": 0, - "FailureReason": reason, - }).Error - if err != nil { - logger.Logger.WithFields(logrus.Fields{ - "payment_hash": dbTransaction.PaymentHash, - }).WithError(err).Error("Failed to mark transaction as failed") - return err - } - logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Info("Marked transaction as failed") - - svc.eventPublisher.Publish(&events.Event{ - Event: "nwc_payment_failed", - Properties: dbTransaction, - }) return nil } + +// markPaymentFailed marks the transaction as failed in its own database +// transaction and publishes the failed event after it commits, so subscribers +// never observe uncommitted state. It returns whether this call transitioned +// the transaction to failed (false if it was already failed), and refuses to +// mark a settled transaction as failed. +func (svc *transactionsService) markPaymentFailed(dbTransaction *db.Transaction, reason string) (bool, error) { + markedFailed := false + var eventsToPublish []*events.Event + err := svc.db.Transaction(func(tx *gorm.DB) error { + // lock all transactions with the same payment hash so a concurrent + // settlement cannot slip in between the state check and the update + if err := svc.lockTransactionsByPaymentHash(tx, dbTransaction.PaymentHash); err != nil { + return err + } + + var existingTransaction db.Transaction + result := tx.Limit(1).Find(&existingTransaction, &db.Transaction{ + ID: dbTransaction.ID, + }) + + if result.Error != nil { + logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).WithError(result.Error).Error("could not find transaction to mark as failed") + return result.Error + } + + if result.RowsAffected == 0 { + logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Error("could not find transaction to mark as failed") + return NewNotFoundError() + } + + if existingTransaction.State == constants.TRANSACTION_STATE_FAILED { + logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Info("payment already marked as failed") + return nil + } + + if existingTransaction.State == constants.TRANSACTION_STATE_SETTLED { + logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Error("cannot mark settled payment as failed") + return errors.New("cannot mark settled payment as failed") + } + + err := tx.Model(dbTransaction).Updates(map[string]interface{}{ + "State": constants.TRANSACTION_STATE_FAILED, + "FeeReserveMsat": 0, + "FailureReason": reason, + }).Error + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "payment_hash": dbTransaction.PaymentHash, + }).WithError(err).Error("Failed to mark transaction as failed") + return err + } + logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Info("Marked transaction as failed") + + markedFailed = true + eventsToPublish = append(eventsToPublish, &events.Event{ + Event: "nwc_payment_failed", + Properties: dbTransaction, + }) + return nil + }) + if err != nil { + return false, err + } + svc.publishEvents(eventsToPublish) + return markedFailed, nil +} From d198b19bef49851e6a04ecc609be89dfdc893464 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Sat, 8 Aug 2026 13:38:46 +0700 Subject: [PATCH 108/136] 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 * fix: reset connect-card dialog form on open and show empty name validation error Co-Authored-By: Claude Fable 5 * chore: use shadcn Button for other-card option in connect dialog Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- frontend/src/screens/cards/Cards.tsx | 113 ++++++++++++++++++++++++--- 1 file changed, 103 insertions(+), 10 deletions(-) diff --git a/frontend/src/screens/cards/Cards.tsx b/frontend/src/screens/cards/Cards.tsx index 20921d3a..51e7a225 100644 --- a/frontend/src/screens/cards/Cards.tsx +++ b/frontend/src/screens/cards/Cards.tsx @@ -12,7 +12,7 @@ import { ZapIcon, } from "lucide-react"; import React from "react"; -import { Link } from "react-router"; +import { Link, useNavigate } from "react-router"; import twoFiatLogo from "src/assets/cards/2fiat.png"; import freedomiaLogo from "src/assets/cards/freedomia.png"; import redotpayLogo from "src/assets/cards/redotpay.png"; @@ -34,6 +34,9 @@ import { DialogHeader, DialogTitle, } from "src/components/ui/dialog"; +import { FieldError } from "src/components/ui/field"; +import { Input } from "src/components/ui/input"; +import { Label } from "src/components/ui/label"; import { Select, SelectContent, @@ -783,8 +786,100 @@ function ConnectCardDialog({ onOpenChange: (open: boolean) => void; providers: Provider[]; }) { + const navigate = useNavigate(); + const [showOtherCardForm, setShowOtherCardForm] = React.useState(false); + const [otherCardName, setOtherCardName] = React.useState(""); + const [otherCardNameError, setOtherCardNameError] = React.useState(""); + + // The dialog is controlled and opened programmatically (no DialogTrigger), + // so onOpenChange never fires with true — reset the form here instead. + React.useEffect(() => { + if (open) { + setShowOtherCardForm(false); + setOtherCardName(""); + setOtherCardNameError(""); + } + }, [open]); + + const handleOpenChange = (o: boolean) => { + if (o) { + setShowOtherCardForm(false); + setOtherCardName(""); + setOtherCardNameError(""); + } + onOpenChange(o); + }; + + const handleOtherCardSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const cardName = otherCardName.trim(); + if (!cardName) { + setOtherCardNameError("Enter a card name"); + return; + } + sendEvent("debit_card_connect", { name: cardName }); + onOpenChange(false); + navigate( + `/apps/new?app=bitcoin-card-topup&name=${encodeURIComponent(`${cardName} - Bitcoin Card Topup`)}` + ); + }; + + if (showOtherCardForm) { + return ( +

    + + + Name your card + + We'll use it to label your top-up connection. + + + +
    +
    + + { + setOtherCardName(e.target.value); + setOtherCardNameError(""); + }} + placeholder="e.g. Moon" + required + autoComplete="off" + aria-invalid={!!otherCardNameError || undefined} + aria-describedby={ + otherCardNameError ? "other-card-name-error" : undefined + } + /> + + {otherCardNameError} + +
    +
    + + +
    +
    +
    +
    + ); + } + return ( - + Pick your card provider @@ -835,13 +930,11 @@ function ConnectCardDialog({ ); })} - { - sendEvent("debit_card_connect", { name: "Other" }); - onOpenChange(false); - }} - className="flex items-center gap-3 rounded-lg border border-dashed border-border p-3 hover:bg-accent/40 transition-colors" +
    From 6d0cb6fd2c0088463f64e75f7309a8859c9ed9af Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Sat, 8 Aug 2026 16:39:02 +0700 Subject: [PATCH 109/136] fix: bark onboarding, migration messaging and receive settlement for bark 0.6.0 (#2523) * 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 * 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 * 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 --------- Co-authored-by: Claude Fable 5 --- api/api.go | 10 +-- frontend/src/screens/settings/Backup.tsx | 10 --- frontend/src/screens/setup/ImportMnemonic.tsx | 31 +-------- frontend/src/screens/setup/SetupSecurity.tsx | 24 ++++--- lnclient/bark/bark.go | 68 +++++++++++++++++-- 5 files changed, 82 insertions(+), 61 deletions(-) diff --git a/api/api.go b/api/api.go index da75b818..55634a34 100644 --- a/api/api.go +++ b/api/api.go @@ -1730,10 +1730,12 @@ func (api *api) Setup(ctx context.Context, setupRequest *SetupRequest) error { return errors.New("no unlock password provided") } - // Bark and Cashu both store wallet state on local disk and have no - // remote-backup mechanism, so they cannot run in environments without - // persistent volumes (e.g. Alby Cloud). The default OAuth client ID - // identifies a local / self-hosted deployment. + // Bark and Cashu both store wallet state on local disk, so they cannot + // run in environments without persistent volumes (e.g. Alby Cloud). Bark + // can recover spendable VTXOs from the mnemonic alone, but in-flight + // payment checkpoints and wallet metadata are local-only, so persistent + // storage is still required. The default OAuth client ID identifies a + // local / self-hosted deployment. if !api.cfg.GetEnv().IsDefaultClientId() { switch setupRequest.LNBackendType { case config.BarkBackendType, config.CashuBackendType: diff --git a/frontend/src/screens/settings/Backup.tsx b/frontend/src/screens/settings/Backup.tsx index 33c82be7..d332cdb9 100644 --- a/frontend/src/screens/settings/Backup.tsx +++ b/frontend/src/screens/settings/Backup.tsx @@ -125,16 +125,6 @@ export default function Backup() { phrase, you will lose access to your funds. - {info?.backendType === "BARK" && ( - - - Bark Support Coming Soon - - During the beta period, your recovery phrase is not - sufficient to restore your funds. - - - )} {info?.backendType === "CASHU" && }
    diff --git a/frontend/src/screens/setup/ImportMnemonic.tsx b/frontend/src/screens/setup/ImportMnemonic.tsx index 8328736d..1de6f94e 100644 --- a/frontend/src/screens/setup/ImportMnemonic.tsx +++ b/frontend/src/screens/setup/ImportMnemonic.tsx @@ -1,11 +1,6 @@ import * as bip39 from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english.js"; -import { - AlertTriangleIcon, - LifeBuoyIcon, - ShieldAlertIcon, - ShieldCheckIcon, -} from "lucide-react"; +import { AlertTriangleIcon, LifeBuoyIcon, ShieldCheckIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { useNavigate } from "react-router"; @@ -14,14 +9,11 @@ import MnemonicInputs from "src/components/mnemonic/MnemonicInputs"; import TwoColumnLayoutHeader from "src/components/TwoColumnLayoutHeader"; import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert"; import { Button } from "src/components/ui/button"; -import { Checkbox } from "src/components/ui/checkbox"; -import { Label } from "src/components/ui/label"; import useSetupStore from "src/state/SetupStore"; export function ImportMnemonic() { const navigate = useNavigate(); const setupStore = useSetupStore(); - const [backedUp, setIsBackedUp] = useState(false); useEffect(() => { // in case the user presses back, remove their last-saved mnemonic @@ -93,32 +85,11 @@ export function ImportMnemonic() { Keep it safe and private to ensure your funds remain secure.
    -
    -
    - -
    - - Your recovery phrase cannot restore funds from lightning channels. - If you had active channels on a different device, contact Alby - support before proceeding. - -
    -
    - setIsBackedUp(!backedUp)} - /> - -
    ); diff --git a/frontend/src/screens/setup/SetupSecurity.tsx b/frontend/src/screens/setup/SetupSecurity.tsx index 77fcd719..1ae5c5ea 100644 --- a/frontend/src/screens/setup/SetupSecurity.tsx +++ b/frontend/src/screens/setup/SetupSecurity.tsx @@ -1,7 +1,6 @@ import { ClockIcon, HandCoinsIcon, - HardDriveIcon, LandmarkIcon, ShieldAlertIcon, UnlockIcon, @@ -76,14 +75,6 @@ export function SetupSecurity() { small fee.
    -
    - - - During beta, your funds{" "} - cannot be recovered from - your recovery phrase alone. - -
    )}
    @@ -109,7 +100,20 @@ export function SetupSecurity() { choose the LDK node type.
    - ) : store.nodeInfo.backendType === "BARK" ? null : ( + ) : store.hasImportedMnemonic && + store.nodeInfo.backendType === "LDK" ? ( +
    +
    + +
    + + Your recovery phrase can only restore funds from lightning + channels if you have dynamic channel backups enabled. If you had + active channels on a different device, contact Alby support + before proceeding. + +
    + ) : (
    diff --git a/lnclient/bark/bark.go b/lnclient/bark/bark.go index c73e8f02..9db69cd0 100644 --- a/lnclient/bark/bark.go +++ b/lnclient/bark/bark.go @@ -34,8 +34,12 @@ const ( // Movement status reported once a movement has settled. A movement first // appears as "pending" and is updated to this once complete. movementStatusSuccessful = "successful" - // LightningReceive.State values in which we hold the preimage. + // LightningReceive.State values at or past preimage reveal. + // "delivering" (added in bark 0.6.0) sits between preimage reveal and + // settlement: the claim is recorded and delivery resumes automatically, + // so the funds are already irrevocably received. receiveStatePreimageRevealed = "preimage-revealed" + receiveStateDelivering = "delivering" receiveStateSettled = "settled" // Grace period to allow the notification loop to unwind on shutdown. shutdownGracePeriod = 10 * time.Second @@ -562,17 +566,33 @@ func (bs *BarkService) lightningReceiveToTransaction(receive *bark.LightningRece Description: paymentRequest.Description, DescriptionHash: paymentRequest.DescriptionHash, } - // "preimage-revealed" until the claim is recorded, "settled" after. - if receive.State == receiveStatePreimageRevealed || receive.State == receiveStateSettled { - now := time.Now().Unix() - tx.SettledAt = &now - if receive.PaymentPreimage != nil { - tx.Preimage = *receive.PaymentPreimage + // Only report the receive as settled when we can include the preimage — + // a settled transaction without one is rejected by the transactions + // service. + if receive.PaymentPreimage != nil && receiveIsPaid(receive.State) { + tx.Preimage = *receive.PaymentPreimage + settledAt := time.Now().Unix() + if receive.SettledAt != nil { + settledAt = *receive.SettledAt } + tx.SettledAt = &settledAt } return tx, nil } +// receiveIsPaid reports whether a receive's state is at or past preimage +// reveal, meaning the payer holds the preimage and the payment is final. +// The state is the only reliable signal: bark generates and stores the +// preimage at invoice creation, so LightningReceive.PaymentPreimage can be +// set long before anything is paid. +func receiveIsPaid(state string) bool { + switch state { + case receiveStatePreimageRevealed, receiveStateDelivering, receiveStateSettled: + return true + } + return false +} + func (bs *BarkService) GetBalances(ctx context.Context, includeInactiveChannels bool) (*lnclient.BalancesResponse, error) { balance, err := bs.wallet.Balance() if err != nil { @@ -745,6 +765,7 @@ const ( nodeCommandDebug = "debug" nodeCommandClaimLightningReceives = "claimlightningreceives" nodeCommandRunMaintenance = "runmaintenance" + nodeCommandRecoveryReport = "recoveryreport" ) func (bs *BarkService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef { @@ -764,6 +785,11 @@ func (bs *BarkService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCo Description: "Run wallet maintenance, which progresses pending rounds and refreshes VTXOs. Use this to nudge funds that are stuck 'pending in round'.", Args: nil, }, + { + Name: nodeCommandRecoveryReport, + Description: "Show the result of the seed-recovery scan that runs when a wallet is created from an existing recovery phrase. Use this to verify your funds were restored after migrating to a new device.", + Args: nil, + }, } } @@ -775,6 +801,8 @@ func (bs *BarkService) ExecuteCustomNodeCommand(ctx context.Context, command *ln return bs.executeCommandClaimLightningReceives() case nodeCommandRunMaintenance: return bs.executeCommandRunMaintenance() + case nodeCommandRecoveryReport: + return bs.executeCommandRecoveryReport() } return nil, lnclient.ErrUnknownCustomNodeCommand @@ -893,3 +921,29 @@ func (bs *BarkService) executeCommandClaimLightningReceives() (*lnclient.CustomN }, }, nil } + +func (bs *BarkService) executeCommandRecoveryReport() (*lnclient.CustomNodeCommandResponse, error) { + // The report is produced by the seed-recovery scan bark runs during the + // wallet open that creates the wallet locally (e.g. when restoring from a + // recovery phrase on a new device). It is only available in the session + // that created the wallet; on subsequent starts no scan runs. + report := bs.wallet.RecoveryReport() + if report == nil { + return &lnclient.CustomNodeCommandResponse{ + Response: map[string]interface{}{ + "message": "No recovery scan ran on this wallet start. A scan only runs when the wallet is first created, e.g. after restoring from a recovery phrase.", + }, + }, nil + } + + return &lnclient.CustomNodeCommandResponse{ + Response: map[string]interface{}{ + "isComplete": report.IsComplete, + "recovered": report.Recovered, + "skipped": report.Skipped, + "foreign": report.Foreign, + "failed": report.Failed, + "exited": report.Exited, + }, + }, nil +} From ffee8cbcbeab3969654604a45076af8e094a523a Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:20:41 +0700 Subject: [PATCH 110/136] 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 --- README.md | 8 +- api/api.go | 2 + api/backup.go | 53 ++++- api/backup_test.go | 106 +++++++++ api/models.go | 2 + cmd/db_migrate/main.go | 247 ++------------------- cmd/db_migrate/migrate_test.go | 122 +++++++--- db/db_migrate.go | 235 ++++++++++++++++++++ frontend/src/screens/MigrateNode.tsx | 28 ++- frontend/src/screens/settings/About.tsx | 20 ++ frontend/src/screens/setup/RestoreNode.tsx | 12 +- frontend/src/types.ts | 2 + 12 files changed, 560 insertions(+), 277 deletions(-) create mode 100644 api/backup_test.go create mode 100644 db/db_migrate.go diff --git a/README.md b/README.md index 1572f1dc..a80cdc04 100644 --- a/README.md +++ b/README.md @@ -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. @@ -207,6 +207,12 @@ 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) diff --git a/api/api.go b/api/api.go index 55634a34..1cc227f3 100644 --- a/api/api.go +++ b/api/api.go @@ -1519,6 +1519,8 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { info.LdkVssEnabled = ldkVssEnabled == "true" info.JitChannelsEnabled = jitChannelsEnabled != "false" info.VssSupported = backendType == config.LDKBackendType && api.cfg.GetEnv().LDKVssUrl != "" + info.LdkVssUrl = api.cfg.GetEnv().LDKVssUrl + info.DatabaseType = api.db.Dialector.Name() info.SupportsBolt12 = backendType == config.LDKBackendType || backendType == config.CLNBackendType info.AutoUnlockPasswordEnabled = autoUnlockPassword != "" info.AutoUnlockPasswordSupported = api.cfg.GetEnv().IsDefaultClientId() diff --git a/api/backup.go b/api/backup.go index 9d89ef7b..60db76b7 100644 --- a/api/backup.go +++ b/api/backup.go @@ -38,8 +38,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) @@ -76,6 +77,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. @@ -126,8 +171,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") @@ -245,7 +288,7 @@ func (api *api) RestoreBackup(unlockPassword string, r io.Reader) error { // 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") } } diff --git a/api/backup_test.go b/api/backup_test.go new file mode 100644 index 00000000..b41eea19 --- /dev/null +++ b/api/backup_test.go @@ -0,0 +1,106 @@ +package api + +import ( + "archive/zip" + "bytes" + "io" + "os" + "path/filepath" + "strconv" + "testing" + + "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) + + 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, + } + + unlockPassword := "" + + 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) +} diff --git a/api/models.go b/api/models.go index eda20676..aadfd0ed 100644 --- a/api/models.go +++ b/api/models.go @@ -317,7 +317,9 @@ type InfoResponse struct { 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"` diff --git a/cmd/db_migrate/main.go b/cmd/db_migrate/main.go index c10f4a3e..4207a4c6 100644 --- a/cmd/db_migrate/main.go +++ b/cmd/db_migrate/main.go @@ -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 -} diff --git a/cmd/db_migrate/migrate_test.go b/cmd/db_migrate/migrate_test.go index ecc6a78d..12ae54e1 100644 --- a/cmd/db_migrate/migrate_test.go +++ b/cmd/db_migrate/migrate_test.go @@ -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) }) } } @@ -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 { diff --git a/db/db_migrate.go b/db/db_migrate.go new file mode 100644 index 00000000..666e77c3 --- /dev/null +++ b/db/db_migrate.go @@ -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 +} diff --git a/frontend/src/screens/MigrateNode.tsx b/frontend/src/screens/MigrateNode.tsx index 762114b8..0c97b578 100644 --- a/frontend/src/screens/MigrateNode.tsx +++ b/frontend/src/screens/MigrateNode.tsx @@ -1,4 +1,4 @@ -import { InfoIcon, TriangleAlertIcon } from "lucide-react"; +import { DatabaseIcon, InfoIcon, TriangleAlertIcon } from "lucide-react"; import React, { useState } from "react"; import { useNavigate } from "react-router"; import PasswordInput from "src/components/password/PasswordInput"; @@ -8,6 +8,13 @@ import { Button } from "src/components/ui/button"; import { LinkButton } from "src/components/ui/custom/link-button"; import { LoadingButton } from "src/components/ui/custom/loading-button"; import { Label } from "src/components/ui/label"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "src/components/ui/tooltip"; +import { useInfo } from "src/hooks/useInfo"; import { handleRequestError } from "src/utils/handleRequestError"; import { isHttpMode } from "src/utils/isHttpMode"; @@ -15,6 +22,7 @@ import { request } from "src/utils/request"; export function MigrateNode() { const navigate = useNavigate(); + const { data: info } = useInfo(); const [unlockPassword, setUnlockPassword] = React.useState(""); const [showPasswordScreen, setShowPasswordScreen] = useState(false); @@ -113,6 +121,24 @@ export function MigrateNode() { another device or server.

    + {info?.databaseType === "postgres" && ( +
    + +

    Your database will be migrated

    + + + + + + + The contents of your PostgreSQL database will be copied into a + local SQLite database while the migration file is being + created. Your new Alby Hub will use this SQLite database. + + + +
    + )}
    {showPasswordScreen ? ( diff --git a/frontend/src/screens/settings/About.tsx b/frontend/src/screens/settings/About.tsx index e82f784b..b2e254ba 100644 --- a/frontend/src/screens/settings/About.tsx +++ b/frontend/src/screens/settings/About.tsx @@ -66,6 +66,26 @@ export function About() {

    {backendTypeConfigs[info.backendType].title}

    + {info.databaseType && ( +
    +

    Database Storage

    +

    + {info.databaseType === "postgres" + ? "PostgreSQL" + : info.databaseType === "sqlite" + ? "SQLite" + : info.databaseType} +

    +
    + )} + {info.backendType === "LDK" && ( +
    +

    VSS

    +

    + {info.ldkVssEnabled ? `Enabled (${info.ldkVssUrl})` : "Disabled"} +

    +
    + )} {info.chainDataSourceType && (

    Chain Data Source

    diff --git a/frontend/src/screens/setup/RestoreNode.tsx b/frontend/src/screens/setup/RestoreNode.tsx index e581ac2e..e364eca4 100644 --- a/frontend/src/screens/setup/RestoreNode.tsx +++ b/frontend/src/screens/setup/RestoreNode.tsx @@ -52,9 +52,9 @@ export function RestoreNode() { />

    - If you're running in the cloud, your Alby Hub will restart - automatically. Otherwise, please manually restart your Alby Hub to - finish the restore process. + If you're running in a cloud VM or linux service, your Alby Hub will + restart automatically. Otherwise, please manually restart your Alby + Hub to finish the restore process.

    Waiting for restart...

    @@ -158,9 +158,9 @@ export function RestoreNode() { down.

    - If you're running in the cloud, your Alby Hub will restart - automatically. Otherwise, please manually restart your Alby - Hub to finish the restore process. + If you're running in a cloud VM or linux service, your Alby + Hub will restart automatically. Otherwise, please manually + restart your Alby Hub to finish the restore process.

    diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 2e67d98f..f828f2ca 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -158,7 +158,9 @@ export interface InfoResponse { oauthRedirect: boolean; albyAccountConnected: boolean; ldkVssEnabled: boolean; + ldkVssUrl: string; vssSupported: boolean; + databaseType: string; running: boolean; albyAuthUrl: string; nextBackupReminder: string; From 56d118a851ebfecc4ebbd8336d5c7c5947fa0e6a Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:33:20 +0700 Subject: [PATCH 111/136] fix: remove dollar sign from linux install scripts (#2525) --- scripts/linux-aarch64/README.md | 2 +- scripts/linux-x86_64/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/linux-aarch64/README.md b/scripts/linux-aarch64/README.md index d1b3b51e..d14bd994 100644 --- a/scripts/linux-aarch64/README.md +++ b/scripts/linux-aarch64/README.md @@ -15,7 +15,7 @@ If you do a fresh server setup make sure to do the basic setup like for example Run the installation script on your server: - $ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/getAlby/hub/master/scripts/linux-aarch64/install.sh)" + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/getAlby/hub/master/scripts/linux-aarch64/install.sh)" The install script will prompt you for an installation folder and will install Alby Hub. Optionally it can also create a systemd service for you. diff --git a/scripts/linux-x86_64/README.md b/scripts/linux-x86_64/README.md index 2f803b19..c9ab2bdd 100644 --- a/scripts/linux-x86_64/README.md +++ b/scripts/linux-x86_64/README.md @@ -15,7 +15,7 @@ If you do a fresh server setup make sure to do the basic setup like for example Run the installation script on your server: - $ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/getAlby/hub/master/scripts/linux-x86_64/install.sh)" + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/getAlby/hub/master/scripts/linux-x86_64/install.sh)" The install script will prompt you for an installation folder and will install Alby Hub. Optionally it can also create a systemd service for you. From 037765794d026623544c19dc38d34cf722564c40 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:37:26 +0700 Subject: [PATCH 112/136] fix: keep showing migration success page after creating migration file (#2527) * 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 * 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 * 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 --------- Co-authored-by: Claude Fable 5 --- api/api.go | 18 ++++++++++++++++++ api/backup.go | 10 ++++++++++ api/models.go | 1 + .../src/components/redirects/HomeRedirect.tsx | 7 +++++++ .../src/components/redirects/StartRedirect.tsx | 6 ++++++ frontend/src/types.ts | 1 + 6 files changed, 43 insertions(+) diff --git a/api/api.go b/api/api.go index 1cc227f3..70c0d174 100644 --- a/api/api.go +++ b/api/api.go @@ -15,6 +15,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "time" "github.com/sirupsen/logrus" @@ -50,6 +51,9 @@ type api struct { startupError error startupErrorTime time.Time eventPublisher events.EventPublisher + // set after a migration file is created; the hub is halted at that point + // and the frontend should keep showing the migration success page + nodeMigrationFileCreated atomic.Bool } func NewAPI(svc service.Service, gormDB *gorm.DB, config config.Config, keys keys.Keys, albySvc alby.AlbyService, albyOAuthSvc alby.AlbyOAuthService, eventPublisher events.EventPublisher) *api { @@ -1491,6 +1495,19 @@ func (api *api) RequestMempoolApi(ctx context.Context, endpoint string) (interfa func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { info := InfoResponse{} + + if api.nodeMigrationFileCreated.Load() { + // the hub is halted and the database is closed after a migration file + // is created, so return a minimal response without reading any config + // or node state; the frontend only needs the flag to keep showing the + // migration success page + info.NodeMigrationFileCreated = true + info.SetupCompleted = true + info.Version = version.Tag + info.Relays = []InfoResponseRelay{} + return &info, nil + } + backendType, _ := api.cfg.Get("LNBackendType", "") ldkVssEnabled, _ := api.cfg.Get("LdkVssEnabled", "") jitChannelsEnabled, _ := api.cfg.Get("JitChannelsEnabled", "") @@ -1510,6 +1527,7 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { } lnClient := api.svc.GetLNClient() info.Running = lnClient != nil + info.NodeMigrationFileCreated = api.nodeMigrationFileCreated.Load() info.BackendType = backendType info.AlbyAuthUrl = api.albyOAuthSvc.GetAuthUrl() info.OAuthRedirect = !api.cfg.GetEnv().IsDefaultClientId() diff --git a/api/backup.go b/api/backup.go index 60db76b7..f8f6b0fc 100644 --- a/api/backup.go +++ b/api/backup.go @@ -195,8 +195,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 } diff --git a/api/models.go b/api/models.go index aadfd0ed..355e6602 100644 --- a/api/models.go +++ b/api/models.go @@ -338,6 +338,7 @@ type InfoResponse struct { JitChannelsEnabled bool `json:"jitChannelsEnabled"` HideUpdateBanner bool `json:"hideUpdateBanner"` SupportsBolt12 bool `json:"supportsBolt12"` + NodeMigrationFileCreated bool `json:"nodeMigrationFileCreated"` } type UpdateSettingsRequest struct { diff --git a/frontend/src/components/redirects/HomeRedirect.tsx b/frontend/src/components/redirects/HomeRedirect.tsx index 48c7c7f1..d390c177 100644 --- a/frontend/src/components/redirects/HomeRedirect.tsx +++ b/frontend/src/components/redirects/HomeRedirect.tsx @@ -14,6 +14,13 @@ export function HomeRedirect() { return; } + if (info.nodeMigrationFileCreated) { + // the hub is halted after creating a migration file and should not be + // used anymore, so keep showing the migration success instructions + navigate("/create-node-migration-file-success", { replace: true }); + return; + } + const setupReturnTo = window.localStorage.getItem( localStorageKeys.setupReturnTo ); diff --git a/frontend/src/components/redirects/StartRedirect.tsx b/frontend/src/components/redirects/StartRedirect.tsx index 739b425f..53a9a7e8 100644 --- a/frontend/src/components/redirects/StartRedirect.tsx +++ b/frontend/src/components/redirects/StartRedirect.tsx @@ -9,6 +9,12 @@ export function StartRedirect({ children }: React.PropsWithChildren) { const navigate = useNavigate(); React.useEffect(() => { + if (info?.nodeMigrationFileCreated) { + // the hub is halted after creating a migration file and should not be + // used anymore, so keep showing the migration success instructions + navigate("/create-node-migration-file-success", { replace: true }); + return; + } if (!info || (info.setupCompleted && !info.running)) { if (info && !info.albyAccountConnected && info.albyUserIdentifier) { navigate("/alby/auth"); diff --git a/frontend/src/types.ts b/frontend/src/types.ts index f828f2ca..24fbb620 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -187,6 +187,7 @@ export interface InfoResponse { jitChannelsEnabled: boolean; hideUpdateBanner: boolean; supportsBolt12: boolean; + nodeMigrationFileCreated: boolean; } export type BitcoinDisplayFormat = "sats" | "bip177"; From fa5cc3511e5f0507d6de67644f4bbf28b002088c Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:42:45 +0700 Subject: [PATCH 113/136] 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 --- api/backup.go | 18 +++++++++++++++- api/backup_test.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/api/backup.go b/api/backup.go index f8f6b0fc..9d2d7622 100644 --- a/api/backup.go +++ b/api/backup.go @@ -257,8 +257,24 @@ func (api *api) RestoreBackup(unlockPassword string, r io.Reader) error { return fmt.Errorf("failed to create zip reader: %w", err) } + restoreDir := filepath.Join(workDir, "restore") + 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(restoreDir, entryName) + + // Confirm the cleaned path is still contained within the restore + // directory. + if fsFilePath != restoreDir && !strings.HasPrefix(fsFilePath, restoreDir+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) diff --git a/api/backup_test.go b/api/backup_test.go index b41eea19..784367f9 100644 --- a/api/backup_test.go +++ b/api/backup_test.go @@ -104,3 +104,55 @@ func TestCreateBackup(t *testing.T) { 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 /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) + 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.Error(t, err) + + _, statErr := os.Stat(escapeTarget) + require.True(t, os.IsNotExist(statErr), "traversal entry must not be written outside the restore directory") +} From 0b0cbbd985b75a59b282789a7077b8739ae7716d Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Tue, 11 Aug 2026 14:48:41 +0700 Subject: [PATCH 114/136] 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 --- tests/mock_event_consumer.go | 26 +++++++++++- transactions/app_payments_test.go | 13 +++--- .../check_unsettled_transaction_test.go | 14 ++++--- transactions/isolated_app_payments_test.go | 11 ++--- transactions/keysend_test.go | 40 ++++++++++++------- transactions/payments_test.go | 33 ++++++++------- transactions/self_payments_test.go | 4 +- 7 files changed, 92 insertions(+), 49 deletions(-) diff --git a/tests/mock_event_consumer.go b/tests/mock_event_consumer.go index a23cd53b..1eceb1c4 100644 --- a/tests/mock_event_consumer.go +++ b/tests/mock_event_consumer.go @@ -2,12 +2,14 @@ package tests import ( "context" + "sync" "time" "github.com/getAlby/hub/events" ) type mockEventConsumer struct { + mtx sync.Mutex consumedEvents []*events.Event } @@ -18,11 +20,33 @@ func NewMockEventConsumer() *mockEventConsumer { } func (e *mockEventConsumer) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) { + e.mtx.Lock() + defer e.mtx.Unlock() e.consumedEvents = append(e.consumedEvents, event) } func (e *mockEventConsumer) GetConsumedEvents() []*events.Event { // events are consumed async - give it a bit of time for tests time.Sleep(10 * time.Millisecond) - return e.consumedEvents + return e.snapshotConsumedEvents() +} + +// WaitForConsumedEvents waits until at least count events have been consumed +// (events are consumed async) and returns them. On timeout it returns the +// events consumed so far, so the caller's assertions fail with a useful message. +func (e *mockEventConsumer) WaitForConsumedEvents(count int) []*events.Event { + deadline := time.Now().Add(5 * time.Second) + for { + consumedEvents := e.snapshotConsumedEvents() + if len(consumedEvents) >= count || time.Now().After(deadline) { + return consumedEvents + } + time.Sleep(10 * time.Millisecond) + } +} + +func (e *mockEventConsumer) snapshotConsumedEvents() []*events.Event { + e.mtx.Lock() + defer e.mtx.Unlock() + return append([]*events.Event{}, e.consumedEvents...) } diff --git a/transactions/app_payments_test.go b/transactions/app_payments_test.go index b989407a..bc3da8bc 100644 --- a/transactions/app_payments_test.go +++ b/transactions/app_payments_test.go @@ -95,7 +95,7 @@ func TestMarkSettled_App_BudgetWarning(t *testing.T) { _, err = transactionsService.markTransactionSettled(&dbTransaction, "test", 0, false) assert.NoError(t, err) - consumedEvents := mockEventConsumer.GetConsumedEvents() + consumedEvents := mockEventConsumer.WaitForConsumedEvents(2) assert.Equal(t, 2, len(consumedEvents)) eventNames := []string{} for _, consumedEvent := range consumedEvents { @@ -136,12 +136,13 @@ func TestSendPaymentSync_App_BudgetExceeded(t *testing.T) { assert.ErrorIs(t, err, NewQuotaExceededError()) assert.Nil(t, transaction) - assert.Equal(t, 1, len(mockEventConsumer.GetConsumedEvents())) - assert.Equal(t, "nwc_permission_denied", mockEventConsumer.GetConsumedEvents()[0].Event) - assert.Equal(t, app.Name, mockEventConsumer.GetConsumedEvents()[0].Properties.(map[string]interface{})["app_name"]) - assert.Equal(t, constants.ERROR_QUOTA_EXCEEDED, mockEventConsumer.GetConsumedEvents()[0].Properties.(map[string]interface{})["code"]) + consumedEvents := mockEventConsumer.WaitForConsumedEvents(1) + assert.Equal(t, 1, len(consumedEvents)) + assert.Equal(t, "nwc_permission_denied", consumedEvents[0].Event) + assert.Equal(t, app.Name, consumedEvents[0].Properties.(map[string]interface{})["app_name"]) + assert.Equal(t, constants.ERROR_QUOTA_EXCEEDED, consumedEvents[0].Properties.(map[string]interface{})["code"]) expectedMessage := NewQuotaExceededError().Error() + " te" // invoice description is "te" in the mock invoice - assert.Equal(t, expectedMessage, mockEventConsumer.GetConsumedEvents()[0].Properties.(map[string]interface{})["message"]) + assert.Equal(t, expectedMessage, consumedEvents[0].Properties.(map[string]interface{})["message"]) } func TestSendPaymentSync_App_BudgetExceeded_SettledPayment(t *testing.T) { diff --git a/transactions/check_unsettled_transaction_test.go b/transactions/check_unsettled_transaction_test.go index 1cdff0e9..db736b22 100644 --- a/transactions/check_unsettled_transaction_test.go +++ b/transactions/check_unsettled_transaction_test.go @@ -45,9 +45,10 @@ func TestCheckUnsettledTransaction(t *testing.T) { assert.NoError(t, err) assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, dbTransaction.State) - assert.Equal(t, 1, len(mockEventConsumer.GetConsumedEvents())) - assert.Equal(t, "nwc_payment_sent", mockEventConsumer.GetConsumedEvents()[0].Event) - settledTransaction := mockEventConsumer.GetConsumedEvents()[0].Properties.(*db.Transaction) + consumedEvents := mockEventConsumer.WaitForConsumedEvents(1) + assert.Equal(t, 1, len(consumedEvents)) + assert.Equal(t, "nwc_payment_sent", consumedEvents[0].Event) + settledTransaction := consumedEvents[0].Properties.(*db.Transaction) assert.Equal(t, &dbTransaction, settledTransaction) } @@ -91,8 +92,9 @@ func TestCheckUnsettledTransactions(t *testing.T) { }) assert.NoError(t, err) assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, dbTransaction.State) - assert.Equal(t, 1, len(mockEventConsumer.GetConsumedEvents())) - assert.Equal(t, "nwc_payment_sent", mockEventConsumer.GetConsumedEvents()[0].Event) - settledTransaction := mockEventConsumer.GetConsumedEvents()[0].Properties.(*db.Transaction) + consumedEvents := mockEventConsumer.WaitForConsumedEvents(1) + assert.Equal(t, 1, len(consumedEvents)) + assert.Equal(t, "nwc_payment_sent", consumedEvents[0].Event) + settledTransaction := consumedEvents[0].Properties.(*db.Transaction) assert.Equal(t, dbTransaction.ID, settledTransaction.ID) } diff --git a/transactions/isolated_app_payments_test.go b/transactions/isolated_app_payments_test.go index e9a3e8e2..bb680b5a 100644 --- a/transactions/isolated_app_payments_test.go +++ b/transactions/isolated_app_payments_test.go @@ -80,12 +80,13 @@ func TestSendPaymentSync_IsolatedApp_BalanceInsufficient(t *testing.T) { assert.ErrorIs(t, err, NewInsufficientBalanceError()) assert.Nil(t, transaction) - assert.Equal(t, 1, len(mockEventConsumer.GetConsumedEvents())) - assert.Equal(t, "nwc_permission_denied", mockEventConsumer.GetConsumedEvents()[0].Event) - assert.Equal(t, app.Name, mockEventConsumer.GetConsumedEvents()[0].Properties.(map[string]interface{})["app_name"]) - assert.Equal(t, constants.ERROR_INSUFFICIENT_BALANCE, mockEventConsumer.GetConsumedEvents()[0].Properties.(map[string]interface{})["code"]) + consumedEvents := mockEventConsumer.WaitForConsumedEvents(1) + assert.Equal(t, 1, len(consumedEvents)) + assert.Equal(t, "nwc_permission_denied", consumedEvents[0].Event) + assert.Equal(t, app.Name, consumedEvents[0].Properties.(map[string]interface{})["app_name"]) + assert.Equal(t, constants.ERROR_INSUFFICIENT_BALANCE, consumedEvents[0].Properties.(map[string]interface{})["code"]) expectedMessage := NewInsufficientBalanceError().Error() + " te" // invoice description is "te" in the mock invoice - assert.Equal(t, expectedMessage, mockEventConsumer.GetConsumedEvents()[0].Properties.(map[string]interface{})["message"]) + assert.Equal(t, expectedMessage, consumedEvents[0].Properties.(map[string]interface{})["message"]) } func TestSendPaymentSync_IsolatedApp_BalanceSufficient(t *testing.T) { diff --git a/transactions/keysend_test.go b/transactions/keysend_test.go index ba50db49..6fe1f27e 100644 --- a/transactions/keysend_test.go +++ b/transactions/keysend_test.go @@ -43,9 +43,10 @@ func TestSendKeysend(t *testing.T) { assert.NotNil(t, transaction.Preimage) assert.Equal(t, 64, len(*transaction.Preimage)) - assert.Equal(t, 1, len(mockEventConsumer.GetConsumedEvents())) - assert.Equal(t, "nwc_payment_sent", mockEventConsumer.GetConsumedEvents()[0].Event) - settledTransaction := mockEventConsumer.GetConsumedEvents()[0].Properties.(*db.Transaction) + consumedEvents := mockEventConsumer.WaitForConsumedEvents(1) + assert.Equal(t, 1, len(consumedEvents)) + assert.Equal(t, "nwc_payment_sent", consumedEvents[0].Event) + settledTransaction := consumedEvents[0].Properties.(*db.Transaction) assert.Equal(t, transaction, settledTransaction) } func TestSendKeysend_FailedRemovesFeeReserve(t *testing.T) { @@ -72,8 +73,9 @@ func TestSendKeysend_FailedRemovesFeeReserve(t *testing.T) { assert.Zero(t, failedTransaction.FeeReserveMsat) assert.Equal(t, "Some error", failedTransaction.FailureReason) - assert.Equal(t, 1, len(mockEventConsumer.GetConsumedEvents())) - assert.Equal(t, "nwc_payment_failed", mockEventConsumer.GetConsumedEvents()[0].Event) + consumedEvents := mockEventConsumer.WaitForConsumedEvents(1) + assert.Equal(t, 1, len(consumedEvents)) + assert.Equal(t, "nwc_payment_failed", consumedEvents[0].Event) } func TestSendKeysend_CustomPreimage(t *testing.T) { @@ -190,11 +192,12 @@ func TestSendKeysend_App_BudgetExceeded(t *testing.T) { assert.ErrorIs(t, err, NewQuotaExceededError()) assert.Nil(t, transaction) - assert.Equal(t, 1, len(mockEventConsumer.GetConsumedEvents())) - assert.Equal(t, "nwc_permission_denied", mockEventConsumer.GetConsumedEvents()[0].Event) - assert.Equal(t, app.Name, mockEventConsumer.GetConsumedEvents()[0].Properties.(map[string]interface{})["app_name"]) - assert.Equal(t, constants.ERROR_QUOTA_EXCEEDED, mockEventConsumer.GetConsumedEvents()[0].Properties.(map[string]interface{})["code"]) - assert.Equal(t, NewQuotaExceededError().Error(), mockEventConsumer.GetConsumedEvents()[0].Properties.(map[string]interface{})["message"]) + consumedEvents := mockEventConsumer.WaitForConsumedEvents(1) + assert.Equal(t, 1, len(consumedEvents)) + assert.Equal(t, "nwc_permission_denied", consumedEvents[0].Event) + assert.Equal(t, app.Name, consumedEvents[0].Properties.(map[string]interface{})["app_name"]) + assert.Equal(t, constants.ERROR_QUOTA_EXCEEDED, consumedEvents[0].Properties.(map[string]interface{})["code"]) + assert.Equal(t, NewQuotaExceededError().Error(), consumedEvents[0].Properties.(map[string]interface{})["message"]) } func TestSendKeysend_App_BudgetNotExceeded(t *testing.T) { svc, err := tests.CreateTestService(t) @@ -530,13 +533,20 @@ func TestSendKeysend_IsolatedAppToIsolatedApp(t *testing.T) { assert.Equal(t, int64(123000), balanceMsat) // check notifications - assert.Equal(t, 2, len(mockEventConsumer.GetConsumedEvents())) + consumedEvents := mockEventConsumer.WaitForConsumedEvents(2) + assert.Equal(t, 2, len(consumedEvents)) - assert.Equal(t, "nwc_payment_sent", mockEventConsumer.GetConsumedEvents()[1].Event) - settledTransaction := mockEventConsumer.GetConsumedEvents()[1].Properties.(*db.Transaction) + // we can't guarantee which notification was processed first because events are published async + // so swap them if they are back to front + if consumedEvents[1].Event == "nwc_payment_received" { + consumedEvents[0], consumedEvents[1] = consumedEvents[1], consumedEvents[0] + } + + assert.Equal(t, "nwc_payment_sent", consumedEvents[1].Event) + settledTransaction := consumedEvents[1].Properties.(*db.Transaction) assert.Equal(t, transaction.ID, settledTransaction.ID) - assert.Equal(t, "nwc_payment_received", mockEventConsumer.GetConsumedEvents()[0].Event) - receivedTransaction := mockEventConsumer.GetConsumedEvents()[0].Properties.(*db.Transaction) + assert.Equal(t, "nwc_payment_received", consumedEvents[0].Event) + receivedTransaction := consumedEvents[0].Properties.(*db.Transaction) assert.Equal(t, incomingTransaction.ID, receivedTransaction.ID) } diff --git a/transactions/payments_test.go b/transactions/payments_test.go index ef8a5037..5fa8fe71 100644 --- a/transactions/payments_test.go +++ b/transactions/payments_test.go @@ -184,9 +184,10 @@ func TestMarkSettled_Sent(t *testing.T) { assert.NoError(t, err) assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, dbTransaction.State) - assert.Equal(t, 1, len(mockEventConsumer.GetConsumedEvents())) - assert.Equal(t, "nwc_payment_sent", mockEventConsumer.GetConsumedEvents()[0].Event) - settledTransaction := mockEventConsumer.GetConsumedEvents()[0].Properties.(*db.Transaction) + consumedEvents := mockEventConsumer.WaitForConsumedEvents(1) + assert.Equal(t, 1, len(consumedEvents)) + assert.Equal(t, "nwc_payment_sent", consumedEvents[0].Event) + settledTransaction := consumedEvents[0].Properties.(*db.Transaction) assert.Equal(t, &dbTransaction, settledTransaction) } @@ -233,9 +234,10 @@ func TestMarkSettled_Twice(t *testing.T) { var reloadedTransaction db.Transaction require.NoError(t, svc.DB.First(&reloadedTransaction, dbTransaction.ID).Error) assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, reloadedTransaction.State) - assert.Equal(t, 1, len(mockEventConsumer.GetConsumedEvents())) - assert.Equal(t, "nwc_payment_sent", mockEventConsumer.GetConsumedEvents()[0].Event) - settledTransaction := mockEventConsumer.GetConsumedEvents()[0].Properties.(*db.Transaction) + consumedEvents := mockEventConsumer.WaitForConsumedEvents(1) + assert.Equal(t, 1, len(consumedEvents)) + assert.Equal(t, "nwc_payment_sent", consumedEvents[0].Event) + settledTransaction := consumedEvents[0].Properties.(*db.Transaction) assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, settledTransaction.State) assert.Equal(t, dbTransaction.PaymentHash, settledTransaction.PaymentHash) } @@ -260,9 +262,10 @@ func TestMarkSettled_Received(t *testing.T) { assert.NoError(t, err) assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, dbTransaction.State) - assert.Equal(t, 1, len(mockEventConsumer.GetConsumedEvents())) - assert.Equal(t, "nwc_payment_received", mockEventConsumer.GetConsumedEvents()[0].Event) - settledTransaction := mockEventConsumer.GetConsumedEvents()[0].Properties.(*db.Transaction) + consumedEvents := mockEventConsumer.WaitForConsumedEvents(1) + assert.Equal(t, 1, len(consumedEvents)) + assert.Equal(t, "nwc_payment_received", consumedEvents[0].Event) + settledTransaction := consumedEvents[0].Properties.(*db.Transaction) assert.Equal(t, &dbTransaction, settledTransaction) } @@ -311,9 +314,10 @@ func TestMarkFailed(t *testing.T) { assert.NoError(t, err) assert.True(t, markedFailed) assert.Equal(t, constants.TRANSACTION_STATE_FAILED, dbTransaction.State) - assert.Equal(t, 1, len(mockEventConsumer.GetConsumedEvents())) - assert.Equal(t, "nwc_payment_failed", mockEventConsumer.GetConsumedEvents()[0].Event) - settledTransaction := mockEventConsumer.GetConsumedEvents()[0].Properties.(*db.Transaction) + consumedEvents := mockEventConsumer.WaitForConsumedEvents(1) + assert.Equal(t, 1, len(consumedEvents)) + assert.Equal(t, "nwc_payment_failed", consumedEvents[0].Event) + settledTransaction := consumedEvents[0].Properties.(*db.Transaction) assert.Equal(t, &dbTransaction, settledTransaction) assert.Equal(t, "some routing error", settledTransaction.FailureReason) } @@ -399,8 +403,9 @@ func TestSendPaymentSync_FailedRemovesFeeReserve(t *testing.T) { assert.Zero(t, transaction.FeeReserveMsat) assert.Nil(t, transaction.Preimage) - assert.Equal(t, 1, len(mockEventConsumer.GetConsumedEvents())) - assert.Equal(t, "nwc_payment_failed", mockEventConsumer.GetConsumedEvents()[0].Event) + consumedEvents := mockEventConsumer.WaitForConsumedEvents(1) + assert.Equal(t, 1, len(consumedEvents)) + assert.Equal(t, "nwc_payment_failed", consumedEvents[0].Event) } func TestSendPaymentSync_PendingHasFeeReserve(t *testing.T) { diff --git a/transactions/self_payments_test.go b/transactions/self_payments_test.go index 0754bea5..97102f0c 100644 --- a/transactions/self_payments_test.go +++ b/transactions/self_payments_test.go @@ -399,11 +399,11 @@ func TestSendPaymentSync_SelfPayment_IsolatedAppToIsolatedApp(t *testing.T) { assert.Equal(t, int64(0), balanceMsat) // check notifications - assert.Equal(t, 2, len(mockEventConsumer.GetConsumedEvents())) + consumedEvents := mockEventConsumer.WaitForConsumedEvents(2) + assert.Equal(t, 2, len(consumedEvents)) // we can't guarantee which notification was processed first because events are published async // so swap them if they are back to front - consumedEvents := mockEventConsumer.GetConsumedEvents() if consumedEvents[1].Event == "nwc_payment_received" { consumedEvents[0], consumedEvents[1] = consumedEvents[1], consumedEvents[0] } From 3d229933897a13b56f5ddcf60734dfa300fe8875 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Tue, 11 Aug 2026 14:57:51 +0700 Subject: [PATCH 115/136] 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 --- api/api.go | 38 ++++++++++++-------- api/apps_test.go | 26 ++++++++++++++ frontend/src/screens/apps/NewApp.tsx | 8 +++-- frontend/src/screens/peers/ConnectPeer.tsx | 3 +- frontend/src/utils/safeReturnToUrl.ts | 17 +++++++++ frontend/vite.config.ts | 40 +++++++++++++++++----- http/http_service.go | 1 + 7 files changed, 105 insertions(+), 28 deletions(-) create mode 100644 frontend/src/utils/safeReturnToUrl.ts diff --git a/api/api.go b/api/api.go index 70c0d174..e4e7b12e 100644 --- a/api/api.go +++ b/api/api.go @@ -130,21 +130,7 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons responseBody.RelayUrls = relayUrls responseBody.Lud16 = lightningAddress - if createAppRequest.ReturnTo != "" { - returnToUrl, err := url.Parse(createAppRequest.ReturnTo) - if err == nil { - query := returnToUrl.Query() - for _, relayUrl := range relayUrls { - query.Add("relay", relayUrl) - } - query.Add("pubkey", *app.WalletPubkey) - if lightningAddress != "" && !app.Isolated { - query.Add("lud16", lightningAddress) - } - returnToUrl.RawQuery = query.Encode() - responseBody.ReturnTo = returnToUrl.String() - } - } + responseBody.ReturnTo = buildReturnToUrl(createAppRequest.ReturnTo, relayUrls, *app.WalletPubkey, lightningAddress, app.Isolated) var lud16 string if lightningAddress != "" && !app.Isolated { @@ -155,6 +141,28 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons return responseBody, nil } +// buildReturnToUrl adds the connection query parameters to the return_to +// URL the user will be redirected to. Only http and https URLs are accepted. +func buildReturnToUrl(returnTo string, relayUrls []string, walletPubkey string, lightningAddress string, isolated bool) string { + if returnTo == "" { + return "" + } + returnToUrl, err := url.Parse(returnTo) + if err != nil || (returnToUrl.Scheme != "http" && returnToUrl.Scheme != "https") { + return "" + } + query := returnToUrl.Query() + for _, relayUrl := range relayUrls { + query.Add("relay", relayUrl) + } + query.Add("pubkey", walletPubkey) + if lightningAddress != "" && !isolated { + query.Add("lud16", lightningAddress) + } + returnToUrl.RawQuery = query.Encode() + return returnToUrl.String() +} + func (api *api) UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) error { resolvedMaxAmountSat := ResolveToSat(updateAppRequest.MaxAmountSat, updateAppRequest.MaxAmountMsat, updateAppRequest.MaxAmount, nil) diff --git a/api/apps_test.go b/api/apps_test.go index ff00783f..6cd69daa 100644 --- a/api/apps_test.go +++ b/api/apps_test.go @@ -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) diff --git a/frontend/src/screens/apps/NewApp.tsx b/frontend/src/screens/apps/NewApp.tsx index 8aed62fc..e8924d68 100644 --- a/frontend/src/screens/apps/NewApp.tsx +++ b/frontend/src/screens/apps/NewApp.tsx @@ -50,6 +50,7 @@ import { import { useApp } from "src/hooks/useApp"; import { ConnectAppCard } from "src/screens/apps/ConnectAppCard"; import { handleRequestError } from "src/utils/handleRequestError"; +import { safeReturnToUrl } from "src/utils/safeReturnToUrl"; import Permissions from "../../components/Permissions"; import { AppStoreApp } from "../../components/connections/SuggestedAppData"; @@ -87,7 +88,7 @@ const NewAppInternal = ({ capabilities }: NewAppInternalProps) => { appStoreApp?.firefoxLink; const pubkey = queryParams.get("pubkey") ?? ""; - const returnTo = queryParams.get("return_to") ?? ""; + const returnTo = safeReturnToUrl(queryParams.get("return_to")) ?? ""; const nameParam = queryParams.get("name") || queryParams.get("c"); const [appName, setAppName] = useState(nameParam || appStoreApp?.title || ""); @@ -307,10 +308,11 @@ const NewAppInternal = ({ capabilities }: NewAppInternalProps) => { ); } - if (createAppResponse.returnTo) { + const returnToUrl = safeReturnToUrl(createAppResponse.returnTo); + if (returnToUrl) { // open connection URI directly in an app // eslint-disable-next-line react-hooks/immutability - window.location.href = createAppResponse.returnTo; + window.location.href = returnToUrl; return; } toast("App created"); diff --git a/frontend/src/screens/peers/ConnectPeer.tsx b/frontend/src/screens/peers/ConnectPeer.tsx index b6c1583c..601832ea 100644 --- a/frontend/src/screens/peers/ConnectPeer.tsx +++ b/frontend/src/screens/peers/ConnectPeer.tsx @@ -9,6 +9,7 @@ import { Label } from "src/components/ui/label"; import { splitSocketAddress } from "src/lib/utils"; import { ConnectPeerRequest } from "src/types"; import { request } from "src/utils/request"; +import { safeReturnToUrl } from "src/utils/safeReturnToUrl"; export default function ConnectPeer() { const navigate = useNavigate(); @@ -18,7 +19,7 @@ export default function ConnectPeer() { const [connectionString, setConnectionString] = React.useState( queryParams.get("peer") ?? "" ); - const returnTo = queryParams.get("return_to") ?? ""; + const returnTo = safeReturnToUrl(queryParams.get("return_to")) ?? ""; const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); diff --git a/frontend/src/utils/safeReturnToUrl.ts b/frontend/src/utils/safeReturnToUrl.ts new file mode 100644 index 00000000..4e3ac6d2 --- /dev/null +++ b/frontend/src/utils/safeReturnToUrl.ts @@ -0,0 +1,17 @@ +// Parses a return_to URL and only returns it if it is a +// http or https URL. Relative URLs are resolved against the +// current origin. +export function safeReturnToUrl(returnTo: string | null): string | undefined { + if (!returnTo) { + return undefined; + } + try { + const url = new URL(returnTo, window.location.origin); + if (url.protocol === "http:" || url.protocol === "https:") { + return url.toString(); + } + } catch { + // ignore invalid URLs + } + return undefined; +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index e34442fe..41326cb5 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -50,7 +50,7 @@ export default defineConfig(({ command }) => ({ maximumFileSizeToCacheInBytes: 3000000, // 3MB }, }), - ...(command === "serve" ? [insertDevCSPPlugin] : []), + ...(command === "serve" ? [insertDevCSPPlugin] : [insertProdCSPPlugin]), ], server: { port: process.env.VITE_PORT ? parseInt(process.env.VITE_PORT) : undefined, @@ -89,17 +89,39 @@ export default defineConfig(({ command }) => ({ const DEVELOPMENT_NONCE = "'nonce-DEVELOPMENT'"; +// when making changes here, also update the CSP header in http_service.go +const buildCSP = (nonce?: string) => + `default-src 'self'${nonce ? " " + nonce : ""}; img-src 'self' https://uploads.getalby-assets.com https://cdn.getalby-assets.com https://getalby.com; connect-src 'self' https://api.getalby.com https://getalby.com https://zapplanner.albylabs.com wss://relay.getalby.com wss://relay2.getalby.com; frame-src https://www.youtube-nocookie.com`; + +const insertCSPMetaTag = (comment: string, csp: string) => (html: string) => + html.replace( + "", + ` + + ` + ); + const insertDevCSPPlugin: Plugin = { name: "dev-csp", transformIndexHtml: { order: "pre", - handler: (html) => { - return html.replace( - "", - ` - - ` - ); - }, + handler: insertCSPMetaTag( + "DEV-ONLY CSP - when making changes here, also update the CSP header in http_service.go (without the nonce!)", + buildCSP(DEVELOPMENT_NONCE) + ), + }, +}; + +// the same CSP is served as a HTTP header in http mode (see http_service.go). +// The meta tag ensures the policy also applies where no HTTP headers are set, +// e.g. in the desktop app. +const insertProdCSPPlugin: Plugin = { + name: "prod-csp", + transformIndexHtml: { + order: "pre", + handler: insertCSPMetaTag( + "when making changes here, also update the CSP header in http_service.go", + buildCSP() + ), }, }; diff --git a/http/http_service.go b/http/http_service.go index 53bc1e0f..8d605389 100644 --- a/http/http_service.go +++ b/http/http_service.go @@ -66,6 +66,7 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) { e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ ContentTypeNosniff: "nosniff", XFrameOptions: "DENY", + // when making changes here, also update the CSP in frontend/vite.config.ts ContentSecurityPolicy: "default-src 'self'; img-src 'self' https://uploads.getalby-assets.com https://cdn.getalby-assets.com https://getalby.com; connect-src 'self' https://api.getalby.com https://getalby.com https://zapplanner.albylabs.com wss://relay.getalby.com wss://relay2.getalby.com; frame-src https://www.youtube-nocookie.com", ReferrerPolicy: "no-referrer", })) From 5f9a88843cc13eb401b3bc8c45156425b222e8bd Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:05:56 +0700 Subject: [PATCH 116/136] 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 --- api/api.go | 136 ++++++++++++++++++++++++++++++++++++--- api/setup_test.go | 161 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 289 insertions(+), 8 deletions(-) create mode 100644 api/setup_test.go diff --git a/api/api.go b/api/api.go index e4e7b12e..fc53d8e0 100644 --- a/api/api.go +++ b/api/api.go @@ -2,8 +2,11 @@ package api import ( "context" + "crypto/tls" + "crypto/x509" "encoding/hex" "encoding/json" + "encoding/pem" "errors" "flag" "fmt" @@ -11,6 +14,7 @@ import ( "net/http" "net/url" "os" + "path/filepath" "slices" "strconv" "strings" @@ -19,6 +23,7 @@ import ( "time" "github.com/sirupsen/logrus" + "gopkg.in/macaroon.v2" "gorm.io/datatypes" "gorm.io/gorm" @@ -1805,12 +1810,18 @@ func (api *api) Setup(ctx context.Context, setupRequest *SetupRequest) error { } } if setupRequest.LNDCertFile != "" { - certBytes, err := os.ReadFile(setupRequest.LNDCertFile) + // The file path is provided by the (unauthenticated) setup request, so + // only persist the content if it parses as a certificate. Storing the + // re-encoded certificate(s) guarantees nothing but the parsed structure + // reaches the database - e.g. a private key bundled in the same PEM file + // is dropped rather than persisted. + certHex, err := readAndCanonicalizeLNDCert(setupRequest.LNDCertFile) if err != nil { - logger.Logger.WithError(err).Error("Failed to read lnd cert file") - return err + // Return a generic error and log the detail server-side so the + // response is not a file existence/readability oracle. + logger.Logger.WithError(err).Error("Failed to process lnd cert file") + return errors.New("invalid LND certificate file") } - certHex := hex.EncodeToString(certBytes) err = api.cfg.SetUpdate("LNDCertHex", certHex, setupRequest.UnlockPassword) if err != nil { logger.Logger.WithError(err).Error("Failed to save lnd cert hex") @@ -1818,12 +1829,17 @@ func (api *api) Setup(ctx context.Context, setupRequest *SetupRequest) error { } } if setupRequest.LNDMacaroonFile != "" { - macaroonBytes, err := os.ReadFile(setupRequest.LNDMacaroonFile) + // The file path is provided by the (unauthenticated) setup request, so + // only persist the content if it parses as a macaroon. Storing the + // re-marshalled macaroon guarantees only the parsed structure reaches + // the database. + macaroonHex, err := readAndCanonicalizeLNDMacaroon(setupRequest.LNDMacaroonFile) if err != nil { - logger.Logger.WithError(err).Error("Failed to read lnd macaroon file") - return err + // Return a generic error and log the detail server-side so the + // response is not a file existence/readability oracle. + logger.Logger.WithError(err).Error("Failed to process lnd macaroon file") + return errors.New("invalid LND macaroon file") } - macaroonHex := hex.EncodeToString(macaroonBytes) err = api.cfg.SetUpdate("LNDMacaroonHex", macaroonHex, setupRequest.UnlockPassword) if err != nil { logger.Logger.WithError(err).Error("Failed to save lnd macaroon hex") @@ -1863,6 +1879,15 @@ func (api *api) Setup(ctx context.Context, setupRequest *SetupRequest) error { } if setupRequest.CLNLightningDir != "" { + // The directory path is provided by the (unauthenticated) setup request. + // Validate that it holds the expected CLN TLS credentials before saving, + // so the path cannot be used as an existence/readability oracle for + // arbitrary directories (the failure otherwise surfaces via startupError + // on the anonymous /api/info response). + if err := validateCLNLightningDir(setupRequest.CLNLightningDir, setupRequest.CLNAddressHold != ""); err != nil { + logger.Logger.WithError(err).Error("Failed to validate CLN lightning directory") + return errors.New("invalid CLN lightning directory") + } err = api.cfg.SetUpdate("CLNLightningDir", setupRequest.CLNLightningDir, setupRequest.UnlockPassword) if err != nil { logger.Logger.WithError(err).Error("Failed to save CLN Lightning directory path") @@ -1881,6 +1906,101 @@ func (api *api) Setup(ctx context.Context, setupRequest *SetupRequest) error { return nil } +// readAndCanonicalizeLNDCert reads the LND TLS certificate at the given path, +// validates that it contains at least one parseable certificate, and returns +// the hex-encoded re-encoding of only the parsed certificate(s). Any non +// CERTIFICATE PEM blocks (e.g. a bundled private key) are discarded so they are +// never persisted. Callers must not reflect the returned error to the client. +func readAndCanonicalizeLNDCert(path string) (string, error) { + raw, err := os.ReadFile(path) + if err != nil { + return "", fmt.Errorf("failed to read LND cert file: %w", err) + } + + var canonical []byte + rest := raw + for { + var block *pem.Block + block, rest = pem.Decode(rest) + if block == nil { + break + } + if block.Type != "CERTIFICATE" { + continue + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return "", fmt.Errorf("failed to parse LND certificate: %w", err) + } + canonical = append(canonical, pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + })...) + } + if len(canonical) == 0 { + return "", errors.New("no valid certificate found in LND cert file") + } + + return hex.EncodeToString(canonical), nil +} + +// readAndCanonicalizeLNDMacaroon reads the LND macaroon at the given path, +// validates that it is a well-formed macaroon, and returns the hex-encoded +// re-marshalling so that only the parsed structure is persisted. Callers must +// not reflect the returned error to the client. +func readAndCanonicalizeLNDMacaroon(path string) (string, error) { + raw, err := os.ReadFile(path) + if err != nil { + return "", fmt.Errorf("failed to read LND macaroon file: %w", err) + } + + mac := &macaroon.Macaroon{} + if err := mac.UnmarshalBinary(raw); err != nil { + return "", fmt.Errorf("failed to parse LND macaroon: %w", err) + } + canonical, err := mac.MarshalBinary() + if err != nil { + return "", fmt.Errorf("failed to marshal LND macaroon: %w", err) + } + + return hex.EncodeToString(canonical), nil +} + +// validateCLNLightningDir checks that the given directory holds the CLN TLS +// credentials that will later be loaded at connect time (ca.pem, client.pem, +// client-key.pem), for each gRPC server name the config will use. This mirrors +// the parses performed by the CLN client's loadTLSCredentials so a directory +// that passes here is one CLN can actually use. Callers must not reflect the +// returned error to the client. +func validateCLNLightningDir(lightningDir string, hold bool) error { + // "cln" reads the directory directly; other server names are joined as a + // subdirectory, matching loadTLSCredentials in lnclient/cln. + serverNames := []string{"cln"} + if hold { + serverNames = append(serverNames, "hold") + } + + for _, serverName := range serverNames { + dir := lightningDir + if serverName != "cln" { + dir = filepath.Join(dir, serverName) + } + + caPEM, err := os.ReadFile(filepath.Join(dir, "ca.pem")) + if err != nil { + return fmt.Errorf("failed to read CLN CA cert (%s): %w", serverName, err) + } + if !x509.NewCertPool().AppendCertsFromPEM(caPEM) { + return fmt.Errorf("failed to parse CLN CA cert (%s)", serverName) + } + if _, err := tls.LoadX509KeyPair(filepath.Join(dir, "client.pem"), filepath.Join(dir, "client-key.pem")); err != nil { + return fmt.Errorf("failed to load CLN client cert/key (%s): %w", serverName, err) + } + } + + return nil +} + func (api *api) GetWalletCapabilities(ctx context.Context) (*WalletCapabilitiesResponse, error) { lnClient := api.svc.GetLNClient() if lnClient == nil { diff --git a/api/setup_test.go b/api/setup_test.go new file mode 100644 index 00000000..2ac03ed5 --- /dev/null +++ b/api/setup_test.go @@ -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)) + }) +} From 4402d2fff88b86c9e4136f616260ebb83d304074 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:49:26 +0700 Subject: [PATCH 117/136] 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 --- .../wails/src/utils/request.ts | 2 +- http/http_service.go | 2 +- wails/wails_handlers.go | 69 ------------------- 3 files changed, 2 insertions(+), 71 deletions(-) diff --git a/frontend/platform_specific/wails/src/utils/request.ts b/frontend/platform_specific/wails/src/utils/request.ts index aef986bf..4d7d8dfb 100644 --- a/frontend/platform_specific/wails/src/utils/request.ts +++ b/frontend/platform_specific/wails/src/utils/request.ts @@ -10,7 +10,7 @@ export const request = async ( 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); } diff --git a/http/http_service.go b/http/http_service.go index 8d605389..bfcd59f9 100644 --- a/http/http_service.go +++ b/http/http_service.go @@ -1231,7 +1231,7 @@ func (httpSvc *HttpService) appsCreateHandler(c echo.Context) error { responseBody, err := httpSvc.api.CreateApp(&requestData) if err != nil { - logger.Logger.WithField("requestData", requestData).WithError(err).Error("Failed to save app") + logger.Logger.WithField("appName", requestData.Name).WithError(err).Error("Failed to save app") return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: fmt.Sprintf("Failed to save app: %v", err), }) diff --git a/wails/wails_handlers.go b/wails/wails_handlers.go index 5462a229..e24f62d9 100644 --- a/wails/wails_handlers.go +++ b/wails/wails_handlers.go @@ -43,7 +43,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -108,7 +107,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -189,7 +187,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -289,7 +286,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -379,7 +375,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -404,7 +399,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, "currency": currency, }).WithError(err).Error("Failed to get Bitcoin rate") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} @@ -420,7 +414,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -441,7 +434,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -452,7 +444,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -469,7 +460,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to get currencies") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -480,7 +470,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to get stories") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -494,7 +483,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -512,7 +500,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -546,7 +533,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -577,7 +563,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -600,7 +585,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -637,7 +621,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -696,7 +679,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -742,7 +724,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -758,7 +739,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -775,7 +755,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -792,9 +771,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - // Skip logging the body for this request as we don't want the - // unlock password to end up in any logs - // "body": body, }).WithError(err).Error("Failed to parse mnemonic request") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -803,9 +779,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - // Skip logging the body for this request as we don't want the - // unlock password to end up in any logs - // "body": body, }).WithError(err).Error("Failed to get mnemonic") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -818,7 +791,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -828,7 +800,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to store backup reminder") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -840,7 +811,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -850,7 +820,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to change unlock password") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -862,7 +831,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -872,7 +840,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to update settings") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -884,7 +851,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -894,7 +860,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to set auto unlock password") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -906,7 +871,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -921,7 +885,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -930,7 +893,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to setup node") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -942,7 +904,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -955,7 +916,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to open save file dialog") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -965,7 +925,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to create backup file") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -978,7 +937,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to create backup") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -990,7 +948,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1003,7 +960,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to open save file dialog") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1013,7 +969,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to open backup file") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1025,7 +980,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to restore backup") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1036,7 +990,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to check node health") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1048,7 +1001,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1057,7 +1009,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to generate BOLT-12 offer") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1068,7 +1019,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to get node commands") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1080,7 +1030,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1089,7 +1038,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to execute command") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1102,7 +1050,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to get auto swap configuration") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1114,7 +1061,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1123,7 +1069,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to enable auto swap") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1134,7 +1079,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to disable auto swap") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1146,7 +1090,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to get swap out info") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1157,7 +1100,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to get swap in info") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1169,7 +1111,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1178,7 +1119,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to initiate swap out") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1190,7 +1130,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1199,7 +1138,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to initiate swap in") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1211,7 +1149,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1220,7 +1157,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to process swap refund") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1235,7 +1171,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1245,7 +1180,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to set node alias") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1259,7 +1193,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to send event") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1277,7 +1210,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -1373,7 +1305,6 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string logger.Logger.WithFields(logrus.Fields{ "route": route, "method": method, - "body": body, }).WithError(err).Error("Failed to get log output") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } From 621db07fc8a6e7c1ac69c34cd5d09fdc355dd0bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:40:39 +0530 Subject: [PATCH 118/136] build(deps): bump @fontsource-variable/figtree from 5.2.10 to 5.3.0 in /frontend (#2519) 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package.json | 2 +- frontend/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index b46ba73b..9638d180 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@base-ui/react": "^1.5.0", - "@fontsource-variable/figtree": "^5.2.10", + "@fontsource-variable/figtree": "^5.3.0", "@fontsource-variable/inter": "^5.3.0", "@getalby/lightning-tools": "^8.1.0", "@getalby/sdk": "^8.0.3", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 8dddf83d..a2468881 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1329,10 +1329,10 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.11.tgz#a269e055e40e2f45873bae9d1a2fdccbd314ea3f" integrity sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg== -"@fontsource-variable/figtree@^5.2.10": - version "5.2.10" - resolved "https://registry.yarnpkg.com/@fontsource-variable/figtree/-/figtree-5.2.10.tgz#ad72d1b91646918073108e06464974d4f132b878" - integrity sha512-a5Gumbpy3mdd+Yg31g6Qb7CmjYbrfyutJa3bWfP5q8A4GclIOwX7mI+ZuSHsJnw/mHvW6r9oh1AHJcJTIxK4JA== +"@fontsource-variable/figtree@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@fontsource-variable/figtree/-/figtree-5.3.0.tgz#df8e3e467d3cceb1eb982ae2ed04a4accb284f65" + integrity sha512-VRVodD7OiG7apQwE8/2fMjaqpY/a0qRJqL5HtYj8CKIVps+9amaFcdfKmDIf7iqYQx6iPLlArVXAfKeIZmVcNQ== "@fontsource-variable/inter@^5.3.0": version "5.3.0" From 7c7dfa687600085edee78645a1c3bbaec4df240f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:48:31 +0530 Subject: [PATCH 119/136] 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package.json | 2 +- frontend/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 9638d180..dae351c3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -35,7 +35,7 @@ "date-fns": "^4.1.0", "dayjs": "^1.11.20", "embla-carousel-react": "^8.6.0", - "lucide-react": "^1.7.0", + "lucide-react": "^1.28.0", "qr-code-styling": "^1.9.2", "radix-ui": "^1.4.3", "react": "^19.2.6", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index a2468881..8a287667 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -4781,10 +4781,10 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -lucide-react@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-1.7.0.tgz#ad72fe48ebe1e5631a9cffdc94fc99dd853aa247" - integrity sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg== +lucide-react@^1.28.0: + version "1.28.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-1.28.0.tgz#d955536afb0df490a4e97faf068ceb8c6b2dafbf" + integrity sha512-fARAFJULsGuDDydjp6+6blekG/sBIM29TerzLjc9bQUKAcEfrSc4ZQKb25KRz4OMKd87cZTb5dgq0w/T6KufVg== magic-string@^0.30.21, magic-string@^0.30.3: version "0.30.21" From d3847fdaae8481cbdafb02f25a3f0c287b37efdd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 16:20:48 +0530 Subject: [PATCH 120/136] 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] * fix: remove stale react paths override in tsconfig --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Adithya Vardhan --- frontend/package.json | 2 +- frontend/tsconfig.json | 6 +- frontend/yarn.lock | 496 ++++++++++++++++------------------------- 3 files changed, 198 insertions(+), 306 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index dae351c3..5984d891 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -76,7 +76,7 @@ "tailwindcss": "^4.3.0", "typescript": "^5.9.3", "typescript-eslint": "^8.61.0", - "vite": "^5.4.0", + "vite": "^8.2.0", "vite-plugin-pwa": "^1.3.0", "vite-tsconfig-paths": "^6.1.1" }, diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 94765c8d..8e784faf 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -20,11 +20,7 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - - "paths": { - "react": ["./node_modules/@types/react"] - } + "noFallthroughCasesInSwitch": true }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 8a287667..9dfc2189 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1119,121 +1119,6 @@ dependencies: tslib "^2.4.0" -"@esbuild/aix-ppc64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" - integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== - -"@esbuild/android-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" - integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== - -"@esbuild/android-arm@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" - integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== - -"@esbuild/android-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" - integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== - -"@esbuild/darwin-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" - integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== - -"@esbuild/darwin-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" - integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== - -"@esbuild/freebsd-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" - integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== - -"@esbuild/freebsd-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" - integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== - -"@esbuild/linux-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" - integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== - -"@esbuild/linux-arm@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" - integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== - -"@esbuild/linux-ia32@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" - integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== - -"@esbuild/linux-loong64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" - integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== - -"@esbuild/linux-mips64el@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" - integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== - -"@esbuild/linux-ppc64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" - integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== - -"@esbuild/linux-riscv64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" - integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== - -"@esbuild/linux-s390x@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" - integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== - -"@esbuild/linux-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" - integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== - -"@esbuild/netbsd-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" - integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== - -"@esbuild/openbsd-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" - integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== - -"@esbuild/sunos-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" - integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== - -"@esbuild/win32-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" - integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== - -"@esbuild/win32-ia32@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" - integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== - -"@esbuild/win32-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" - integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== - "@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1": version "4.9.1" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" @@ -1489,6 +1374,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@oxc-project/types@=0.143.0": + version "0.143.0" + resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.143.0.tgz#c3e4f3178b7b54e4dd194eac6d45258a60f0092b" + integrity sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA== + "@radix-ui/number@1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.1.1.tgz#7b2c9225fbf1b126539551f5985769d0048d9090" @@ -2168,6 +2058,76 @@ resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.1.tgz#78244efe12930c56fd255d7923865857c41ac8cb" integrity sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw== +"@rolldown/binding-android-arm64@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz#001b8b0b01844701efda1bb6bed84b681c4a488b" + integrity sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw== + +"@rolldown/binding-darwin-arm64@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz#5e87c602ed634a6fef092e2162e24fbfb881c4ec" + integrity sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA== + +"@rolldown/binding-darwin-x64@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz#f32e0b286714bd03a421d693415d05d97d265b77" + integrity sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA== + +"@rolldown/binding-freebsd-x64@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz#5f38ad5761b6b7b21b57a99566bb52634c60ab19" + integrity sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg== + +"@rolldown/binding-linux-arm-gnueabihf@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz#ab4dcd07f1bd88e8d659ae0c3bb9d2f290adb897" + integrity sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw== + +"@rolldown/binding-linux-arm64-gnu@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz#d279b7016039a725fb66d82784b9841f42df83da" + integrity sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw== + +"@rolldown/binding-linux-arm64-musl@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz#d08bbc93d2742214548c5adf7df7788944e5a89a" + integrity sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q== + +"@rolldown/binding-linux-ppc64-gnu@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz#6418e63745b3193f26ab3bb88744b3a4a1356d7c" + integrity sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w== + +"@rolldown/binding-linux-s390x-gnu@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz#77ec30d0704cf4eb1cb4a63f501c9852c6728cf4" + integrity sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg== + +"@rolldown/binding-linux-x64-gnu@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz#3b9b6e0dd3e86c597f42858748ca25f1dfd58ed8" + integrity sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w== + +"@rolldown/binding-linux-x64-musl@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz#f78033c592c8bd2af48284a45f8e4baaa0befbf5" + integrity sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A== + +"@rolldown/binding-openharmony-arm64@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz#36e951f5a6fca922a5205e283d0a82b9f98199ca" + integrity sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug== + +"@rolldown/binding-win32-arm64-msvc@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz#c1e494ac47e13bd857fca0b3ad59c33580241f7e" + integrity sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw== + +"@rolldown/binding-win32-x64-msvc@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz#b0effffcd6872f8a021373eb437916b1b52283a4" + integrity sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg== + "@rolldown/pluginutils@^1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz#e3fcee093fbb5ce765e1ad088ff4de2889f6f9be" @@ -2218,101 +2178,51 @@ estree-walker "^2.0.2" picomatch "^4.0.2" -"@rollup/rollup-android-arm-eabi@4.46.2": - version "4.46.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz#292e25953d4988d3bd1af0f5ebbd5ee4d65c90b4" - integrity sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA== - "@rollup/rollup-android-arm-eabi@4.61.1": version "4.61.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz#ce83f259581a4f5e6255d92902249f0366a15dd3" integrity sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA== -"@rollup/rollup-android-arm64@4.46.2": - version "4.46.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz#053b3def3451e6fc1a9078188f22799e868d7c59" - integrity sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ== - "@rollup/rollup-android-arm64@4.61.1": version "4.61.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz#1a763329ffbc2a19057128ac266a1a46782a5f17" integrity sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw== -"@rollup/rollup-darwin-arm64@4.46.2": - version "4.46.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz#98d90445282dec54fd05440305a5e8df79a91ece" - integrity sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ== - "@rollup/rollup-darwin-arm64@4.61.1": version "4.61.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz#ff9cffe102d29e052f49e5017fd036142f9bb7ef" integrity sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA== -"@rollup/rollup-darwin-x64@4.46.2": - version "4.46.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz#fe05f95a736423af5f9c3a59a70f41ece52a1f20" - integrity sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA== - "@rollup/rollup-darwin-x64@4.61.1": version "4.61.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz#1ef1e8f5bd16865d8d2f377a58e7622820b3dda3" integrity sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ== -"@rollup/rollup-freebsd-arm64@4.46.2": - version "4.46.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz#41e1fbdc1f8c3dc9afb6bc1d6e3fb3104bd81eee" - integrity sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg== - "@rollup/rollup-freebsd-arm64@4.61.1": version "4.61.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz#cb7d041010788213879f663d3100c4320c0910d9" integrity sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw== -"@rollup/rollup-freebsd-x64@4.46.2": - version "4.46.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz#69131e69cb149d547abb65ef3b38fc746c940e24" - integrity sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw== - "@rollup/rollup-freebsd-x64@4.61.1": version "4.61.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz#25d4b4d7e52bb1a144fd130209732e5d0518251a" integrity sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw== -"@rollup/rollup-linux-arm-gnueabihf@4.46.2": - version "4.46.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz#977ded91c7cf6fc0d9443bb9c0a064e45a805267" - integrity sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA== - "@rollup/rollup-linux-arm-gnueabihf@4.61.1": version "4.61.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz#9569a8dd884a22950df4461de8b26c750390531c" integrity sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA== -"@rollup/rollup-linux-arm-musleabihf@4.46.2": - version "4.46.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz#dc034fc3c0f0eb5c75b6bc3eca3b0b97fd35f49a" - integrity sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ== - "@rollup/rollup-linux-arm-musleabihf@4.61.1": version "4.61.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz#33d0080b8cce62df8c3e6240875abf0d6c125cda" integrity sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ== -"@rollup/rollup-linux-arm64-gnu@4.46.2": - version "4.46.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz#5e92613768d3de3ffcabc965627dd0a59b3e7dfc" - integrity sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng== - "@rollup/rollup-linux-arm64-gnu@4.61.1": version "4.61.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz#9c66a9b4d1746595680eec691e136f8efcfe3d78" integrity sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg== -"@rollup/rollup-linux-arm64-musl@4.46.2": - version "4.46.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz#2a44f88e83d28b646591df6e50aa0a5a931833d8" - integrity sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg== - "@rollup/rollup-linux-arm64-musl@4.61.1": version "4.61.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz#e37aa97039af9dbb76a324148db06c6266acc9a0" @@ -2328,16 +2238,6 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz#778c983d0050792d85a62e2c74009fff821e5606" integrity sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ== -"@rollup/rollup-linux-loongarch64-gnu@4.46.2": - version "4.46.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz#bd5897e92db7fbf7dc456f61d90fff96c4651f2e" - integrity sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA== - -"@rollup/rollup-linux-ppc64-gnu@4.46.2": - version "4.46.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz#a7065025411c14ad9ec34cc1cd1414900ec2a303" - integrity sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw== - "@rollup/rollup-linux-ppc64-gnu@4.61.1": version "4.61.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz#74f34764b688a6081c8f75e155b58d2cdb39112f" @@ -2348,51 +2248,26 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz#821ccda4531dcdb42e56adddc178907615a6da07" integrity sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw== -"@rollup/rollup-linux-riscv64-gnu@4.46.2": - version "4.46.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz#17f9c0c675e13ef4567cfaa3730752417257ccc3" - integrity sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ== - "@rollup/rollup-linux-riscv64-gnu@4.61.1": version "4.61.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz#849fa5c6b43fc6c4d257671fcebd701fe6947bd6" integrity sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g== -"@rollup/rollup-linux-riscv64-musl@4.46.2": - version "4.46.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz#bc6ed3db2cedc1ba9c0a2183620fe2f792c3bf3f" - integrity sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw== - "@rollup/rollup-linux-riscv64-musl@4.61.1": version "4.61.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz#80a8711af6c316ce448b93294c4a0891c2ddacbe" integrity sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ== -"@rollup/rollup-linux-s390x-gnu@4.46.2": - version "4.46.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz#440c4f6753274e2928e06d2a25613e5a1cf97b41" - integrity sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA== - "@rollup/rollup-linux-s390x-gnu@4.61.1": version "4.61.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz#90acba54363c128b73dbb310642b977b5e6b9daa" integrity sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g== -"@rollup/rollup-linux-x64-gnu@4.46.2": - version "4.46.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz#1e936446f90b2574ea4a83b4842a762cc0a0aed3" - integrity sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA== - "@rollup/rollup-linux-x64-gnu@4.61.1": version "4.61.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz#32085c3f532c59269824ed9239e13f5acbe182b9" integrity sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q== -"@rollup/rollup-linux-x64-musl@4.46.2": - version "4.46.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz#c6f304dfba1d5faf2be5d8b153ccbd8b5d6f1166" - integrity sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA== - "@rollup/rollup-linux-x64-musl@4.61.1": version "4.61.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz#becf0e9e29d77e7d04de841bda635e9f73b89dfb" @@ -2408,21 +2283,11 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz#b367bc49355b7ec2508cbad970721ac78b41bf0c" integrity sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA== -"@rollup/rollup-win32-arm64-msvc@4.46.2": - version "4.46.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz#b4ad4a79219892aac112ed1c9d1356cad0566ef5" - integrity sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g== - "@rollup/rollup-win32-arm64-msvc@4.61.1": version "4.61.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz#d7be3478b45d6434d13cbe62cce29c4fb6366948" integrity sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g== -"@rollup/rollup-win32-ia32-msvc@4.46.2": - version "4.46.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz#b1b22eb2a9568048961e4a6f540438b4a762aa62" - integrity sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ== - "@rollup/rollup-win32-ia32-msvc@4.61.1": version "4.61.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz#7ec5801739cae3bf119f419b77a10cb0dc49f40f" @@ -2433,11 +2298,6 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz#94a83572bf151772d945ffd4777ded305cd8c346" integrity sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ== -"@rollup/rollup-win32-x64-msvc@4.46.2": - version "4.46.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz#87079f137b5fdb75da11508419aa998cc8cc3d8b" - integrity sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg== - "@rollup/rollup-win32-x64-msvc@4.61.1": version "4.61.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz#cb98a579ab6eec9940bda7a736df5e2b94eb0a27" @@ -2753,11 +2613,6 @@ resolved "https://registry.yarnpkg.com/@types/esrecurse/-/esrecurse-4.3.1.tgz#6f636af962fbe6191b830bd676ba5986926bccec" integrity sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw== -"@types/estree@1.0.8": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" - integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== - "@types/estree@1.0.9", "@types/estree@^1.0.0", "@types/estree@^1.0.6", "@types/estree@^1.0.8": version "1.0.9" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.9.tgz#cf3f0e876d7bee15a93ab925b82bf570a3904a24" @@ -3679,35 +3534,6 @@ es-toolkit@^1.46.0: resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.46.1.tgz#38ca27191a98a867fc544b81cf1477a68947fb06" integrity sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ== -esbuild@^0.21.3: - version "0.21.5" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" - integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== - optionalDependencies: - "@esbuild/aix-ppc64" "0.21.5" - "@esbuild/android-arm" "0.21.5" - "@esbuild/android-arm64" "0.21.5" - "@esbuild/android-x64" "0.21.5" - "@esbuild/darwin-arm64" "0.21.5" - "@esbuild/darwin-x64" "0.21.5" - "@esbuild/freebsd-arm64" "0.21.5" - "@esbuild/freebsd-x64" "0.21.5" - "@esbuild/linux-arm" "0.21.5" - "@esbuild/linux-arm64" "0.21.5" - "@esbuild/linux-ia32" "0.21.5" - "@esbuild/linux-loong64" "0.21.5" - "@esbuild/linux-mips64el" "0.21.5" - "@esbuild/linux-ppc64" "0.21.5" - "@esbuild/linux-riscv64" "0.21.5" - "@esbuild/linux-s390x" "0.21.5" - "@esbuild/linux-x64" "0.21.5" - "@esbuild/netbsd-x64" "0.21.5" - "@esbuild/openbsd-x64" "0.21.5" - "@esbuild/sunos-x64" "0.21.5" - "@esbuild/win32-arm64" "0.21.5" - "@esbuild/win32-ia32" "0.21.5" - "@esbuild/win32-x64" "0.21.5" - escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" @@ -4631,56 +4457,111 @@ lightningcss-android-arm64@1.32.0: resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz#f033885116dfefd9c6f54787523e3514b61e1968" integrity sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg== +lightningcss-android-arm64@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz#9a6841f88ae50fc83502903892b41af41bc2b907" + integrity sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg== + lightningcss-darwin-arm64@1.32.0: version "1.32.0" resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz#50b71871b01c8199584b649e292547faea7af9b5" integrity sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ== +lightningcss-darwin-arm64@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz#c0f2c31c0bfd19fa4dd3f18e957a1f1a152097d6" + integrity sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg== + lightningcss-darwin-x64@1.32.0: version "1.32.0" resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz#35f3e97332d130b9ca181e11b568ded6aebc6d5e" integrity sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w== +lightningcss-darwin-x64@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz#cb0705965acb538c6683949ce6925fb3cdf7c361" + integrity sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ== + lightningcss-freebsd-x64@1.32.0: version "1.32.0" resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz#9777a76472b64ed6ff94342ad64c7bafd794a575" integrity sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig== +lightningcss-freebsd-x64@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz#763538828b26bab2680dadafcc84ee78b0eb502b" + integrity sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg== + lightningcss-linux-arm-gnueabihf@1.32.0: version "1.32.0" resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz#13ae652e1ab73b9135d7b7da172f666c410ad53d" integrity sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw== +lightningcss-linux-arm-gnueabihf@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz#6862e3176a331aedbdec1ed352b4d7d0dd0784de" + integrity sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ== + lightningcss-linux-arm64-gnu@1.32.0: version "1.32.0" resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz#417858795a94592f680123a1b1f9da8a0e1ef335" integrity sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ== +lightningcss-linux-arm64-gnu@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz#c6a3a2ed15141daf6bdc2628930f8e39bdf473aa" + integrity sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg== + lightningcss-linux-arm64-musl@1.32.0: version "1.32.0" resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz#6be36692e810b718040802fd809623cffe732133" integrity sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg== +lightningcss-linux-arm64-musl@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz#7fa1334971fc82845f9827df6ef8a0b20914bac6" + integrity sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ== + lightningcss-linux-x64-gnu@1.32.0: version "1.32.0" resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz#0b7803af4eb21cfd38dd39fe2abbb53c7dd091f6" integrity sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA== +lightningcss-linux-x64-gnu@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz#8b927862ea8c2bbc6831a46509244b50d9936e55" + integrity sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg== + lightningcss-linux-x64-musl@1.32.0: version "1.32.0" resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz#88dc8ba865ddddb1ac5ef04b0f161804418c163b" integrity sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg== +lightningcss-linux-x64-musl@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz#0c525bb077dfd94404c059cfe42dad797e96aeaf" + integrity sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw== + lightningcss-win32-arm64-msvc@1.32.0: version "1.32.0" resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz#4f30ba3fa5e925f5b79f945e8cc0d176c3b1ab38" integrity sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw== +lightningcss-win32-arm64-msvc@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz#850ee1103dac989cfab50e3ac22d1a69e394e63d" + integrity sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA== + lightningcss-win32-x64-msvc@1.32.0: version "1.32.0" resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz#141aa5605645064928902bb4af045fa7d9f4220a" integrity sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q== +lightningcss-win32-x64-msvc@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz#e343ae152eed3609dc6e11949d1a3bf39a1c946f" + integrity sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA== + lightningcss@1.32.0: version "1.32.0" resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.32.0.tgz#b85aae96486dcb1bf49a7c8571221273f4f1e4a9" @@ -4700,6 +4581,25 @@ lightningcss@1.32.0: lightningcss-win32-arm64-msvc "1.32.0" lightningcss-win32-x64-msvc "1.32.0" +lightningcss@^1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.33.0.tgz#c08867d71a79385c6e190214fd72fef3e5f95f0b" + integrity sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA== + dependencies: + detect-libc "^2.0.3" + optionalDependencies: + lightningcss-android-arm64 "1.33.0" + lightningcss-darwin-arm64 "1.33.0" + lightningcss-darwin-x64 "1.33.0" + lightningcss-freebsd-x64 "1.33.0" + lightningcss-linux-arm-gnueabihf "1.33.0" + lightningcss-linux-arm64-gnu "1.33.0" + lightningcss-linux-arm64-musl "1.33.0" + lightningcss-linux-x64-gnu "1.33.0" + lightningcss-linux-x64-musl "1.33.0" + lightningcss-win32-arm64-msvc "1.33.0" + lightningcss-win32-x64-msvc "1.33.0" + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -4862,10 +4762,10 @@ ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nanoid@^3.3.11: - version "3.3.11" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" - integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== +nanoid@^3.3.17: + version "3.3.17" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.17.tgz#f1c3aa253c52547956a52c50bff754316f61037a" + integrity sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g== natural-compare@^1.4.0: version "1.4.0" @@ -5048,10 +4948,10 @@ picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -picomatch@^4.0.2, picomatch@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" - integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== +picomatch@^4.0.2, picomatch@^4.0.3, picomatch@^4.0.4, picomatch@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.5.tgz#51ea57a17d86f605f81039595fbc40ed06a55fab" + integrity sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A== possible-typed-array-names@^1.0.0: version "1.1.0" @@ -5066,12 +4966,12 @@ postcss-selector-parser@6.0.10: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss@^8.4.43: - version "8.5.6" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" - integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== +postcss@^8.5.23: + version "8.5.26" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.26.tgz#6e75135780c7e10df3433bf2266c552d35c8c620" + integrity sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ== dependencies: - nanoid "^3.3.11" + nanoid "^3.3.17" picocolors "^1.1.1" source-map-js "^1.2.1" @@ -5392,34 +5292,28 @@ rfdc@^1.4.1: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== -rollup@^4.20.0: - version "4.46.2" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.46.2.tgz#09b1a45d811e26d09bed63dc3ecfb6831c16ce32" - integrity sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg== +rolldown@~1.2.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.2.3.tgz#103bdcbbd575d51265277b8b510f080827b6eb6f" + integrity sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A== dependencies: - "@types/estree" "1.0.8" + "@oxc-project/types" "=0.143.0" + "@rolldown/pluginutils" "^1.0.0" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.46.2" - "@rollup/rollup-android-arm64" "4.46.2" - "@rollup/rollup-darwin-arm64" "4.46.2" - "@rollup/rollup-darwin-x64" "4.46.2" - "@rollup/rollup-freebsd-arm64" "4.46.2" - "@rollup/rollup-freebsd-x64" "4.46.2" - "@rollup/rollup-linux-arm-gnueabihf" "4.46.2" - "@rollup/rollup-linux-arm-musleabihf" "4.46.2" - "@rollup/rollup-linux-arm64-gnu" "4.46.2" - "@rollup/rollup-linux-arm64-musl" "4.46.2" - "@rollup/rollup-linux-loongarch64-gnu" "4.46.2" - "@rollup/rollup-linux-ppc64-gnu" "4.46.2" - "@rollup/rollup-linux-riscv64-gnu" "4.46.2" - "@rollup/rollup-linux-riscv64-musl" "4.46.2" - "@rollup/rollup-linux-s390x-gnu" "4.46.2" - "@rollup/rollup-linux-x64-gnu" "4.46.2" - "@rollup/rollup-linux-x64-musl" "4.46.2" - "@rollup/rollup-win32-arm64-msvc" "4.46.2" - "@rollup/rollup-win32-ia32-msvc" "4.46.2" - "@rollup/rollup-win32-x64-msvc" "4.46.2" - fsevents "~2.3.2" + "@rolldown/binding-android-arm64" "1.2.3" + "@rolldown/binding-darwin-arm64" "1.2.3" + "@rolldown/binding-darwin-x64" "1.2.3" + "@rolldown/binding-freebsd-x64" "1.2.3" + "@rolldown/binding-linux-arm-gnueabihf" "1.2.3" + "@rolldown/binding-linux-arm64-gnu" "1.2.3" + "@rolldown/binding-linux-arm64-musl" "1.2.3" + "@rolldown/binding-linux-ppc64-gnu" "1.2.3" + "@rolldown/binding-linux-s390x-gnu" "1.2.3" + "@rolldown/binding-linux-x64-gnu" "1.2.3" + "@rolldown/binding-linux-x64-musl" "1.2.3" + "@rolldown/binding-openharmony-arm64" "1.2.3" + "@rolldown/binding-win32-arm64-msvc" "1.2.3" + "@rolldown/binding-win32-x64-msvc" "1.2.3" rollup@^4.53.3: version "4.61.1" @@ -5934,13 +5828,13 @@ tinyexec@^1.0.0, tinyexec@^1.0.4: resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.0.4.tgz#6c60864fe1d01331b2f17c6890f535d7e5385408" integrity sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw== -tinyglobby@^0.2.10, tinyglobby@^0.2.15: - version "0.2.15" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" - integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== +tinyglobby@^0.2.10, tinyglobby@^0.2.15, tinyglobby@^0.2.17: + version "0.2.17" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631" + integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g== dependencies: fdir "^6.5.0" - picomatch "^4.0.3" + picomatch "^4.0.4" to-regex-range@^5.0.1: version "5.0.1" @@ -6170,14 +6064,16 @@ vite-tsconfig-paths@^6.1.1: globrex "^0.1.2" tsconfck "^3.0.3" -vite@^5.4.0: - version "5.4.19" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.19.tgz#20efd060410044b3ed555049418a5e7d1998f959" - integrity sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA== +vite@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/vite/-/vite-8.2.0.tgz#902fcd3dc0312f553c85b6cbc4625dcaca2d8df8" + integrity sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ== dependencies: - esbuild "^0.21.3" - postcss "^8.4.43" - rollup "^4.20.0" + lightningcss "^1.33.0" + picomatch "^4.0.5" + postcss "^8.5.23" + rolldown "~1.2.0" + tinyglobby "^0.2.17" optionalDependencies: fsevents "~2.3.3" From ffc705e536c9a047bd09053e81ea6c4acc7eb6bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 16:22:33 +0530 Subject: [PATCH 121/136] 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] * chore: bump wails version to v2.14.0 --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Adithya Vardhan --- go.mod | 4 ++-- go.sum | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index d7e69397..991e421e 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/peterldowns/pgtestdb v0.1.1 github.com/stretchr/testify v1.11.1 github.com/tyler-smith/go-bip39 v1.1.0 - github.com/wailsapp/wails/v2 v2.12.0 + github.com/wailsapp/wails/v2 v2.14.0 gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.15.0 golang.org/x/crypto v0.54.0 golang.org/x/oauth2 v0.36.0 @@ -122,7 +122,7 @@ require ( github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee // indirect github.com/kkdai/bstream v1.0.0 // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/labstack/gommon v0.5.0 // indirect github.com/leaanthony/go-ansi-parser v1.6.1 // indirect github.com/leaanthony/gosod v1.0.4 // indirect diff --git a/go.sum b/go.sum index 23adf326..2288d0b2 100644 --- a/go.sum +++ b/go.sum @@ -312,8 +312,8 @@ github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -572,8 +572,8 @@ github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6N github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= -github.com/wailsapp/wails/v2 v2.12.0 h1:BHO/kLNWFHYjCzucxbzAYZWUjub1Tvb4cSguQozHn5c= -github.com/wailsapp/wails/v2 v2.12.0/go.mod h1:mo1bzK1DEJrobt7YrBjgxvb5Sihb1mhAY09hppbibQg= +github.com/wailsapp/wails/v2 v2.14.0 h1:+GlF6BM8Mg7Saa1lxUAzfZ6jsEMg2zQhQvUHtqB7xpc= +github.com/wailsapp/wails/v2 v2.14.0/go.mod h1:scxrgwfsv6yR6fE6cCF+Flfl+JeU+SR87T9x4kILJ6M= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -591,8 +591,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.12.1 h1:jEnl0leC9n7EsT9Rn339nC9e/9GWCMPXzmzowzxmY24= -gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.12.1/go.mod h1:1jAwB/XR4i3D72fz3qWAd41tQLYcOCGfWZHMagn5fNg= gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.15.0 h1:X6TmMsVcvwOo5CxnlOQ/104OBO7cdOvYcTIatS5yfu4= gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.15.0/go.mod h1:1jAwB/XR4i3D72fz3qWAd41tQLYcOCGfWZHMagn5fNg= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= From 852b30fa02d6c1203a12810aed9b1d6453856d57 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 16:36:15 +0530 Subject: [PATCH 122/136] 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 991e421e..fed9ed56 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( google.golang.org/grpc v1.79.3 google.golang.org/protobuf v1.36.10 gopkg.in/macaroon.v2 v2.1.0 - gorm.io/driver/postgres v1.6.0 + gorm.io/driver/postgres v1.6.2 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.2 ) @@ -109,7 +109,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.9.2 // indirect + github.com/jackc/pgx/v5 v5.10.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect github.com/jessevdk/go-flags v1.6.1 // indirect diff --git a/go.sum b/go.sum index 2288d0b2..2502c404 100644 --- a/go.sum +++ b/go.sum @@ -273,8 +273,8 @@ github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUO github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= -github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= @@ -817,8 +817,8 @@ gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk= gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY= gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= -gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= -gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/driver/postgres v1.6.2 h1:BvXQ/cNUg63q5TFNg672DmDcowZSFrNLkkA3Xe6GXq4= +gorm.io/driver/postgres v1.6.2/go.mod h1:0c4fQA44XhOklXDkgtuKqysHCycTa5i9e3EIpDGCwXk= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc= From e3278beffbd8a219b446673ecff2f1312d15e03e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 16:36:30 +0530 Subject: [PATCH 123/136] 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index fed9ed56..d1a33510 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/go-gormigrate/gormigrate/v2 v2.1.6 github.com/google/uuid v1.6.0 github.com/labstack/echo/v4 v4.15.4 - github.com/mattn/go-sqlite3 v1.14.48 + github.com/mattn/go-sqlite3 v1.14.49 github.com/nbd-wtf/ln-decodepay v1.13.0 github.com/orandin/lumberjackrus v1.0.1 github.com/peterldowns/pgtestdb v0.1.1 diff --git a/go.sum b/go.sum index 2502c404..63b0db9d 100644 --- a/go.sum +++ b/go.sum @@ -396,8 +396,8 @@ github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= -github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs= -github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= +github.com/mattn/go-sqlite3 v1.14.49 h1:B8jBHC3xhxZgxztrgruTuLucebnULQnx4W7cF7SAE9w= +github.com/mattn/go-sqlite3 v1.14.49/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= From 459b825cfb79ce5a3f7c72e51e9962f002944dc7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:07:03 +0530 Subject: [PATCH 124/136] 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index d1a33510..f6d71aee 100644 --- a/go.mod +++ b/go.mod @@ -141,7 +141,7 @@ require ( github.com/lightningnetwork/lnd/sqldb v1.0.13 // indirect github.com/lightningnetwork/lnd/ticker v1.1.1 // indirect github.com/lightningnetwork/lnd/tlv v1.3.2 // indirect - github.com/lightningnetwork/lnd/tor v1.1.6 // indirect + github.com/lightningnetwork/lnd/tor v1.1.7 // indirect github.com/ltcsuite/ltcd v0.23.5 // indirect github.com/ltcsuite/ltcd/chaincfg/chainhash v1.0.2 // indirect github.com/mailru/easyjson v0.9.0 // indirect @@ -261,7 +261,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/kelseyhightower/envconfig v1.4.0 github.com/labstack/echo-jwt/v4 v4.4.0 - github.com/lightningnetwork/lnd v0.21.0-beta + github.com/lightningnetwork/lnd v0.21.1-beta github.com/sirupsen/logrus v1.9.4 github.com/tyler-smith/go-bip32 v1.0.0 golang.org/x/exp v0.0.0-20250811191247-51f88131bc50 // indirect diff --git a/go.sum b/go.sum index 63b0db9d..0129435a 100644 --- a/go.sum +++ b/go.sum @@ -357,8 +357,8 @@ github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display h1:Y2WiPkBS github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= github.com/lightningnetwork/lightning-onion v1.3.0 h1:FqILgHjD6euc/Muo1VOzZ4+XDPuFnw6EYROBq0rR/5c= github.com/lightningnetwork/lightning-onion v1.3.0/go.mod h1:nP85zMHG7c0si/eHBbSQpuDCtnIXfSvFrK3tW6YWzmU= -github.com/lightningnetwork/lnd v0.21.0-beta h1:bDP5UH15E7DVGTztsmBPQLqgyilq5EXDrglvQFmRc3U= -github.com/lightningnetwork/lnd v0.21.0-beta/go.mod h1:HcKq9DyxbVEZXuR28TIyGbIIgAjANCxI+N6dqOnRBAA= +github.com/lightningnetwork/lnd v0.21.1-beta h1:XYr8CRY3EB5bQ8I43/6QbOZP1dRi/XI6dwzTHlS2jxQ= +github.com/lightningnetwork/lnd v0.21.1-beta/go.mod h1:iqqf8Cyq0F7ssUftb17+Y644OWVg2OMOpvWSyzympXs= github.com/lightningnetwork/lnd/actor v0.0.6 h1:Ge8N2wivARG+27qJBwTlB0vwsypStZYZy8vk4Zl38sU= github.com/lightningnetwork/lnd/actor v0.0.6/go.mod h1:YAsoniSbY/cAM9HTVNfZLvt7RI6swDxy6wzPspTcMZg= github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0= @@ -377,8 +377,8 @@ github.com/lightningnetwork/lnd/ticker v1.1.1 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6 github.com/lightningnetwork/lnd/ticker v1.1.1/go.mod h1:waPTRAAcwtu7Ji3+3k+u/xH5GHovTsCoSVpho0KDvdA= github.com/lightningnetwork/lnd/tlv v1.3.2 h1:MO4FCk7F4k5xPMqVZF6Nb/kOpxlwPrUQpYjmyKny5s0= github.com/lightningnetwork/lnd/tlv v1.3.2/go.mod h1:pJuiBj1ecr1WWLOtcZ+2+hu9Ey25aJWFIsjmAoPPnmc= -github.com/lightningnetwork/lnd/tor v1.1.6 h1:WHUumk7WgU6BUFsqHuqszI9P6nfhMeIG+rjJBlVE6OE= -github.com/lightningnetwork/lnd/tor v1.1.6/go.mod h1:qSRB8llhAK+a6kaTPWOLLXSZc6Hg8ZC0mq1sUQ/8JfI= +github.com/lightningnetwork/lnd/tor v1.1.7 h1:BfY1KQGz3LVaeaAGI4XYGYCwDQR1ADFD1P/uXsS2dQc= +github.com/lightningnetwork/lnd/tor v1.1.7/go.mod h1:DANQ6QCQBiR+CqrM+mKudgkMy6CR/RS4ALDbjb2W4FU= github.com/ltcsuite/ltcd v0.23.5 h1:MFWjmx2hCwxrUu9v0wdIPOSN7PHg9BWQeh+AO4FsVLI= github.com/ltcsuite/ltcd v0.23.5/go.mod h1:JV6swXR5m0cYFi0VYdQPp3UnMdaDQxaRUCaU1PPjb+g= github.com/ltcsuite/ltcd/chaincfg/chainhash v1.0.2 h1:xuWxvRKxLvOKuS7/Q/7I3tpc3cWAB0+hZpU8YdVqkzg= From 0c24ab84c1e59c999970a78ab821b87a7062a50e Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Tue, 11 Aug 2026 18:45:59 +0530 Subject: [PATCH 125/136] fix: avoid leaking raw postgres error details in duplicate key response (#2538) --- nip47/controllers/map_nip47_error.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/nip47/controllers/map_nip47_error.go b/nip47/controllers/map_nip47_error.go index 58e693fc..7acd7433 100644 --- a/nip47/controllers/map_nip47_error.go +++ b/nip47/controllers/map_nip47_error.go @@ -6,10 +6,12 @@ import ( "github.com/getAlby/hub/constants" "github.com/getAlby/hub/nip47/models" "github.com/getAlby/hub/transactions" + "gorm.io/gorm" ) func mapNip47Error(err error) *models.Error { code := constants.ERROR_INTERNAL + message := err.Error() if errors.Is(err, transactions.NewNotFoundError()) { code = constants.ERROR_NOT_FOUND } @@ -19,9 +21,13 @@ func mapNip47Error(err error) *models.Error { if errors.Is(err, transactions.NewQuotaExceededError()) { code = constants.ERROR_QUOTA_EXCEEDED } + if errors.Is(err, gorm.ErrDuplicatedKey) { + // avoid leaking raw driver/SQL error details (e.g. constraint names) to NWC apps + message = gorm.ErrDuplicatedKey.Error() + } return &models.Error{ Code: code, - Message: err.Error(), + Message: message, } } From 4c5bef42c61e1ae36a944d07ef7f2b58d11cddec Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Wed, 12 Aug 2026 12:29:03 +0700 Subject: [PATCH 126/136] 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 --- http/http_service.go | 2 +- http/http_service_test.go | 116 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/http/http_service.go b/http/http_service.go index bfcd59f9..25c9032f 100644 --- a/http/http_service.go +++ b/http/http_service.go @@ -142,7 +142,6 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) { readOnlyApiGroup.GET("/transactions/:paymentHash", httpSvc.lookupTransactionHandler) readOnlyApiGroup.GET("/balances", httpSvc.balancesHandler) readOnlyApiGroup.GET("/mempool", httpSvc.mempoolApiHandler) - readOnlyApiGroup.GET("/log/:type", httpSvc.getLogOutputHandler) readOnlyApiGroup.GET("/health", httpSvc.healthHandler) readOnlyApiGroup.GET("/commands", httpSvc.getCustomNodeCommandsHandler) readOnlyApiGroup.GET("/swaps", httpSvc.listSwapsHandler) @@ -192,6 +191,7 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) { fullAccessApiGroup.POST("/swaps/in", httpSvc.initiateSwapInHandler) fullAccessApiGroup.POST("/swaps/refund", httpSvc.refundSwapHandler) fullAccessApiGroup.GET("/swaps/mnemonic", httpSvc.swapMnemonicHandler) + fullAccessApiGroup.GET("/log/:type", httpSvc.getLogOutputHandler) fullAccessApiGroup.POST("/autoswap", httpSvc.enableAutoSwapOutHandler) fullAccessApiGroup.DELETE("/autoswap", httpSvc.disableAutoSwapOutHandler) fullAccessApiGroup.POST("/node/alias", httpSvc.setNodeAliasHandler) diff --git a/http/http_service_test.go b/http/http_service_test.go index 30b1faad..0d452677 100644 --- a/http/http_service_test.go +++ b/http/http_service_test.go @@ -438,3 +438,119 @@ func TestCreateApp_ReadonlyPermission(t *testing.T) { assert.Equal(t, http.StatusForbidden, rec2.Code) } + +func TestGetLogOutput_ReadonlyPermission(t *testing.T) { + e := echo.New() + logger.Init(strconv.Itoa(int(logrus.DebugLevel))) + mockSvc := mocks.NewMockService(t) + gormDb, err := db.NewDB(t) + require.NoError(t, err) + defer db.CloseDB(gormDb) + + mockEventPublisher := events.NewEventPublisher() + + mockConfig := mocks.NewMockConfig(t) + mockConfig.On("GetEnv").Return(&config.AppConfig{}) + mockConfig.On("CheckUnlockPassword", "123").Return(true) + mockConfig.On("GetJWTSecret").Return("dummy secret", nil) + + mockSvc.On("GetDB").Return(gormDb) + mockSvc.On("GetConfig").Return(mockConfig) + mockSvc.On("GetKeys").Return(mocks.NewMockKeys(t)) + mockSvc.On("GetAlbySvc").Return(mocks.NewMockAlbyService(t)) + mockSvc.On("GetAlbyOAuthSvc").Return(mocks.NewMockAlbyOAuthService(t)) + lnClient := mocks.NewMockLNClient(t) + lnClient.On("GetNodeStatus", mock.Anything).Return(&lnclient.NodeStatus{}, nil) + mockSvc.On("GetLNClient").Return(lnClient) + + httpSvc := NewHttpService(mockSvc, mockEventPublisher) + httpSvc.RegisterSharedRoutes(e) + + requestBody := api.UnlockRequest{UnlockPassword: "123", Permission: "readonly"} + jsonBody, _ := json.Marshal(requestBody) + req := httptest.NewRequest(http.MethodPost, "/api/unlock", bytes.NewBuffer(jsonBody)) + req.Header.Set("Content-Type", "application/json") // Set Content-Type header + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + + body, err := io.ReadAll(rec.Body) + require.NoError(t, err) + + type authTokenResponse struct { + Token string `json:"token"` + } + + var unlockAuthTokenResponse authTokenResponse + err = json.Unmarshal(body, &unlockAuthTokenResponse) + require.NoError(t, err) + assert.NotEmpty(t, unlockAuthTokenResponse.Token) + + req2 := httptest.NewRequest(http.MethodGet, "/api/log/app", nil) + req2.Header.Set("Authorization", "Bearer "+unlockAuthTokenResponse.Token) + rec2 := httptest.NewRecorder() + e.ServeHTTP(rec2, req2) + + assert.Equal(t, http.StatusForbidden, rec2.Code) +} + +func TestGetLogOutput_FullPermission(t *testing.T) { + e := echo.New() + logger.Init(strconv.Itoa(int(logrus.DebugLevel))) + mockSvc := mocks.NewMockService(t) + gormDb, err := db.NewDB(t) + require.NoError(t, err) + defer db.CloseDB(gormDb) + + mockEventPublisher := events.NewEventPublisher() + + mockConfig := mocks.NewMockConfig(t) + mockConfig.On("GetEnv").Return(&config.AppConfig{}) + mockConfig.On("CheckUnlockPassword", "123").Return(true) + mockConfig.On("GetJWTSecret").Return("dummy secret", nil) + + mockSvc.On("GetDB").Return(gormDb) + mockSvc.On("GetConfig").Return(mockConfig) + mockSvc.On("GetKeys").Return(mocks.NewMockKeys(t)) + mockSvc.On("GetAlbySvc").Return(mocks.NewMockAlbyService(t)) + mockSvc.On("GetAlbyOAuthSvc").Return(mocks.NewMockAlbyOAuthService(t)) + lnClient := mocks.NewMockLNClient(t) + lnClient.On("GetNodeStatus", mock.Anything).Return(&lnclient.NodeStatus{}, nil) + mockSvc.On("GetLNClient").Return(lnClient) + + httpSvc := NewHttpService(mockSvc, mockEventPublisher) + httpSvc.RegisterSharedRoutes(e) + + requestBody := api.UnlockRequest{UnlockPassword: "123", Permission: "full"} + jsonBody, _ := json.Marshal(requestBody) + req := httptest.NewRequest(http.MethodPost, "/api/unlock", bytes.NewBuffer(jsonBody)) + req.Header.Set("Content-Type", "application/json") // Set Content-Type header + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + + body, err := io.ReadAll(rec.Body) + require.NoError(t, err) + + type authTokenResponse struct { + Token string `json:"token"` + } + + var unlockAuthTokenResponse authTokenResponse + err = json.Unmarshal(body, &unlockAuthTokenResponse) + require.NoError(t, err) + assert.NotEmpty(t, unlockAuthTokenResponse.Token) + + req2 := httptest.NewRequest(http.MethodGet, "/api/log/app", nil) + req2.Header.Set("Authorization", "Bearer "+unlockAuthTokenResponse.Token) + rec2 := httptest.NewRecorder() + e.ServeHTTP(rec2, req2) + + assert.Equal(t, http.StatusOK, rec2.Code) + + var logResponse api.GetLogOutputResponse + err = json.Unmarshal(rec2.Body.Bytes(), &logResponse) + require.NoError(t, err) +} From f4010e239acde5ac91678e3b018f604e8299c5b8 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Wed, 12 Aug 2026 12:33:40 +0700 Subject: [PATCH 127/136] 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 --- swaps/swaps_service.go | 70 ++++++++++++++++++++--- swaps/swaps_service_test.go | 109 ++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+), 8 deletions(-) create mode 100644 swaps/swaps_service_test.go diff --git a/swaps/swaps_service.go b/swaps/swaps_service.go index 7a8c2f31..8bb815a0 100644 --- a/swaps/swaps_service.go +++ b/swaps/swaps_service.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "math" "net/http" "strconv" "sync" @@ -68,7 +69,7 @@ type SwapsService interface { } const ( - AlbySwapServiceFee = 1.0 + AlbySwapServiceFeePercentage = 1.0 ) type SwapInfo struct { @@ -275,7 +276,7 @@ func (svc *swapsService) SwapOut(amountSat uint64, destination string, autoSwap, }).Info("Calculated fees for swap out") albyFee := &boltz.ExtraFees{ - Percentage: AlbySwapServiceFee, + Percentage: AlbySwapServiceFeePercentage, Id: "albyServiceFee", } @@ -334,14 +335,15 @@ func (svc *swapsService) SwapOut(amountSat uint64, destination string, autoSwap, return err } - paymentRequest, err := decodepay.Decodepay(swap.Invoice) + maxSendAmountSat := calculateMaxSwapOutSendAmountSat(amountSat, fees.Percentage, fees.MinerFees.Lockup, fees.MinerFees.Claim) + sendAmountSat, err := verifySwapOutInvoice(swap.Invoice, paymentHash, maxSendAmountSat) if err != nil { - return fmt.Errorf("failed to decode bolt11 invoice") + return fmt.Errorf("invalid swap invoice: %w", err) } err = tx.Model(&dbSwap).Updates(&db.Swap{ SwapId: swap.Id, - SendAmountSat: uint64(paymentRequest.MSatoshi / 1000), + SendAmountSat: sendAmountSat, Invoice: swap.Invoice, LockupAddress: swap.LockupAddress, TimeoutBlockHeight: swap.TimeoutBlockHeight, @@ -379,6 +381,44 @@ func (svc *swapsService) SwapOut(amountSat uint64, destination string, autoSwap, }, nil } +// swapOutInvoiceToleranceSat covers rounding differences that can occur when +// the swap provider converts the requested on-chain amount into an invoice amount. +const swapOutInvoiceToleranceSat = 10 + +// calculateMaxSwapOutSendAmountSat returns the maximum invoice amount accepted for +// a swap out: the requested on-chain amount plus the quoted miner fees, marked up +// by the quoted percentage fees (which are charged on the invoice amount), plus a +// small rounding tolerance. +func calculateMaxSwapOutSendAmountSat(receiveAmountSat uint64, serviceFeePercentage float64, lockupFeeSat uint64, claimFeeSat uint64) uint64 { + totalFeePercentage := serviceFeePercentage + AlbySwapServiceFeePercentage + if totalFeePercentage >= 100 { + return 0 + } + onchainAmountSat := float64(receiveAmountSat + claimFeeSat + lockupFeeSat) + expectedSendAmountSat := math.Ceil(onchainAmountSat / (1 - totalFeePercentage/100)) + return uint64(expectedSendAmountSat) + swapOutInvoiceToleranceSat +} + +// verifySwapOutInvoice checks that a swap out invoice is bound to the swap's +// payment hash and does not exceed maxSendAmountSat, and returns its amount. +func verifySwapOutInvoice(invoice string, expectedPaymentHash string, maxSendAmountSat uint64) (uint64, error) { + paymentRequest, err := decodepay.Decodepay(invoice) + if err != nil { + return 0, fmt.Errorf("failed to decode bolt11 invoice: %w", err) + } + if paymentRequest.PaymentHash != expectedPaymentHash { + return 0, fmt.Errorf("invoice payment hash %s does not match swap payment hash %s", paymentRequest.PaymentHash, expectedPaymentHash) + } + if paymentRequest.MSatoshi <= 0 { + return 0, errors.New("invoice does not have an amount") + } + sendAmountSat := uint64(paymentRequest.MSatoshi) / 1000 + if sendAmountSat > maxSendAmountSat { + return 0, fmt.Errorf("invoice amount %d sat exceeds maximum expected amount %d sat", sendAmountSat, maxSendAmountSat) + } + return sendAmountSat, nil +} + func (svc *swapsService) SwapIn(amountSat uint64, autoSwap bool) (*SwapResponse, error) { amountMsat := amountSat * 1000 invoice, err := svc.transactionsService.MakeInvoice(svc.ctx, amountMsat, "On-chain to lightning swap", "", 0, nil, svc.lnClient, nil, nil, nil) @@ -409,7 +449,7 @@ func (svc *swapsService) SwapIn(amountSat uint64, autoSwap bool) (*SwapResponse, }).Info("Calculated fees for swap in") albyFee := &boltz.ExtraFees{ - Percentage: AlbySwapServiceFee, + Percentage: AlbySwapServiceFeePercentage, Id: "albyServiceFee", } @@ -522,7 +562,7 @@ func (svc *swapsService) GetSwapOutInfo() (*SwapInfo, error) { limits := pairInfo.Limits return &SwapInfo{ - AlbyServiceFee: AlbySwapServiceFee, + AlbyServiceFee: AlbySwapServiceFeePercentage, BoltzServiceFee: fees.Percentage, BoltzNetworkFeeSat: fees.MinerFees.Lockup + fees.MinerFees.Claim, MinAmountSat: limits.Minimal, @@ -546,7 +586,7 @@ func (svc *swapsService) GetSwapInInfo() (*SwapInfo, error) { limits := pairInfo.Limits return &SwapInfo{ - AlbyServiceFee: AlbySwapServiceFee, + AlbyServiceFee: AlbySwapServiceFeePercentage, BoltzServiceFee: fees.Percentage, BoltzNetworkFeeSat: fees.MinerFees, MinAmountSat: limits.Minimal, @@ -1095,6 +1135,13 @@ func (svc *swapsService) startSwapOutListener(swap *db.Swap) { return } + if err = tree.CheckAddress(swap.LockupAddress, network, nil); err != nil { + logger.Logger.WithError(err).WithFields(logrus.Fields{ + "swapId": swap.SwapId, + }).Error("Failed to check address") + return + } + claimTicker := time.NewTicker(10 * time.Second) defer claimTicker.Stop() @@ -1158,6 +1205,13 @@ func (svc *swapsService) startSwapOutListener(swap *db.Swap) { logger.Logger.WithError(err).WithField("swapId", swap.SwapId).Warn("Failed to lookup transaction") return } + if _, err := verifySwapOutInvoice(swap.Invoice, swap.PaymentHash, swap.SendAmountSat); err != nil { + logger.Logger.WithError(err).WithFields(logrus.Fields{ + "swapId": swap.SwapId, + }).Error("Refusing to pay swap invoice") + paymentErrorCh <- err + return + } metadata := map[string]interface{}{ "swap_id": swap.SwapId, } diff --git a/swaps/swaps_service_test.go b/swaps/swaps_service_test.go new file mode 100644 index 00000000..a6cb3a81 --- /dev/null +++ b/swaps/swaps_service_test.go @@ -0,0 +1,109 @@ +package swaps + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "testing" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/zpay32" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func makeTestInvoice(t *testing.T, paymentHash [32]byte, amountMsat uint64) string { + t.Helper() + + privKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + invoice, err := zpay32.NewInvoice( + &chaincfg.MainNetParams, + paymentHash, + time.Now(), + zpay32.Amount(lnwire.MilliSatoshi(amountMsat)), + zpay32.Description("test swap invoice"), + ) + require.NoError(t, err) + + encoded, err := invoice.Encode(zpay32.MessageSigner{ + SignCompact: func(msg []byte) ([]byte, error) { + return ecdsa.SignCompact(privKey, chainhash.HashB(msg), true), nil + }, + }) + require.NoError(t, err) + + return encoded +} + +func makeTestPaymentHash(t *testing.T) ([32]byte, string) { + t.Helper() + + preimage := make([]byte, 32) + _, err := rand.Read(preimage) + require.NoError(t, err) + paymentHash := sha256.Sum256(preimage) + return paymentHash, hex.EncodeToString(paymentHash[:]) +} + +func TestVerifySwapOutInvoice(t *testing.T) { + paymentHash, paymentHashHex := makeTestPaymentHash(t) + + t.Run("accepts invoice with matching payment hash and amount", func(t *testing.T) { + invoice := makeTestInvoice(t, paymentHash, 100_000_000) + + sendAmountSat, err := verifySwapOutInvoice(invoice, paymentHashHex, 100_000) + require.NoError(t, err) + assert.Equal(t, uint64(100_000), sendAmountSat) + }) + + t.Run("rejects invoice with different payment hash", func(t *testing.T) { + otherPaymentHash, _ := makeTestPaymentHash(t) + invoice := makeTestInvoice(t, otherPaymentHash, 100_000_000) + + _, err := verifySwapOutInvoice(invoice, paymentHashHex, 100_000) + require.Error(t, err) + assert.Contains(t, err.Error(), "does not match swap payment hash") + }) + + t.Run("rejects invoice exceeding maximum amount", func(t *testing.T) { + invoice := makeTestInvoice(t, paymentHash, 100_001_000) + + _, err := verifySwapOutInvoice(invoice, paymentHashHex, 100_000) + require.Error(t, err) + assert.Contains(t, err.Error(), "exceeds maximum expected amount") + }) + + t.Run("rejects invoice without an amount", func(t *testing.T) { + invoice := makeTestInvoice(t, paymentHash, 0) + + _, err := verifySwapOutInvoice(invoice, paymentHashHex, 100_000) + require.Error(t, err) + assert.Contains(t, err.Error(), "does not have an amount") + }) + + t.Run("rejects unparseable invoice", func(t *testing.T) { + _, err := verifySwapOutInvoice("lnbc1notaninvoice", paymentHashHex, 100_000) + require.Error(t, err) + }) +} + +func TestCalculateMaxSwapOutSendAmountSat(t *testing.T) { + // 100_000 requested + 300 claim fee + 500 lockup fee = 100_800, + // marked up by 0.5% boltz + 1% alby fee on the invoice amount: + // ceil(100_800 / 0.985) = 102_336, plus 10 sat tolerance + assert.Equal(t, uint64(102_346), calculateMaxSwapOutSendAmountSat(100_000, 0.5, 500, 300)) + + // with a 0% boltz fee only the alby fee percentage applies: + // ceil(100_800 / 0.99) = 101_819, plus 10 sat tolerance + assert.Equal(t, uint64(101_829), calculateMaxSwapOutSendAmountSat(100_000, 0, 500, 300)) + + // invalid fee rates of 100% or more are never accepted + assert.Equal(t, uint64(0), calculateMaxSwapOutSendAmountSat(100_000, 100, 500, 300)) +} From 979644cf688282ee3c4014554be7215fb9e8f151 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:38:13 +0700 Subject: [PATCH 128/136] 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 * fix: validate invoice expiry range and guard LSPS2 cache reads Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- lnclient/ldk/ldk.go | 121 ++++++++++++++++++++++++++++++++------- lnclient/ldk/ldk_test.go | 96 +++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 22 deletions(-) diff --git a/lnclient/ldk/ldk.go b/lnclient/ldk/ldk.go index c83a0cd6..02b52cd1 100644 --- a/lnclient/ldk/ldk.go +++ b/lnclient/ldk/ldk.go @@ -61,6 +61,7 @@ type LDKService struct { lsps2InfoFetchedAt time.Time lsps2MinPaymentSizeMsat *uint64 lsps2MaxPaymentSizeMsat *uint64 + lsps2OpeningFeeParamsMenu []ldk_node.Lsps2OpeningFeeParams shuttingDown bool eventHandlingMutex sync.Mutex } @@ -69,6 +70,17 @@ const resetRouterKey = "ResetRouter" const maxInvoiceExpiry = 24 * time.Hour const lsps2InfoCacheTTL = 60 * time.Minute +// cached opening fee params must be at most this old when used to derive the +// maximum LSP fee for a new JIT channel invoice +const lsps2FeeCapCacheTTL = 1 * time.Minute + +// absolute ceiling on the LSPS2 opening fee accepted for a JIT channel, +// regardless of the fee menu the LSP advertises: the greater of a base amount +// and a percentage of the payment, so small payments can absorb the fixed +// cost of a channel open while larger payments cannot be overcharged. +const lsps2MaxOpeningFeeBaseMsat = 5_000_000 +const lsps2MaxOpeningFeePercent = 10 + func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events.EventPublisher, mnemonic, workDir string, vssToken string, setStartupState func(startupState string), channelPeerSuggestions []alby.ChannelPeerSuggestion) (result lnclient.LNClient, err error) { if mnemonic == "" || workDir == "" { return nil, errors.New("one or more required LDK configuration are missing") @@ -786,10 +798,10 @@ func (ls *LDKService) getMaxSpendable() uint64 { return spendable } -func (ls *LDKService) MakeInvoice(ctx context.Context, amountMsat int64, description string, descriptionHash string, expiry int64, throughNodePubkey *string) (transaction *lnclient.Transaction, err error) { +func (ls *LDKService) MakeInvoice(ctx context.Context, amountMsat int64, description string, descriptionHash string, expirySeconds int64, throughNodePubkey *string) (transaction *lnclient.Transaction, err error) { - if time.Duration(expiry)*time.Second > maxInvoiceExpiry { - return nil, errors.New("expiry is too long") + if expirySeconds < 0 || expirySeconds > int64(maxInvoiceExpiry/time.Second) { + return nil, errors.New("invalid invoice expiry") } maxReceivable := ls.getMaxReceivable() @@ -814,8 +826,8 @@ func (ls *LDKService) MakeInvoice(ctx context.Context, amountMsat int64, descrip }) } - if expiry == 0 { - expiry = lnclient.DEFAULT_INVOICE_EXPIRY + if expirySeconds == 0 { + expirySeconds = lnclient.DEFAULT_INVOICE_EXPIRY } var descriptionType ldk_node.Bolt11InvoiceDescription @@ -830,17 +842,19 @@ func (ls *LDKService) MakeInvoice(ctx context.Context, amountMsat int64, descrip var invoiceObj *ldk_node.Bolt11Invoice if isJitInvoice { + // cap the opening fee the LSP may deduct from the incoming payment + maxLspFeeLimitMsat := ls.getLsps2MaxTotalOpeningFeeMsat(uint64(amountMsat)) invoiceObj, err = ls.node.Bolt11Payment().ReceiveViaJitChannel( uint64(amountMsat), descriptionType, - uint32(expiry), - nil, + uint32(expirySeconds), + &maxLspFeeLimitMsat, ) } else { invoiceObj, err = ls.node.Bolt11Payment().Receive( uint64(amountMsat), descriptionType, - uint32(expiry), + uint32(expirySeconds), ) } @@ -2440,9 +2454,9 @@ func (ls *LDKService) ExecuteCustomNodeCommand(ctx context.Context, command *lnc return nil, lnclient.ErrUnknownCustomNodeCommand } -func (ls *LDKService) MakeHoldInvoice(ctx context.Context, amountMsat int64, description string, descriptionHash string, expiry int64, paymentHash string, minCltvExpiryDelta *uint64) (*lnclient.Transaction, error) { - if time.Duration(expiry)*time.Second > maxInvoiceExpiry { - return nil, errors.New("expiry is too long") +func (ls *LDKService) MakeHoldInvoice(ctx context.Context, amountMsat int64, description string, descriptionHash string, expirySeconds int64, paymentHash string, minCltvExpiryDelta *uint64) (*lnclient.Transaction, error) { + if expirySeconds < 0 || expirySeconds > int64(maxInvoiceExpiry/time.Second) { + return nil, errors.New("invalid invoice expiry") } maxReceivable := ls.getMaxReceivable() @@ -2456,8 +2470,8 @@ func (ls *LDKService) MakeHoldInvoice(ctx context.Context, amountMsat int64, des }) } - if expiry == 0 { - expiry = lnclient.DEFAULT_INVOICE_EXPIRY + if expirySeconds == 0 { + expirySeconds = lnclient.DEFAULT_INVOICE_EXPIRY } var descriptionType ldk_node.Bolt11InvoiceDescription @@ -2491,7 +2505,7 @@ func (ls *LDKService) MakeHoldInvoice(ctx context.Context, amountMsat int64, des invoiceObj, err = ls.node.Bolt11Payment().ReceiveForHashWithMinCltvExpiryDelta( uint64(amountMsat), descriptionType, - uint32(expiry), + uint32(expirySeconds), ldkPaymentHash, uint16(*minCltvExpiryDelta), ) @@ -2499,7 +2513,7 @@ func (ls *LDKService) MakeHoldInvoice(ctx context.Context, amountMsat int64, des invoiceObj, err = ls.node.Bolt11Payment().ReceiveForHash( uint64(amountMsat), descriptionType, - uint32(expiry), + uint32(expirySeconds), ldkPaymentHash, ) } @@ -2662,16 +2676,36 @@ func (ls *LDKService) GetLiquiditySourceLsps2() string { } func (ls *LDKService) GetLiquiditySourceLsps2MinPaymentSizeMsat() *uint64 { - ls.fetchLsps2OpeningFeeParams() + ls.fetchLsps2OpeningFeeParams(lsps2InfoCacheTTL) + + ls.lsps2InfoMu.Lock() + defer ls.lsps2InfoMu.Unlock() + return ls.lsps2MinPaymentSizeMsat } func (ls *LDKService) GetLiquiditySourceLsps2MaxPaymentSizeMsat() *uint64 { - ls.fetchLsps2OpeningFeeParams() + ls.fetchLsps2OpeningFeeParams(lsps2InfoCacheTTL) + + ls.lsps2InfoMu.Lock() + defer ls.lsps2InfoMu.Unlock() + return ls.lsps2MaxPaymentSizeMsat } -func (ls *LDKService) fetchLsps2OpeningFeeParams() { +// getLsps2MaxTotalOpeningFeeMsat returns the maximum opening fee to accept +// for a JIT channel invoice of the given payment size, derived from the +// LSP's advertised opening fee menu and an absolute ceiling. +func (ls *LDKService) getLsps2MaxTotalOpeningFeeMsat(paymentSizeMsat uint64) uint64 { + ls.fetchLsps2OpeningFeeParams(lsps2FeeCapCacheTTL) + + ls.lsps2InfoMu.Lock() + defer ls.lsps2InfoMu.Unlock() + + return computeLsps2MaxTotalOpeningFeeMsat(paymentSizeMsat, ls.lsps2OpeningFeeParamsMenu) +} + +func (ls *LDKService) fetchLsps2OpeningFeeParams(maxCacheAge time.Duration) { if ls.lsps2Pubkey == "" || ls.lsps2Address == "" { return } @@ -2679,7 +2713,7 @@ func (ls *LDKService) fetchLsps2OpeningFeeParams() { ls.lsps2InfoMu.Lock() defer ls.lsps2InfoMu.Unlock() - if !ls.lsps2InfoFetchedAt.IsZero() && time.Since(ls.lsps2InfoFetchedAt) < lsps2InfoCacheTTL { + if !ls.lsps2InfoFetchedAt.IsZero() && time.Since(ls.lsps2InfoFetchedAt) < maxCacheAge { return } @@ -2708,11 +2742,46 @@ func (ls *LDKService) fetchLsps2OpeningFeeParams() { ls.lsps2MinPaymentSizeMsat = minPaymentSizeMsat ls.lsps2MaxPaymentSizeMsat = maxPaymentSizeMsat + ls.lsps2OpeningFeeParamsMenu = response.OpeningFeeParamsMenu ls.lsps2InfoFetchedAt = time.Now() } +// computeLsps2MaxTotalOpeningFeeMsat returns the maximum LSPS2 opening fee to +// accept for a payment of the given size: the highest fee the advertised fee +// menu allows for that size, further limited by the absolute fee ceiling. The +// ceiling alone is used when no menu entry covers the payment size. +func computeLsps2MaxTotalOpeningFeeMsat(paymentSizeMsat uint64, menu []ldk_node.Lsps2OpeningFeeParams) uint64 { + maxAcceptableFeeMsat := lsps2MaxAcceptableOpeningFeeMsat(paymentSizeMsat) + + var menuMaxFeeMsat *uint64 + for _, params := range menu { + if paymentSizeMsat < params.MinPaymentSizeMsat || paymentSizeMsat > params.MaxPaymentSizeMsat { + continue + } + feeMsat := ldk_node.Lsps2ComputeOpeningFeeMsat(paymentSizeMsat, params) + if feeMsat == nil { + continue + } + if menuMaxFeeMsat == nil || *feeMsat > *menuMaxFeeMsat { + menuMaxFeeMsat = feeMsat + } + } + + if menuMaxFeeMsat != nil && *menuMaxFeeMsat < maxAcceptableFeeMsat { + return *menuMaxFeeMsat + } + return maxAcceptableFeeMsat +} + +// the absolute ceiling on the LSPS2 opening fee for a payment of the given +// size, independent of the fees the LSP advertises +func lsps2MaxAcceptableOpeningFeeMsat(paymentSizeMsat uint64) uint64 { + return max(lsps2MaxOpeningFeeBaseMsat, paymentSizeMsat/100*lsps2MaxOpeningFeePercent) +} + // finds the smallest incoming payment for which the user is left -// with a usable amount after the LSP skims its LSPS2 opening fee. +// with a usable amount after the LSP skims its LSPS2 opening fee and the fee +// stays within the absolute fee ceiling applied when creating JIT invoices. func computeLsps2MinPaymentSizeMsat(params ldk_node.Lsps2OpeningFeeParams) (uint64, bool) { // The smallest amount the user must net after the opening fee. We require a // whole satoshi rather than a single millisat so the minimum payment size @@ -2728,12 +2797,20 @@ func computeLsps2MinPaymentSizeMsat(params ldk_node.Lsps2OpeningFeeParams) (uint } // The incoming amount must exceed the opening fee by at least 1 sat, // otherwise the user receives a sub-satoshi (effectively zero) amount - // after the LSP skims its fee. - if *openingFeeMsat+minNetReceiveMsat <= paymentSizeMsat { + // after the LSP skims its fee. The fee must also stay within the + // absolute fee ceiling, otherwise invoices of this size are rejected. + if *openingFeeMsat+minNetReceiveMsat <= paymentSizeMsat && + *openingFeeMsat <= lsps2MaxAcceptableOpeningFeeMsat(paymentSizeMsat) { return paymentSizeMsat, paymentSizeMsat <= params.MaxPaymentSizeMsat } nextPaymentSizeMsat := *openingFeeMsat + minNetReceiveMsat + if *openingFeeMsat > lsps2MaxOpeningFeeBaseMsat { + // the smallest payment size at which a fee this large stays within + // the percentage part of the ceiling + minSizeForFeeMsat := (*openingFeeMsat + lsps2MaxOpeningFeePercent - 1) / lsps2MaxOpeningFeePercent * 100 + nextPaymentSizeMsat = max(nextPaymentSizeMsat, minSizeForFeeMsat) + } if nextPaymentSizeMsat <= paymentSizeMsat || nextPaymentSizeMsat > params.MaxPaymentSizeMsat { return 0, false } diff --git a/lnclient/ldk/ldk_test.go b/lnclient/ldk/ldk_test.go index 9f5788e9..c068b8b6 100644 --- a/lnclient/ldk/ldk_test.go +++ b/lnclient/ldk/ldk_test.go @@ -3,6 +3,7 @@ package ldk import ( "testing" + "github.com/getAlby/ldk-node-go/ldk_node" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -36,6 +37,101 @@ func TestGetVssNodeIdentifier2(t *testing.T) { assert.Equal(t, expectedVssNodeIdentifier, vssNodeIdentifier) } +func makeLsps2OpeningFeeParams(minFeeMsat uint64, proportional uint32, minPaymentSizeMsat uint64, maxPaymentSizeMsat uint64) ldk_node.Lsps2OpeningFeeParams { + return ldk_node.Lsps2OpeningFeeParams{ + MinFeeMsat: minFeeMsat, + Proportional: proportional, + ValidUntil: "2035-01-01T00:00:00Z", + MinLifetime: 4032, + MaxClientToSelfDelay: 2016, + MinPaymentSizeMsat: minPaymentSizeMsat, + MaxPaymentSizeMsat: maxPaymentSizeMsat, + Promise: "promise", + } +} + +func TestComputeLsps2MaxTotalOpeningFeeMsat(t *testing.T) { + t.Run("proportional fee above minimum fee", func(t *testing.T) { + menu := []ldk_node.Lsps2OpeningFeeParams{ + // 0.5% of 10M msat = 50k msat > 10k msat minimum + makeLsps2OpeningFeeParams(10_000, 5_000, 1_000_000, 100_000_000), + } + assert.Equal(t, uint64(50_000), computeLsps2MaxTotalOpeningFeeMsat(10_000_000, menu)) + }) + + t.Run("minimum fee above proportional fee", func(t *testing.T) { + menu := []ldk_node.Lsps2OpeningFeeParams{ + // 0.5% of 1M msat = 5k msat < 10k msat minimum + makeLsps2OpeningFeeParams(10_000, 5_000, 1_000_000, 100_000_000), + } + assert.Equal(t, uint64(10_000), computeLsps2MaxTotalOpeningFeeMsat(1_000_000, menu)) + }) + + t.Run("highest fee across menu entries", func(t *testing.T) { + menu := []ldk_node.Lsps2OpeningFeeParams{ + makeLsps2OpeningFeeParams(10_000, 5_000, 1_000_000, 100_000_000), + makeLsps2OpeningFeeParams(10_000, 20_000, 1_000_000, 100_000_000), + } + assert.Equal(t, uint64(200_000), computeLsps2MaxTotalOpeningFeeMsat(10_000_000, menu)) + }) + + t.Run("entries not covering the payment size are skipped", func(t *testing.T) { + menu := []ldk_node.Lsps2OpeningFeeParams{ + makeLsps2OpeningFeeParams(10_000, 5_000, 1_000_000, 100_000_000), + // covers larger payments only, would otherwise win with 2% + makeLsps2OpeningFeeParams(10_000, 20_000, 20_000_000, 100_000_000), + } + assert.Equal(t, uint64(50_000), computeLsps2MaxTotalOpeningFeeMsat(10_000_000, menu)) + }) + + t.Run("menu fee above ceiling is clamped to ceiling", func(t *testing.T) { + menu := []ldk_node.Lsps2OpeningFeeParams{ + // 20% of 100M msat = 20M msat, above the 10% / 10M msat ceiling + makeLsps2OpeningFeeParams(5_000_000, 200_000, 1_000_000, 1_000_000_000), + } + assert.Equal(t, uint64(10_000_000), computeLsps2MaxTotalOpeningFeeMsat(100_000_000, menu)) + }) + + t.Run("base ceiling applies when no entry covers the payment size", func(t *testing.T) { + menu := []ldk_node.Lsps2OpeningFeeParams{ + makeLsps2OpeningFeeParams(10_000, 5_000, 1_000_000, 100_000_000), + } + // 10% of 200M msat = 20M msat + assert.Equal(t, uint64(20_000_000), computeLsps2MaxTotalOpeningFeeMsat(200_000_000, menu)) + }) + + t.Run("base ceiling applies on empty menu", func(t *testing.T) { + assert.Equal(t, uint64(5_000_000), computeLsps2MaxTotalOpeningFeeMsat(10_000_000, nil)) + }) +} + +func TestComputeLsps2MinPaymentSizeMsat(t *testing.T) { + t.Run("minimum fee below ceiling base", func(t *testing.T) { + // 1000 sat minimum fee: smallest usable payment nets 1 sat above the fee + params := makeLsps2OpeningFeeParams(1_000_000, 10_000, 1_000, 100_000_000_000) + minPaymentSizeMsat, ok := computeLsps2MinPaymentSizeMsat(params) + require.True(t, ok) + assert.Equal(t, uint64(1_001_000), minPaymentSizeMsat) + }) + + t.Run("minimum fee above ceiling base", func(t *testing.T) { + // 8000 sat minimum fee exceeds the 5000 sat ceiling base, so the + // smallest payment is where the fee equals 10% of the payment + params := makeLsps2OpeningFeeParams(8_000_000, 10_000, 1_000, 100_000_000_000) + minPaymentSizeMsat, ok := computeLsps2MinPaymentSizeMsat(params) + require.True(t, ok) + assert.Equal(t, uint64(80_000_000), minPaymentSizeMsat) + }) + + t.Run("proportional fee above ceiling percentage never fits", func(t *testing.T) { + // 30% proportional fee with a minimum fee above the ceiling base can + // never satisfy the 10% ceiling + params := makeLsps2OpeningFeeParams(6_000_000, 300_000, 1_000, 1_000_000_000) + _, ok := computeLsps2MinPaymentSizeMsat(params) + assert.False(t, ok) + }) +} + func TestSanitizeChainEndpointForBitcoind(t *testing.T) { tests := []struct { name string From 3b3c37dd0ce87184b2f67b90864cf9288f8727b1 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:48:06 +0700 Subject: [PATCH 129/136] fix: remove legacy acceptance of empty unlock password check (#2534) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * chore: add operation context to unlock password check errors Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- api/backup_test.go | 8 +++-- config/config.go | 13 +++++++- config/config_test.go | 2 -- config/models.go | 1 + config/tests/config_test.go | 61 +++++++++++++++++++++++++++++-------- service/keys/keys.go | 8 +++-- service/keys/keys_test.go | 32 +++++++++++++++++++ service/start.go | 9 ++++++ tests/mocks/Config.go | 53 ++++++++++++++++++++++++++++++++ 9 files changed, 167 insertions(+), 20 deletions(-) diff --git a/api/backup_test.go b/api/backup_test.go index 784367f9..27bc112a 100644 --- a/api/backup_test.go +++ b/api/backup_test.go @@ -39,6 +39,12 @@ func TestCreateBackup(t *testing.T) { 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", @@ -64,8 +70,6 @@ func TestCreateBackup(t *testing.T) { albyOAuthSvc: albyOAuthSvc, } - unlockPassword := "" - var buf bytes.Buffer err = theAPI.CreateBackup(unlockPassword, &buf) require.NoError(t, err) diff --git a/config/config.go b/config/config.go index 57a17685..c0434665 100644 --- a/config/config.go +++ b/config/config.go @@ -408,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 { diff --git a/config/config_test.go b/config/config_test.go index 302c120c..1849e45a 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -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) diff --git a/config/models.go b/config/models.go index 0fb88709..48860104 100644 --- a/config/models.go +++ b/config/models.go @@ -94,6 +94,7 @@ type Config interface { 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 diff --git a/config/tests/config_test.go b/config/tests/config_test.go index ae1bfd0d..c256f542 100644 --- a/config/tests/config_test.go +++ b/config/tests/config_test.go @@ -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) @@ -219,7 +257,7 @@ func TestJWTSecret_GeneratedOnLoad(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.LoadJWTSecret("123") @@ -251,9 +289,6 @@ func TestJWTSecret_WrongPassword(t *testing.T) { cfg, err := config.NewConfig(&config.AppConfig{}, svc.DB) require.NoError(t, err) - err = cfg.ChangeUnlockPassword("", "123") - require.NoError(t, err) - err = cfg.SaveUnlockPasswordCheck("123") require.NoError(t, err) @@ -272,7 +307,7 @@ 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.LoadJWTSecret("123") @@ -282,7 +317,7 @@ func TestJWTSecret_ChangePassword(t *testing.T) { 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() @@ -306,7 +341,7 @@ func TestJWTSecret_ReplaceUnencryptedSecretOnLoad(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 diff --git a/service/keys/keys.go b/service/keys/keys.go index 039efc1d..1a437d2d 100644 --- a/service/keys/keys.go +++ b/service/keys/keys.go @@ -46,11 +46,15 @@ func NewKeys() *keys { } func (keys *keys) Init(cfg config.Config, encryptionKey string) error { - nostrSecretKey, _ := cfg.Get("NostrSecretKey", encryptionKey) + nostrSecretKey, err := cfg.Get("NostrSecretKey", encryptionKey) + if err != nil { + logger.Logger.WithError(err).Error("Failed to decrypt nostr secret key") + return err + } if nostrSecretKey == "" { nostrSecretKey = nostr.GeneratePrivateKey() - err := cfg.SetUpdate("NostrSecretKey", nostrSecretKey, encryptionKey) + err = cfg.SetUpdate("NostrSecretKey", nostrSecretKey, encryptionKey) if err != nil { logger.Logger.WithError(err).Error("Failed to save generated nostr secret key") return err diff --git a/service/keys/keys_test.go b/service/keys/keys_test.go index 9e8a5131..252fb650 100644 --- a/service/keys/keys_test.go +++ b/service/keys/keys_test.go @@ -110,6 +110,38 @@ func TestGenerateNewMnemonic(t *testing.T) { assert.Equal(t, encryptedChannelsBackupKey.String(), derivedKeyFromKeys.String()) } +func TestInit_WrongPasswordDoesNotOverwriteNostrKey(t *testing.T) { + logger.Init(strconv.Itoa(int(logrus.DebugLevel))) + gormDb, err := db.NewDB(t) + require.NoError(t, err) + defer db.CloseDB(gormDb) + + unlockPassword := "correct" + + cfg, err := config.NewConfig(&config.AppConfig{}, gormDb) + require.NoError(t, err) + + // initialise keys under the correct password, storing an encrypted NostrSecretKey + keys := NewKeys() + err = keys.Init(cfg, unlockPassword) + require.NoError(t, err) + + originalSecret, err := cfg.Get("NostrSecretKey", unlockPassword) + require.NoError(t, err) + require.NotEmpty(t, originalSecret) + + // a wrong password must abort instead of mistaking the failed decrypt for + // "no key yet" and overwriting the stored key with a freshly generated one + keys2 := NewKeys() + err = keys2.Init(cfg, "wrong") + require.Error(t, err) + + // the stored key, decrypted with the correct password, must be unchanged + secretAfter, err := cfg.Get("NostrSecretKey", unlockPassword) + require.NoError(t, err) + assert.Equal(t, originalSecret, secretAfter) +} + func TestGenerateSwapMnemonic(t *testing.T) { logger.Init(strconv.Itoa(int(logrus.DebugLevel))) gormDb, err := db.NewDB(t) diff --git a/service/start.go b/service/start.go index 7ea158a9..8c9786a7 100644 --- a/service/start.go +++ b/service/start.go @@ -273,6 +273,15 @@ func (svc *service) StartApp(encryptionKey string) error { if svc.lnClient != nil { return errors.New("app already started") } + unlockPasswordCheckSet, err := svc.cfg.IsUnlockPasswordCheckSet() + if err != nil { + logger.Logger.WithError(err).Error("Failed to check unlock password check") + return fmt.Errorf("check unlock password check: %w", err) + } + if !unlockPasswordCheckSet { + logger.Logger.Error("Unlock password check is missing from the database") + return errors.New("your wallet data is incomplete and cannot be unlocked. Please restore from a backup") + } if !svc.cfg.CheckUnlockPassword(encryptionKey) { logger.Logger.Errorf("Invalid password") return errors.New("invalid password") diff --git a/tests/mocks/Config.go b/tests/mocks/Config.go index 47adb71c..d10f83c2 100644 --- a/tests/mocks/Config.go +++ b/tests/mocks/Config.go @@ -531,6 +531,59 @@ func (_c *MockConfig_GetRelayUrls_Call) RunAndReturn(run func() []string) *MockC return _c } +// IsUnlockPasswordCheckSet provides a mock function for the type MockConfig +func (_mock *MockConfig) IsUnlockPasswordCheckSet() (bool, error) { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for IsUnlockPasswordCheckSet") + } + + var r0 bool + var r1 error + if returnFunc, ok := ret.Get(0).(func() (bool, error)); ok { + return returnFunc() + } + if returnFunc, ok := ret.Get(0).(func() bool); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(bool) + } + if returnFunc, ok := ret.Get(1).(func() error); ok { + r1 = returnFunc() + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockConfig_IsUnlockPasswordCheckSet_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUnlockPasswordCheckSet' +type MockConfig_IsUnlockPasswordCheckSet_Call struct { + *mock.Call +} + +// IsUnlockPasswordCheckSet is a helper method to define mock.On call +func (_e *MockConfig_Expecter) IsUnlockPasswordCheckSet() *MockConfig_IsUnlockPasswordCheckSet_Call { + return &MockConfig_IsUnlockPasswordCheckSet_Call{Call: _e.mock.On("IsUnlockPasswordCheckSet")} +} + +func (_c *MockConfig_IsUnlockPasswordCheckSet_Call) Run(run func()) *MockConfig_IsUnlockPasswordCheckSet_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockConfig_IsUnlockPasswordCheckSet_Call) Return(b bool, err error) *MockConfig_IsUnlockPasswordCheckSet_Call { + _c.Call.Return(b, err) + return _c +} + +func (_c *MockConfig_IsUnlockPasswordCheckSet_Call) RunAndReturn(run func() (bool, error)) *MockConfig_IsUnlockPasswordCheckSet_Call { + _c.Call.Return(run) + return _c +} + // LoadJWTSecret provides a mock function for the type MockConfig func (_mock *MockConfig) LoadJWTSecret(encryptionKey string) error { ret := _mock.Called(encryptionKey) From 363c22f6d399fd8594d614245e669323d343f62e Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:25:56 +0700 Subject: [PATCH 130/136] fix: switch unlock rate limiter from per-IP to global (#2540) 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 --- http/http_service.go | 26 ++++++++---- http/http_service_test.go | 89 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 8 deletions(-) diff --git a/http/http_service.go b/http/http_service.go index 25c9032f..63d0ffcc 100644 --- a/http/http_service.go +++ b/http/http_service.go @@ -97,12 +97,22 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) { e.POST("/api/setup", httpSvc.setupHandler) e.POST("/api/restore", httpSvc.restoreBackupHandler) - // allow one unlock request per second - unlockRateLimiter := middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(1)) + // A single global rate limiter (one bucket for all callers, not per-IP) + // shared by every endpoint that verifies the unlock password, to bound how + // fast the password can be guessed. + unlockRateLimiter := middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ + Store: middleware.NewRateLimiterMemoryStoreWithConfig( + // burst of 2 so unlocking and then immediately acting is not blocked + middleware.RateLimiterMemoryStoreConfig{Rate: 1, Burst: 2}, + ), + IdentifierExtractor: func(c echo.Context) (string, error) { + return "", nil + }, + }) e.POST("/api/start", httpSvc.startHandler, unlockRateLimiter) e.POST("/api/unlock", httpSvc.unlockHandler, unlockRateLimiter) e.POST("/api/backup", httpSvc.createBackupHandler, unlockRateLimiter) - e.GET("/logout", httpSvc.logoutHandler, unlockRateLimiter) + e.GET("/logout", httpSvc.logoutHandler) frontend.RegisterHandlers(e) @@ -157,17 +167,17 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) { fullAccessApiGroup.Use(httpSvc.requireFullAccess) fullAccessApiGroup.POST("/event", httpSvc.eventHandler) - fullAccessApiGroup.PATCH("/unlock-password", httpSvc.changeUnlockPasswordHandler) - fullAccessApiGroup.PATCH("/auto-unlock", httpSvc.autoUnlockHandler) + fullAccessApiGroup.PATCH("/unlock-password", httpSvc.changeUnlockPasswordHandler, unlockRateLimiter) + fullAccessApiGroup.PATCH("/auto-unlock", httpSvc.autoUnlockHandler, unlockRateLimiter) fullAccessApiGroup.PATCH("/settings", httpSvc.updateSettingsHandler) fullAccessApiGroup.PATCH("/apps/:pubkey", httpSvc.appsUpdateHandler) fullAccessApiGroup.PATCH("/transactions/:id/labels", httpSvc.setTransactionUserLabelsHandler) fullAccessApiGroup.DELETE("/apps/:pubkey", httpSvc.appsDeleteHandler) fullAccessApiGroup.POST("/transfers", httpSvc.transfersHandler) - fullAccessApiGroup.POST("/apps", httpSvc.appsCreateHandler) + fullAccessApiGroup.POST("/apps", httpSvc.appsCreateHandler, unlockRateLimiter) fullAccessApiGroup.POST("/lightning-addresses", httpSvc.lightningAddressesCreateHandler) fullAccessApiGroup.DELETE("/lightning-addresses/:appId", httpSvc.lightningAddressesDeleteHandler) - fullAccessApiGroup.POST("/mnemonic", httpSvc.mnemonicHandler) + fullAccessApiGroup.POST("/mnemonic", httpSvc.mnemonicHandler, unlockRateLimiter) fullAccessApiGroup.PATCH("/backup-reminder", httpSvc.backupReminderHandler) fullAccessApiGroup.POST("/channels", httpSvc.openChannelHandler) fullAccessApiGroup.POST("/channels/rebalance", httpSvc.rebalanceChannelHandler) @@ -192,7 +202,7 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) { fullAccessApiGroup.POST("/swaps/refund", httpSvc.refundSwapHandler) fullAccessApiGroup.GET("/swaps/mnemonic", httpSvc.swapMnemonicHandler) fullAccessApiGroup.GET("/log/:type", httpSvc.getLogOutputHandler) - fullAccessApiGroup.POST("/autoswap", httpSvc.enableAutoSwapOutHandler) + fullAccessApiGroup.POST("/autoswap", httpSvc.enableAutoSwapOutHandler, unlockRateLimiter) fullAccessApiGroup.DELETE("/autoswap", httpSvc.disableAutoSwapOutHandler) fullAccessApiGroup.POST("/node/alias", httpSvc.setNodeAliasHandler) diff --git a/http/http_service_test.go b/http/http_service_test.go index 0d452677..f5cce7f9 100644 --- a/http/http_service_test.go +++ b/http/http_service_test.go @@ -92,6 +92,95 @@ func TestUnlock_UnknownPermission(t *testing.T) { mockConfig.AssertNotCalled(t, "GetJWTSecret") } +// TestUnlock_RateLimited verifies that repeated requests to an unlock-password +// endpoint are throttled with HTTP 429 once the limit is exceeded. +func TestUnlock_RateLimited(t *testing.T) { + e := echo.New() + logger.Init(strconv.Itoa(int(logrus.DebugLevel))) + mockSvc := mocks.NewMockService(t) + gormDb, err := db.NewDB(t) + require.NoError(t, err) + defer db.CloseDB(gormDb) + + mockEventPublisher := events.NewEventPublisher() + + mockConfig := mocks.NewMockConfig(t) + mockConfig.On("GetEnv").Return(&config.AppConfig{}) + mockConfig.On("CheckUnlockPassword", "wrong").Return(false) + + mockSvc.On("GetDB").Return(gormDb) + mockSvc.On("GetConfig").Return(mockConfig) + mockSvc.On("GetKeys").Return(mocks.NewMockKeys(t)) + mockSvc.On("GetAlbySvc").Return(mocks.NewMockAlbyService(t)) + mockSvc.On("GetAlbyOAuthSvc").Return(mocks.NewMockAlbyOAuthService(t)) + + httpSvc := NewHttpService(mockSvc, mockEventPublisher) + httpSvc.RegisterSharedRoutes(e) + + jsonBody, _ := json.Marshal(api.UnlockRequest{UnlockPassword: "wrong", Permission: "full"}) + send := func() int { + req := httptest.NewRequest(http.MethodPost, "/api/unlock", bytes.NewBuffer(jsonBody)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + return rec.Code + } + + // the burst of 2 is served (wrong password, so unauthorized) + assert.Equal(t, http.StatusUnauthorized, send()) + assert.Equal(t, http.StatusUnauthorized, send()) + // the next request exceeds the limit and is rejected with 429 + assert.Equal(t, http.StatusTooManyRequests, send()) +} + +// TestUnlock_RateLimitNotBypassedBySpoofedIP verifies that the unlock rate +// limiter is global rather than per-IP: varying the X-Forwarded-For header per +// request does not grant each request a fresh bucket. +func TestUnlock_RateLimitNotBypassedBySpoofedIP(t *testing.T) { + e := echo.New() + logger.Init(strconv.Itoa(int(logrus.DebugLevel))) + mockSvc := mocks.NewMockService(t) + gormDb, err := db.NewDB(t) + require.NoError(t, err) + defer db.CloseDB(gormDb) + + mockEventPublisher := events.NewEventPublisher() + + mockConfig := mocks.NewMockConfig(t) + mockConfig.On("GetEnv").Return(&config.AppConfig{}) + mockConfig.On("CheckUnlockPassword", "wrong").Return(false) + + mockSvc.On("GetDB").Return(gormDb) + mockSvc.On("GetConfig").Return(mockConfig) + mockSvc.On("GetKeys").Return(mocks.NewMockKeys(t)) + mockSvc.On("GetAlbySvc").Return(mocks.NewMockAlbyService(t)) + mockSvc.On("GetAlbyOAuthSvc").Return(mocks.NewMockAlbyOAuthService(t)) + + httpSvc := NewHttpService(mockSvc, mockEventPublisher) + httpSvc.RegisterSharedRoutes(e) + + jsonBody, _ := json.Marshal(api.UnlockRequest{UnlockPassword: "wrong", Permission: "full"}) + + send := func(forwardedFor string) int { + req := httptest.NewRequest(http.MethodPost, "/api/unlock", bytes.NewBuffer(jsonBody)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Forwarded-For", forwardedFor) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + return rec.Code + } + + rateLimited := 0 + for i := 0; i < 12; i++ { + // each request presents a distinct client address + if send("10.0.0."+strconv.Itoa(i)) == http.StatusTooManyRequests { + rateLimited++ + } + } + + assert.Positive(t, rateLimited, "spoofing X-Forwarded-For must not grant a fresh rate-limit bucket") +} + func TestGetApps_NoToken(t *testing.T) { e := echo.New() logger.Init(strconv.Itoa(int(logrus.DebugLevel))) From 5125418188d424a27b437e591c1766c6bb13f767 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:57:27 +0700 Subject: [PATCH 131/136] 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 * 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 --------- Co-authored-by: Claude Fable 5 --- frontend/package.json | 2 +- frontend/yarn.lock | 206 ++++++++++++++++++++++++++++-------------- go.mod | 10 +- go.sum | 20 ++-- 4 files changed, 152 insertions(+), 86 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 5984d891..1b8d75c5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -42,7 +42,7 @@ "react-day-picker": "^9.14.0", "react-dom": "^19.2.6", "react-lottie": "^1.2.4", - "react-router": "^7.14.2", + "react-router": "^7.18.2", "sonner": "^2.0.7", "swr": "^2.4.1", "tailwind-merge": "^3.6.0", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 9dfc2189..70251802 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2,14 +2,6 @@ # yarn lockfile v1 -"@ampproject/remapping@^2.2.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" - integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.24" - "@apideck/better-ajv-errors@^0.3.1": version "0.3.6" resolved "https://registry.yarnpkg.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz#957d4c28e886a64a8141f7522783be65733ff097" @@ -42,21 +34,26 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.0.tgz#9fc6fd58c2a6a15243cd13983224968392070790" integrity sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw== +"@babel/compat-data@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.7.tgz#6f0237f0f36d2e51c0570a636faed9d2d0efe629" + integrity sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg== + "@babel/core@^7.24.4": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.0.tgz#55dad808d5bf3445a108eefc88ea3fdf034749a4" - integrity sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ== + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.7.tgz#80c10b17248082968b57a857b91640971f2070f7" + integrity sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA== dependencies: - "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.27.1" - "@babel/generator" "^7.28.0" - "@babel/helper-compilation-targets" "^7.27.2" - "@babel/helper-module-transforms" "^7.27.3" - "@babel/helpers" "^7.27.6" - "@babel/parser" "^7.28.0" - "@babel/template" "^7.27.2" - "@babel/traverse" "^7.28.0" - "@babel/types" "^7.28.0" + "@babel/code-frame" "^7.29.7" + "@babel/generator" "^7.29.7" + "@babel/helper-compilation-targets" "^7.29.7" + "@babel/helper-module-transforms" "^7.29.7" + "@babel/helpers" "^7.29.7" + "@babel/parser" "^7.29.7" + "@babel/template" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" + "@jridgewell/remapping" "^2.3.5" convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -85,6 +82,17 @@ "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" +"@babel/generator@^7.29.8": + version "7.29.8" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.8.tgz#4b0b887885422643339e09022148a4c4ebaa4979" + integrity sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg== + dependencies: + "@babel/parser" "^7.29.8" + "@babel/types" "^7.29.8" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + "@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3": version "7.27.3" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" @@ -103,6 +111,17 @@ lru-cache "^5.1.1" semver "^6.3.1" +"@babel/helper-compilation-targets@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz#7a1def704302401c47f64fa85589e974ae217042" + integrity sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g== + dependencies: + "@babel/compat-data" "^7.29.7" + "@babel/helper-validator-option" "^7.29.7" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + "@babel/helper-create-class-features-plugin@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz#5bee4262a6ea5ddc852d0806199eb17ca3de9281" @@ -154,7 +173,7 @@ "@babel/traverse" "^7.27.1" "@babel/types" "^7.27.1" -"@babel/helper-module-imports@^7.18.6": +"@babel/helper-module-imports@^7.18.6", "@babel/helper-module-imports@^7.29.7": version "7.29.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz#ef25048a518e828d7393fac5882ddd73921d7396" integrity sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g== @@ -170,7 +189,7 @@ "@babel/traverse" "^7.27.1" "@babel/types" "^7.27.1" -"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.27.3": +"@babel/helper-module-transforms@^7.27.1": version "7.27.3" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz#db0bbcfba5802f9ef7870705a7ef8788508ede02" integrity sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg== @@ -179,6 +198,15 @@ "@babel/helper-validator-identifier" "^7.27.1" "@babel/traverse" "^7.27.3" +"@babel/helper-module-transforms@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz#b062747a5997ba138637201328bbff77960574ae" + integrity sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg== + dependencies: + "@babel/helper-module-imports" "^7.29.7" + "@babel/helper-validator-identifier" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/helper-optimise-call-expression@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz#c65221b61a643f3e62705e5dd2b5f115e35f9200" @@ -191,6 +219,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c" integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== +"@babel/helper-plugin-utils@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz#c0a0766f1a13617d8a17407d7ab8f9d486225ea4" + integrity sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw== + "@babel/helper-remap-async-to-generator@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz#4601d5c7ce2eb2aea58328d43725523fcd362ce6" @@ -247,6 +280,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== +"@babel/helper-validator-option@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz#cf315be940213b354eb4abcc0bd01ebe3f73bc2a" + integrity sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw== + "@babel/helper-wrap-function@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz#b88285009c31427af318d4fe37651cd62a142409" @@ -256,13 +294,13 @@ "@babel/traverse" "^7.27.1" "@babel/types" "^7.27.1" -"@babel/helpers@^7.27.6": - version "7.28.2" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.2.tgz#80f0918fecbfebea9af856c419763230040ee850" - integrity sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw== +"@babel/helpers@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.29.7.tgz#45abfde7548997e34376c3e69feb475cffb4a607" + integrity sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg== dependencies: - "@babel/template" "^7.27.2" - "@babel/types" "^7.28.2" + "@babel/template" "^7.29.7" + "@babel/types" "^7.29.7" "@babel/parser@^7.24.4", "@babel/parser@^7.27.2", "@babel/parser@^7.28.0": version "7.28.5" @@ -278,6 +316,13 @@ dependencies: "@babel/types" "^7.29.7" +"@babel/parser@^7.29.8": + version "7.29.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.8.tgz#9653716a2f10c677b98fbc63d4bfb000c302cf17" + integrity sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA== + dependencies: + "@babel/types" "^7.29.8" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz#61dd8a8e61f7eb568268d1b5f129da3eee364bf9" @@ -541,14 +586,14 @@ "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-modules-systemjs@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz#00e05b61863070d0f3292a00126c16c0e024c4ed" - integrity sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA== + version "7.29.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.8.tgz#e60a6a42ac63a3095f9cc7264f698a100c8fe05d" + integrity sha512-6iSnEK0zlkLKU4heofK/AdmRD4e2SHVpJMtrwnTCzhnaM98ria4rTrOXBBi45BTTYnJtO8txnPsX4fChYXkmeA== dependencies: - "@babel/helper-module-transforms" "^7.27.1" - "@babel/helper-plugin-utils" "^7.27.1" - "@babel/helper-validator-identifier" "^7.27.1" - "@babel/traverse" "^7.27.1" + "@babel/helper-module-transforms" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-validator-identifier" "^7.29.7" + "@babel/traverse" "^7.29.8" "@babel/plugin-transform-modules-umd@^7.27.1": version "7.27.1" @@ -875,7 +920,20 @@ "@babel/types" "^7.29.7" debug "^4.3.1" -"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.0", "@babel/types@^7.28.2", "@babel/types@^7.4.4": +"@babel/traverse@^7.29.8": + version "7.29.8" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.8.tgz#4111014cdc71a0f95d9471907590baa0b8a6b28a" + integrity sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg== + dependencies: + "@babel/code-frame" "^7.29.7" + "@babel/generator" "^7.29.8" + "@babel/helper-globals" "^7.29.7" + "@babel/parser" "^7.29.8" + "@babel/template" "^7.29.7" + "@babel/types" "^7.29.8" + debug "^4.3.1" + +"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.0", "@babel/types@^7.4.4": version "7.28.2" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b" integrity sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ== @@ -899,6 +957,14 @@ "@babel/helper-string-parser" "^7.29.7" "@babel/helper-validator-identifier" "^7.29.7" +"@babel/types@^7.29.8": + version "7.29.8" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.8.tgz#1229eef31d85156d70fa3f4cd859376d0eaf6863" + integrity sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg== + dependencies: + "@babel/helper-string-parser" "^7.29.7" + "@babel/helper-validator-identifier" "^7.29.7" + "@base-ui/react@^1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@base-ui/react/-/react-1.5.0.tgz#7042f8bd9d7f2fe128b0e1fad4dbed1bfbb9ddf0" @@ -2964,24 +3030,24 @@ bitcoin-address-validation@^3.0.0: sha256-uint8array "^0.10.3" brace-expansion@^1.1.7: - version "1.1.12" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" - integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== + version "1.1.18" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.18.tgz#3ce74d89885136be1535341f8c3d4425c29a5cab" + integrity sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" brace-expansion@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" - integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== + version "2.1.4" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.1.4.tgz#589dab11c0018d0366be64cd8bf12c8dbecc8326" + integrity sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg== dependencies: balanced-match "^1.0.0" -brace-expansion@^5.0.5: - version "5.0.5" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.5.tgz#dcc3a37116b79f3e1b46db994ced5d570e930fdb" - integrity sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ== +brace-expansion@^5.0.8: + version "5.0.9" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.9.tgz#7c72438809b5fa5babf54199a1f1c281a6984fcf" + integrity sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg== dependencies: balanced-match "^4.0.2" @@ -3723,9 +3789,9 @@ fast-levenshtein@^2.0.6: integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fast-uri@^3.0.1: - version "3.0.6" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" - integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== + version "3.1.5" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.5.tgz#610f37419a030270430cecd68d74e3d4d96725d0" + integrity sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw== fastq@^1.6.0: version "1.19.1" @@ -3777,9 +3843,9 @@ flat-cache@^4.0.0: keyv "^4.5.4" flatted@^3.2.9: - version "3.3.3" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" - integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== + version "3.4.4" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.4.tgz#aeeca2a506303f0cee61c59e6c9f2a88d2f29fc6" + integrity sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q== for-each@^0.3.3, for-each@^0.3.5: version "0.3.5" @@ -4367,9 +4433,9 @@ jiti@^2.6.1: integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^4.1.0, js-yaml@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" - integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== + version "4.3.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.1.tgz#01216c001d67f48e2cd560d708c7af21090a3848" + integrity sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ== dependencies: argparse "^2.0.1" @@ -4727,11 +4793,11 @@ mini-svg-data-uri@^1.2.3: integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg== minimatch@^10.1.1, minimatch@^10.2.2, minimatch@^10.2.4: - version "10.2.5" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.5.tgz#bd48687a0be38ed2961399105600f832095861d1" - integrity sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg== + version "10.2.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.6.tgz#fd956bbe0b77241e9f15ac5dccb1c638060968ef" + integrity sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A== dependencies: - brace-expansion "^5.0.5" + brace-expansion "^5.0.8" minimatch@^3.1.5: version "3.1.5" @@ -4741,9 +4807,9 @@ minimatch@^3.1.5: brace-expansion "^1.1.7" minimatch@^5.0.1: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + version "5.1.9" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.9.tgz#1293ef15db0098b394540e8f9f744f9fda8dee4b" + integrity sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw== dependencies: brace-expansion "^2.0.1" @@ -4944,9 +5010,9 @@ picocolors@^1.1.1: integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + version "2.3.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601" + integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA== picomatch@^4.0.2, picomatch@^4.0.3, picomatch@^4.0.4, picomatch@^4.0.5: version "4.0.5" @@ -5145,10 +5211,10 @@ react-remove-scroll@^2.6.3: use-callback-ref "^1.3.3" use-sidecar "^1.1.3" -react-router@^7.14.2: - version "7.14.2" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.14.2.tgz#d86e5b01049365b2c982363ebd2baa4928824603" - integrity sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw== +react-router@^7.18.2: + version "7.18.2" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.18.2.tgz#a76c46ce9e5edacd4f51289d5a71f71305db9152" + integrity sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg== dependencies: cookie "^1.0.1" set-cookie-parser "^2.6.0" diff --git a/go.mod b/go.mod index f6d71aee..b491dc60 100644 --- a/go.mod +++ b/go.mod @@ -22,8 +22,8 @@ require ( gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.15.0 golang.org/x/crypto v0.54.0 golang.org/x/oauth2 v0.36.0 - google.golang.org/grpc v1.79.3 - google.golang.org/protobuf v1.36.10 + google.golang.org/grpc v1.82.1 + google.golang.org/protobuf v1.36.11 gopkg.in/macaroon.v2 v2.1.0 gorm.io/driver/postgres v1.6.2 gorm.io/driver/sqlite v1.6.0 @@ -32,7 +32,7 @@ require ( require ( dario.cat/mergo v1.0.2 // indirect - filippo.io/edwards25519 v1.1.0 // indirect + filippo.io/edwards25519 v1.1.1 // indirect git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect @@ -231,8 +231,8 @@ require ( golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.47.0 // indirect google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect gopkg.in/errgo.v1 v1.0.1 // indirect gopkg.in/macaroon-bakery.v2 v2.3.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect diff --git a/go.sum b/go.sum index 0129435a..b036e69e 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= +filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA= git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= @@ -759,8 +759,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -769,18 +769,18 @@ google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= From 1c7c026e541aaa6fb0d631202e955ef8d59c4bae Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:40:43 +0700 Subject: [PATCH 132/136] 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 --- frontend/package.json | 3 +-- frontend/vite.config.ts | 7 +++---- frontend/yarn.lock | 21 +-------------------- 3 files changed, 5 insertions(+), 26 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 1b8d75c5..2ce42762 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -77,8 +77,7 @@ "typescript": "^5.9.3", "typescript-eslint": "^8.61.0", "vite": "^8.2.0", - "vite-plugin-pwa": "^1.3.0", - "vite-tsconfig-paths": "^6.1.1" + "vite-plugin-pwa": "^1.3.0" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 41326cb5..78df01d5 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -3,13 +3,11 @@ import react from "@vitejs/plugin-react-swc"; import path from "path"; import { defineConfig, Plugin } from "vite"; import { VitePWA } from "vite-plugin-pwa"; -import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig(({ command }) => ({ plugins: [ react(), tailwindcss(), - tsconfigPaths(), VitePWA({ registerType: "autoUpdate", // disable service worker - Alby Hub cannot be used offline (and also breaks oauth callback) @@ -66,9 +64,10 @@ export default defineConfig(({ command }) => ({ }, }, resolve: { + tsconfigPaths: true, alias: { - src: path.resolve(__dirname, "./src"), - wailsjs: path.resolve(__dirname, "./wailsjs"), + src: path.resolve(import.meta.dirname, "./src"), + wailsjs: path.resolve(import.meta.dirname, "./wailsjs"), // used to refrence public assets when importing images or other // assets from the public folder // this is necessary to inject the base path during build diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 70251802..24b0426a 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3351,7 +3351,7 @@ dayjs@^1.11.20: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.20.tgz#88d919fd639dc991415da5f4cb6f1b6650811938" integrity sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ== -debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.6, debug@^4.4.1, debug@^4.4.3: +debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.6, debug@^4.4.1, debug@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -4028,11 +4028,6 @@ globalthis@^1.0.4: define-properties "^1.2.1" gopd "^1.0.1" -globrex@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" - integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== - gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" @@ -5921,11 +5916,6 @@ ts-api-utils@^2.5.0: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz#4acd4a155e22734990a5ed1fe9e97f113bcb37c1" integrity sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA== -tsconfck@^3.0.3: - version "3.1.6" - resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.6.tgz#da1f0b10d82237ac23422374b3fce1edb23c3ead" - integrity sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w== - tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" @@ -6121,15 +6111,6 @@ vite-plugin-pwa@^1.3.0: workbox-build "^7.4.1" workbox-window "^7.4.1" -vite-tsconfig-paths@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-6.1.1.tgz#d5c28cba79c89ebf76489ef1040024b21df6da3a" - integrity sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg== - dependencies: - debug "^4.1.1" - globrex "^0.1.2" - tsconfck "^3.0.3" - vite@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/vite/-/vite-8.2.0.tgz#902fcd3dc0312f553c85b6cbc4625dcaca2d8df8" From 1c7abc62e91d75e021ad69b2128e46a1c33ac953 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Wed, 12 Aug 2026 22:04:43 +0700 Subject: [PATCH 133/136] 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 * chore: improve copy --------- Co-authored-by: Claude Fable 5 --- frontend/src/screens/setup/SetupSecurity.tsx | 54 ++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/frontend/src/screens/setup/SetupSecurity.tsx b/frontend/src/screens/setup/SetupSecurity.tsx index 1ae5c5ea..74f2467a 100644 --- a/frontend/src/screens/setup/SetupSecurity.tsx +++ b/frontend/src/screens/setup/SetupSecurity.tsx @@ -1,6 +1,7 @@ import { ClockIcon, HandCoinsIcon, + InfoIcon, LandmarkIcon, ShieldAlertIcon, UnlockIcon, @@ -13,6 +14,12 @@ import TwoColumnLayoutHeader from "src/components/TwoColumnLayoutHeader"; import { Button } from "src/components/ui/button"; import { Checkbox } from "src/components/ui/checkbox"; import { Label } from "src/components/ui/label"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "src/components/ui/tooltip"; import { useInfo } from "src/hooks/useInfo"; import useSetupStore from "src/state/SetupStore"; @@ -70,11 +77,52 @@ export function SetupSecurity() {
    - - Your funds will be refreshed periodically which will incur a - small fee. + + Your funds must be refreshed periodically{" "} + + + + + + + Your virtual UTXOs (VTXOs) will be refreshed + automatically to ensure you maintain ownership. + Refreshes may incur a small fee. Keep your Alby Hub + online so your funds do not expire. + + +
    +
    + + + Unilateral exit is not implemented in Alby Hub yet{" "} + + + + + + + You will need to take your hub wallet data and execute + the exit yourself. + + + + +
    + +

    + By using Bark you agree to{" "} + + Second's Terms of Service + + . +

    +
    )}
    From edd283cdb2c38724657d196cc1ed7fb417bb60ed Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Wed, 12 Aug 2026 22:14:49 +0700 Subject: [PATCH 134/136] 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 * fix: reword migration file warning Co-Authored-By: Claude Fable 5 * 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 * 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 * fix: assert traversal-specific error in restore backup test Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- api/backup.go | 149 ++++++++++++++++++++++----- api/backup_test.go | 103 +++++++++++++++++- frontend/src/screens/MigrateNode.tsx | 10 ++ 3 files changed, 234 insertions(+), 28 deletions(-) diff --git a/api/backup.go b/api/backup.go index 9d2d7622..32e5aa03 100644 --- a/api/backup.go +++ b/api/backup.go @@ -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 @@ -257,8 +295,22 @@ 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 { // Entry names come from the archive and must not be trusted. Reject any // name that is absolute or points outside the restore directory via @@ -268,11 +320,11 @@ func (api *api) RestoreBackup(unlockPassword string, r io.Reader) error { return fmt.Errorf("refusing to extract zip entry outside restore directory: %q", zipFile.Name) } - fsFilePath := filepath.Join(restoreDir, entryName) + fsFilePath := filepath.Join(stagingDir, entryName) - // Confirm the cleaned path is still contained within the restore + // Confirm the cleaned path is still contained within the staging // directory. - if fsFilePath != restoreDir && !strings.HasPrefix(fsFilePath, restoreDir+string(os.PathSeparator)) { + if fsFilePath != stagingDir && !strings.HasPrefix(fsFilePath, stagingDir+string(os.PathSeparator)) { return fmt.Errorf("refusing to extract zip entry outside restore directory: %q", zipFile.Name) } @@ -308,6 +360,13 @@ 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() @@ -328,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) @@ -354,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, } @@ -364,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") } diff --git a/api/backup_test.go b/api/backup_test.go index 27bc112a..9ddfcc5a 100644 --- a/api/backup_test.go +++ b/api/backup_test.go @@ -3,11 +3,14 @@ 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" @@ -148,15 +151,111 @@ func TestRestoreBackupRejectsPathTraversal(t *testing.T) { cw, err := encryptingWriter(&buf, unlockPassword) require.NoError(t, err) zw := zip.NewWriter(cw) - entryWriter, err := zw.Create(escapeEntryName) + // 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.Error(t, err) + 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) } diff --git a/frontend/src/screens/MigrateNode.tsx b/frontend/src/screens/MigrateNode.tsx index 0c97b578..55bbb021 100644 --- a/frontend/src/screens/MigrateNode.tsx +++ b/frontend/src/screens/MigrateNode.tsx @@ -109,6 +109,16 @@ export function MigrateNode() { another device and use the “Advanced” option during the onboarding.

    +
    +
    + +

    Never share your migration file

    +
    +

    + Anyone with this file and your unlock password can access your + funds. Never send it to anyone. Alby support will never ask for it. +

    +
    From d2cebc5c6f8ed6df29c2afc2b61383ffad66f79e Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:52:50 +0700 Subject: [PATCH 135/136] 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 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 --- frontend/package.json | 5 +- frontend/src/components/LottieLoading.tsx | 21 +- frontend/src/components/LottieSuccess.tsx | 21 +- frontend/vite.config.ts | 4 + frontend/yarn.lock | 241 +++++++++------------- 5 files changed, 117 insertions(+), 175 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 2ce42762..3109831a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -35,13 +35,13 @@ "date-fns": "^4.1.0", "dayjs": "^1.11.20", "embla-carousel-react": "^8.6.0", + "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-lottie": "^1.2.4", "react-router": "^7.18.2", "sonner": "^2.0.7", "swr": "^2.4.1", @@ -62,7 +62,6 @@ "@types/node": "^25.9.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.0.0", - "@types/react-lottie": "^1.2.10", "@vitejs/plugin-react-swc": "^4.3.1", "eslint": "^10.4.1", "eslint-config-prettier": "^10.1.8", @@ -76,7 +75,7 @@ "tailwindcss": "^4.3.0", "typescript": "^5.9.3", "typescript-eslint": "^8.61.0", - "vite": "^8.2.0", + "vite": "^8.2.1", "vite-plugin-pwa": "^1.3.0" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" diff --git a/frontend/src/components/LottieLoading.tsx b/frontend/src/components/LottieLoading.tsx index 0d3e6c92..8de046d8 100644 --- a/frontend/src/components/LottieLoading.tsx +++ b/frontend/src/components/LottieLoading.tsx @@ -1,5 +1,4 @@ -import { useMemo } from "react"; -import Lottie from "react-lottie"; +import Lottie from "lottie-react"; import animationDataDark from "src/assets/lotties/loading-dark.json"; import animationDataLight from "src/assets/lotties/loading-light.json"; import { useTheme } from "src/components/ui/theme-provider"; @@ -7,15 +6,13 @@ import { useTheme } from "src/components/ui/theme-provider"; export default function LottieLoading({ size }: { size?: number }) { const { isDarkMode } = useTheme(); - const options = useMemo( - () => ({ - loop: true, - autoplay: true, - animationData: isDarkMode ? animationDataDark : animationDataLight, - rendererSettings: { preserveAspectRatio: "xMidYMid slice" }, - }), - [isDarkMode] + return ( + ); - - return ; } diff --git a/frontend/src/components/LottieSuccess.tsx b/frontend/src/components/LottieSuccess.tsx index cdda9281..a846f938 100644 --- a/frontend/src/components/LottieSuccess.tsx +++ b/frontend/src/components/LottieSuccess.tsx @@ -1,21 +1,16 @@ -import { useMemo } from "react"; -import Lottie from "react-lottie"; +import Lottie from "lottie-react"; import animationData from "src/assets/lotties/success-check.json"; export default function LottieSuccess({ size = 288 }: { size?: number }) { - const options = useMemo( - () => ({ - loop: false, - autoplay: true, - animationData, - rendererSettings: { preserveAspectRatio: "xMidYMid meet" }, - }), - [] - ); - return (
    - +
    ); } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 78df01d5..141145df 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -72,6 +72,10 @@ export default defineConfig(({ command }) => ({ // assets from the public folder // this is necessary to inject the base path during build public: "", + // lottie-react's `browser` field points to a UMD build, which breaks + // under Vite 8's Node-style CJS interop (default import resolves to the + // whole exports object); force the ESM build instead + "lottie-react": "lottie-react/build/index.es.js", }, }, build: { diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 24b0426a..22507596 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1440,10 +1440,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@oxc-project/types@=0.143.0": - version "0.143.0" - resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.143.0.tgz#c3e4f3178b7b54e4dd194eac6d45258a60f0092b" - integrity sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA== +"@oxc-project/types@=0.144.0": + version "0.144.0" + resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.144.0.tgz#7dfbfbfbbb9c24d4abeb6f9856a1eca02aff6468" + integrity sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg== "@radix-ui/number@1.1.1": version "1.1.1" @@ -2124,75 +2124,75 @@ resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.1.tgz#78244efe12930c56fd255d7923865857c41ac8cb" integrity sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw== -"@rolldown/binding-android-arm64@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz#001b8b0b01844701efda1bb6bed84b681c4a488b" - integrity sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw== +"@rolldown/binding-android-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz#9ac390255ded738672ad1425bed6423453ff71fd" + integrity sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA== -"@rolldown/binding-darwin-arm64@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz#5e87c602ed634a6fef092e2162e24fbfb881c4ec" - integrity sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA== +"@rolldown/binding-darwin-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz#c63cad2f656b672782dd70894af895af9cba35c8" + integrity sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ== -"@rolldown/binding-darwin-x64@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz#f32e0b286714bd03a421d693415d05d97d265b77" - integrity sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA== +"@rolldown/binding-darwin-x64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz#54f6305d6785793c63c1521a4da4412bf7d8e089" + integrity sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg== -"@rolldown/binding-freebsd-x64@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz#5f38ad5761b6b7b21b57a99566bb52634c60ab19" - integrity sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg== +"@rolldown/binding-freebsd-x64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz#c069a3ebd4a5dcfffd79a00f33c06bc242d9566b" + integrity sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A== -"@rolldown/binding-linux-arm-gnueabihf@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz#ab4dcd07f1bd88e8d659ae0c3bb9d2f290adb897" - integrity sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw== +"@rolldown/binding-linux-arm-gnueabihf@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz#cff0d05a56e19f02443313f2abf73cf693173867" + integrity sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ== -"@rolldown/binding-linux-arm64-gnu@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz#d279b7016039a725fb66d82784b9841f42df83da" - integrity sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw== +"@rolldown/binding-linux-arm64-gnu@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz#267605366ba2bf3146609417a6db99253943c96f" + integrity sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng== -"@rolldown/binding-linux-arm64-musl@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz#d08bbc93d2742214548c5adf7df7788944e5a89a" - integrity sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q== +"@rolldown/binding-linux-arm64-musl@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz#73d47bfdba2a72c16bfb271c23efb8949b2c2018" + integrity sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w== -"@rolldown/binding-linux-ppc64-gnu@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz#6418e63745b3193f26ab3bb88744b3a4a1356d7c" - integrity sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w== +"@rolldown/binding-linux-ppc64-gnu@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz#3ba455c72d3e9efd973df59689d48e75ca6f7559" + integrity sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ== -"@rolldown/binding-linux-s390x-gnu@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz#77ec30d0704cf4eb1cb4a63f501c9852c6728cf4" - integrity sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg== +"@rolldown/binding-linux-s390x-gnu@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz#2fddcb48eee12a620735c83a0056307b6ede2edb" + integrity sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw== -"@rolldown/binding-linux-x64-gnu@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz#3b9b6e0dd3e86c597f42858748ca25f1dfd58ed8" - integrity sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w== +"@rolldown/binding-linux-x64-gnu@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz#97fb91430f78e46f81d4fe82a049a8c47f4ece96" + integrity sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ== -"@rolldown/binding-linux-x64-musl@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz#f78033c592c8bd2af48284a45f8e4baaa0befbf5" - integrity sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A== +"@rolldown/binding-linux-x64-musl@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz#71ec9967b5ff546a6d5dda16d1c7a97e50bf647a" + integrity sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ== -"@rolldown/binding-openharmony-arm64@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz#36e951f5a6fca922a5205e283d0a82b9f98199ca" - integrity sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug== +"@rolldown/binding-openharmony-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz#1e19ab28fbdb009cb6d9503e3ff6cbeffd7225ae" + integrity sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg== -"@rolldown/binding-win32-arm64-msvc@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz#c1e494ac47e13bd857fca0b3ad59c33580241f7e" - integrity sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw== +"@rolldown/binding-win32-arm64-msvc@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz#9fad167d015c699f4375c2a1d04d1e52e4a32de4" + integrity sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw== -"@rolldown/binding-win32-x64-msvc@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz#b0effffcd6872f8a021373eb437916b1b52283a4" - integrity sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg== +"@rolldown/binding-win32-x64-msvc@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz#7a5f2d3e886c357029332b53f38ad2042e4615c4" + integrity sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ== "@rolldown/pluginutils@^1.0.0": version "1.0.1" @@ -2701,14 +2701,7 @@ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.3.tgz#c1e305d15a52a3e508d54dca770d202cb63abf2c" integrity sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ== -"@types/react-lottie@^1.2.10": - version "1.2.10" - resolved "https://registry.yarnpkg.com/@types/react-lottie/-/react-lottie-1.2.10.tgz#220f68a2dfa0d4b131ab4930e8bf166b9442c68c" - integrity sha512-rCd1p3US4ELKJlqwVnP0h5b24zt5p9OCvKUoNpYExLqwbFZMWEiJ6EGLMmH7nmq5V7KomBIbWO2X/XRFsL0vCA== - dependencies: - "@types/react" "*" - -"@types/react@*", "@types/react@^19.2.14": +"@types/react@^19.2.14": version "19.2.14" resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.14.tgz#39604929b5e3957e3a6fa0001dafb17c7af70bad" integrity sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w== @@ -2992,14 +2985,6 @@ babel-plugin-polyfill-regenerator@^0.6.5: dependencies: "@babel/helper-define-polyfill-provider" "^0.6.5" -babel-runtime@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" - integrity sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g== - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -3252,11 +3237,6 @@ core-js-compat@^3.43.0: dependencies: browserslist "^4.25.1" -core-js@^2.4.0: - version "2.6.12" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" - integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== - cosmiconfig-typescript-loader@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.1.0.tgz#7f644503e1c2bff90aed2d29a637008f279646bb" @@ -4422,7 +4402,7 @@ jiti@^2.6.1: resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.6.1.tgz#178ef2fc9a1a594248c20627cd820187a4d78d92" integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ== -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: +js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -4718,14 +4698,14 @@ log-update@^6.1.0: strip-ansi "^7.1.0" wrap-ansi "^9.0.0" -loose-envify@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== +lottie-react@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lottie-react/-/lottie-react-2.4.1.tgz#4bd3f2a8a5e48edbd43c05ca5080fdd50f049d31" + integrity sha512-LQrH7jlkigIIv++wIyrOYFLHSKQpEY4zehPicL9bQsrt1rnoKRYCYgpCUe5maqylNtacy58/sQDZTkwMcTRxZw== dependencies: - js-tokens "^3.0.0 || ^4.0.0" + lottie-web "^5.10.2" -lottie-web@^5.12.2: +lottie-web@^5.10.2: version "5.13.0" resolved "https://registry.yarnpkg.com/lottie-web/-/lottie-web-5.13.0.tgz#441d3df217cc8ba302338c3f168e1a3af0f221d3" integrity sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ== @@ -4868,11 +4848,6 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" -object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - object-inspect@^1.13.3, object-inspect@^1.13.4: version "1.13.4" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" @@ -5027,7 +5002,7 @@ postcss-selector-parser@6.0.10: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss@^8.5.23: +postcss@^8.5.25: version "8.5.26" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.26.tgz#6e75135780c7e10df3433bf2266c552d35c8c620" integrity sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ== @@ -5056,15 +5031,6 @@ pretty-bytes@^6.1.1: resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.1.1.tgz#38cd6bb46f47afbf667c202cfc754bffd2016a3b" integrity sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ== -prop-types@^15.6.1: - version "15.8.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" - integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.13.1" - pump@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.3.tgz#151d979f1a29668dc0025ec589a455b53282268d" @@ -5173,20 +5139,6 @@ react-dom@^19.2.6: dependencies: scheduler "^0.27.0" -react-is@^16.13.1: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react-lottie@^1.2.4: - version "1.2.10" - resolved "https://registry.yarnpkg.com/react-lottie/-/react-lottie-1.2.10.tgz#399f78a448a7833b2380d74fc489ecf15f8d18c7" - integrity sha512-x0eWX3Z6zSx1XM5QSjnLupc6D22LlMCB0PH06O/N/epR2hsLaj1Vxd9RtMnbbEHjJ/qlsgHJ6bpN3vnZI92hjw== - dependencies: - babel-runtime "^6.26.0" - lottie-web "^5.12.2" - prop-types "^15.6.1" - react-remove-scroll-bar@^2.3.7: version "2.3.8" resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223" @@ -5260,11 +5212,6 @@ regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" - integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== - regexp.prototype.flags@^1.5.3, regexp.prototype.flags@^1.5.4: version "1.5.4" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19" @@ -5353,28 +5300,28 @@ rfdc@^1.4.1: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== -rolldown@~1.2.0: - version "1.2.3" - resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.2.3.tgz#103bdcbbd575d51265277b8b510f080827b6eb6f" - integrity sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A== +rolldown@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.2.4.tgz#a70655fdd305b829bbc0fc598b3dd1765b4a87dc" + integrity sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w== dependencies: - "@oxc-project/types" "=0.143.0" + "@oxc-project/types" "=0.144.0" "@rolldown/pluginutils" "^1.0.0" optionalDependencies: - "@rolldown/binding-android-arm64" "1.2.3" - "@rolldown/binding-darwin-arm64" "1.2.3" - "@rolldown/binding-darwin-x64" "1.2.3" - "@rolldown/binding-freebsd-x64" "1.2.3" - "@rolldown/binding-linux-arm-gnueabihf" "1.2.3" - "@rolldown/binding-linux-arm64-gnu" "1.2.3" - "@rolldown/binding-linux-arm64-musl" "1.2.3" - "@rolldown/binding-linux-ppc64-gnu" "1.2.3" - "@rolldown/binding-linux-s390x-gnu" "1.2.3" - "@rolldown/binding-linux-x64-gnu" "1.2.3" - "@rolldown/binding-linux-x64-musl" "1.2.3" - "@rolldown/binding-openharmony-arm64" "1.2.3" - "@rolldown/binding-win32-arm64-msvc" "1.2.3" - "@rolldown/binding-win32-x64-msvc" "1.2.3" + "@rolldown/binding-android-arm64" "1.2.4" + "@rolldown/binding-darwin-arm64" "1.2.4" + "@rolldown/binding-darwin-x64" "1.2.4" + "@rolldown/binding-freebsd-x64" "1.2.4" + "@rolldown/binding-linux-arm-gnueabihf" "1.2.4" + "@rolldown/binding-linux-arm64-gnu" "1.2.4" + "@rolldown/binding-linux-arm64-musl" "1.2.4" + "@rolldown/binding-linux-ppc64-gnu" "1.2.4" + "@rolldown/binding-linux-s390x-gnu" "1.2.4" + "@rolldown/binding-linux-x64-gnu" "1.2.4" + "@rolldown/binding-linux-x64-musl" "1.2.4" + "@rolldown/binding-openharmony-arm64" "1.2.4" + "@rolldown/binding-win32-arm64-msvc" "1.2.4" + "@rolldown/binding-win32-x64-msvc" "1.2.4" rollup@^4.53.3: version "4.61.1" @@ -6111,15 +6058,15 @@ vite-plugin-pwa@^1.3.0: workbox-build "^7.4.1" workbox-window "^7.4.1" -vite@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/vite/-/vite-8.2.0.tgz#902fcd3dc0312f553c85b6cbc4625dcaca2d8df8" - integrity sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ== +vite@8.2.1: + version "8.2.1" + resolved "https://registry.yarnpkg.com/vite/-/vite-8.2.1.tgz#6fc8d8bb843bd52353091fac978e194d4de5b31d" + integrity sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw== dependencies: lightningcss "^1.33.0" picomatch "^4.0.5" - postcss "^8.5.23" - rolldown "~1.2.0" + postcss "^8.5.25" + rolldown "~1.2.1" tinyglobby "^0.2.17" optionalDependencies: fsevents "~2.3.3" From d8ef0e70e0d265a8424276daee0a595ac31993c0 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:41:11 +0700 Subject: [PATCH 136/136] chore: sync Wails CLI version in CI with go.mod (v2.14.0) (#2547) 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 --- .github/workflows/wails.yml | 2 +- AGENTS.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wails.yml b/.github/workflows/wails.yml index b74faa5c..0cf8acca 100644 --- a/.github/workflows/wails.yml +++ b/.github/workflows/wails.yml @@ -76,7 +76,7 @@ jobs: node-version: "22.x" - name: Install Wails - run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0 + run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.14.0 shell: bash - name: Install Linux Wails deps diff --git a/AGENTS.md b/AGENTS.md index d7512540..e99b0d33 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -88,6 +88,8 @@ go run cmd/http/main.go 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