Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -495,12 +495,16 @@ func (api *api) ListApps(limit uint64, offset uint64, filters ListAppsFilters, o
query = query.Where("last_used_at IS NULL OR last_used_at < ?", time.Now().Add(-60*24*time.Hour))
}

if filters.SubWallets != nil && !*filters.SubWallets {
// exclude subwallets :scream:
if api.db.Dialector.Name() == "sqlite" {
query = query.Where("metadata is NULL OR JSON_EXTRACT(metadata, '$.app_store_app_id') IS NULL OR JSON_EXTRACT(metadata, '$.app_store_app_id') != ?", constants.SUBWALLET_APPSTORE_APP_ID)
if filters.SubWallets != nil {
if *filters.SubWallets {
query = query.Where(datatypes.JSONQuery("metadata").Equals(constants.SUBWALLET_APPSTORE_APP_ID, "app_store_app_id"))
} else {
query = query.Where("metadata IS NULL OR metadata->>'app_store_app_id' IS NULL OR metadata->>'app_store_app_id' != ?", constants.SUBWALLET_APPSTORE_APP_ID)
// exclude subwallets :scream:
if api.db.Dialector.Name() == "sqlite" {
query = query.Where("metadata is NULL OR JSON_EXTRACT(metadata, '$.app_store_app_id') IS NULL OR JSON_EXTRACT(metadata, '$.app_store_app_id') != ?", constants.SUBWALLET_APPSTORE_APP_ID)
} else {
query = query.Where("metadata IS NULL OR metadata->>'app_store_app_id' IS NULL OR metadata->>'app_store_app_id' != ?", constants.SUBWALLET_APPSTORE_APP_ID)
}
}
}

Expand All @@ -523,6 +527,17 @@ func (api *api) ListApps(limit uint64, offset uint64, filters ListAppsFilters, o
logger.Logger.WithError(result.Error).Error("Failed to count DB apps")
return nil, result.Error
}

var totalBalance *int64
if filters.SubWallets != nil && *filters.SubWallets {
totalBalanceMsat, err := queries.GetTotalSubwalletBalance(api.db)
if err != nil {
logger.Logger.WithError(err).Error("Failed to calculate total subwallet balance")
return nil, err
}
totalBalance = &totalBalanceMsat
}

query = query.Offset(int(offset)).Limit(int(limit))

err := query.Find(&dbApps).Error
Expand Down Expand Up @@ -598,8 +613,9 @@ func (api *api) ListApps(limit uint64, offset uint64, filters ListAppsFilters, o
apiApps = append(apiApps, apiApp)
}
return &ListAppsResponse{
Apps: apiApps,
TotalCount: uint64(totalCount),
Apps: apiApps,
TotalCount: uint64(totalCount),
TotalBalance: totalBalance,
}, nil
}

Expand Down
5 changes: 3 additions & 2 deletions api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,9 @@ type ListAppsFilters struct {
}

type ListAppsResponse struct {
Apps []App `json:"apps"`
TotalCount uint64 `json:"totalCount"`
Apps []App `json:"apps"`
TotalCount uint64 `json:"totalCount"`
TotalBalance *int64 `json:"totalBalance,omitempty"`
}

type UpdateAppRequest struct {
Expand Down
40 changes: 40 additions & 0 deletions db/queries/get_total_subwallet_balance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package queries

import (
"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"gorm.io/datatypes"
"gorm.io/gorm"
)

func GetTotalSubwalletBalance(tx *gorm.DB) (int64, error) {
subwalletAppIDsQuery := tx.Model(&db.App{}).
Select("id").
Where(datatypes.JSONQuery("metadata").Equals(constants.SUBWALLET_APPSTORE_APP_ID, "app_store_app_id"))

var received struct {
Sum int64
}
res := tx.
Table("transactions").
Select("SUM(amount_msat) as sum").
Where("app_id IN (?) AND type = ? AND state = ?", subwalletAppIDsQuery, constants.TRANSACTION_TYPE_INCOMING, constants.TRANSACTION_STATE_SETTLED).
Scan(&received)
if res.Error != nil {
return 0, res.Error
}

var spent struct {
Sum int64
}
res = tx.
Table("transactions").
Select("SUM(amount_msat + fee_msat + fee_reserve_msat) as sum").
Where("app_id IN (?) AND type = ? AND (state = ? OR state = ?)", subwalletAppIDsQuery, constants.TRANSACTION_TYPE_OUTGOING, constants.TRANSACTION_STATE_SETTLED, constants.TRANSACTION_STATE_PENDING).
Scan(&spent)
if res.Error != nil {
return 0, res.Error
}

return received.Sum - spent.Sum, nil
}
64 changes: 64 additions & 0 deletions db/queries/get_total_subwallet_balance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package queries

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/datatypes"

"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/tests"
)

func TestGetTotalSubwalletBalance(t *testing.T) {
svc, err := tests.CreateTestService(t)
require.NoError(t, err)
defer svc.Remove()

subwalletA, _, err := tests.CreateApp(svc)
require.NoError(t, err)
subwalletA.Isolated = true
subwalletA.Metadata = datatypes.JSON([]byte(fmt.Sprintf(`{"app_store_app_id":"%s"}`, constants.SUBWALLET_APPSTORE_APP_ID)))
svc.DB.Save(&subwalletA)

subwalletB, _, err := tests.CreateApp(svc)
require.NoError(t, err)
subwalletB.Isolated = true
subwalletB.Metadata = datatypes.JSON([]byte(fmt.Sprintf(`{"app_store_app_id":"%s"}`, constants.SUBWALLET_APPSTORE_APP_ID)))
svc.DB.Save(&subwalletB)

incomingSubwalletTx := db.Transaction{
AppId: &subwalletA.ID,
Type: constants.TRANSACTION_TYPE_INCOMING,
State: constants.TRANSACTION_STATE_SETTLED,
AmountMsat: 5000,
}
svc.DB.Save(&incomingSubwalletTx)

outgoingSettledSubwalletTx := db.Transaction{
AppId: &subwalletA.ID,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_SETTLED,
AmountMsat: 1000,
FeeMsat: 100,
FeeReserveMsat: 0,
}
svc.DB.Save(&outgoingSettledSubwalletTx)

outgoingPendingSubwalletTx := db.Transaction{
AppId: &subwalletB.ID,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_PENDING,
AmountMsat: 2000,
FeeMsat: 0,
FeeReserveMsat: 300,
}
svc.DB.Save(&outgoingPendingSubwalletTx)

total, err := GetTotalSubwalletBalance(svc.DB)
require.NoError(t, err)
assert.Equal(t, int64(1600), total)
}
24 changes: 12 additions & 12 deletions frontend/src/screens/subwallets/SubwalletList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
import { ExternalLinkButton } from "src/components/ui/custom/external-link-button";
import { LinkButton } from "src/components/ui/custom/link-button";
import { UpgradeDialog } from "src/components/UpgradeDialog";
import { LIST_APPS_LIMIT, SUBWALLET_APPSTORE_APP_ID } from "src/constants";
import { LIST_APPS_LIMIT } from "src/constants";
import { useAlbyMe } from "src/hooks/useAlbyMe";
import { useApps } from "src/hooks/useApps";
import { useBalances } from "src/hooks/useBalances";
Expand All @@ -38,11 +38,11 @@ export function SubwalletList() {
const { data: info } = useInfo();
const [page, setPage] = useState(1);
const appsListRef = useRef<HTMLDivElement>(null);
const { data: appsData } = useApps(
const { data: subwalletAppsData } = useApps(
undefined,
page,
{
appStoreAppId: SUBWALLET_APPSTORE_APP_ID,
subWallets: true,
},
"created_at"
);
Expand All @@ -59,21 +59,20 @@ export function SubwalletList() {

if (
!info ||
!appsData ||
!subwalletAppsData ||
!balances ||
(info.albyAccountConnected && !albyMe && !albyMeError)
) {
return <Loading />;
}

const subwalletApps = appsData.apps;
const subwalletApps = subwalletAppsData.apps;

if (!subwalletApps.length) {
if (!subwalletAppsData.totalCount) {
return <SubwalletIntro />;
}

const subwalletTotalAmount =
subwalletApps.reduce((total, app) => total + app.balance, 0) || 0;
const subwalletTotalAmount = subwalletAppsData.totalBalance || 0;
const isSufficientlyBacked =
subwalletTotalAmount <= balances.lightning.totalSpendable;

Expand All @@ -91,7 +90,8 @@ export function SubwalletList() {
>
<HelpCircle className="size-4" />
</ExternalLinkButton>
{!albyMe?.subscription.plan_code && subwalletApps?.length >= 3 ? (
{!albyMe?.subscription.plan_code &&
subwalletAppsData.totalCount >= 3 ? (
<UpgradeDialog>
<ResponsiveButton icon={CirclePlusIcon} text="New Sub-wallet" />
</UpgradeDialog>
Expand All @@ -106,7 +106,7 @@ export function SubwalletList() {
}
/>

{!albyMe?.subscription.plan_code && subwalletApps.length >= 3 && (
{!albyMe?.subscription.plan_code && subwalletAppsData.totalCount >= 3 && (
<>
<Alert>
<InfoIcon />
Expand Down Expand Up @@ -168,7 +168,7 @@ export function SubwalletList() {
<CardContent className="grow flex flex-col gap-4">
<div className="flex flex-col gap-2">
<span className="text-2xl font-medium">
{subwalletApps.length} /{" "}
{subwalletAppsData.totalCount} /{" "}
{albyMe?.subscription.plan_code ? "∞" : 3}
</span>
{isSufficientlyBacked ? (
Expand Down Expand Up @@ -202,7 +202,7 @@ export function SubwalletList() {

<CustomPagination
limit={LIST_APPS_LIMIT}
totalCount={appsData.totalCount}
totalCount={subwalletAppsData.totalCount}
page={page}
handlePageChange={handlePageChange}
/>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,7 @@ export type OnchainTransaction = {
export type ListAppsResponse = {
apps: App[];
totalCount: number;
totalBalance?: number;
};

export type ListTransactionsResponse = {
Expand Down