Skip to content
This repository has been archived by the owner on Sep 2, 2024. It is now read-only.

Commit

Permalink
feat: sweep alby shared funds balance
Browse files Browse the repository at this point in the history
  • Loading branch information
rolznz committed Jun 25, 2024
1 parent c23ec75 commit b83a3a1
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 15 deletions.
35 changes: 35 additions & 0 deletions alby/alby_oauth_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"math"
"net/http"
"strconv"
"strings"
Expand All @@ -19,6 +20,7 @@ import (
"github.com/getAlby/nostr-wallet-connect/config"
"github.com/getAlby/nostr-wallet-connect/db"
"github.com/getAlby/nostr-wallet-connect/events"
"github.com/getAlby/nostr-wallet-connect/lnclient"
"github.com/getAlby/nostr-wallet-connect/logger"
nip47 "github.com/getAlby/nostr-wallet-connect/nip47/models"
"github.com/getAlby/nostr-wallet-connect/service/keys"
Expand Down Expand Up @@ -247,6 +249,39 @@ func (svc *albyOAuthService) GetBalance(ctx context.Context) (*AlbyBalance, erro
return balance, nil
}

func (svc *albyOAuthService) DrainSharedWallet(ctx context.Context, lnClient lnclient.LNClient) error {
balance, err := svc.GetBalance(ctx)
if err != nil {
logger.Logger.WithError(err).Error("Failed to fetch shared balance")
return err
}

amount := int64(math.Floor(
float64(balance.Balance)*1000* // Alby shared node balance in sats
(1-8/1000)* // Alby service fee (0.8%)
0.99)) - // Maximum potential routing fees (1%)
10000 // Alby fee reserve (10 sats)

if amount < 1000 {
return errors.New("Not enough balance remaining")
}

logger.Logger.WithField("amount", amount).WithError(err).Error("Draining Alby shared wallet funds")

transaction, err := lnClient.MakeInvoice(ctx, amount, "Send shared wallet funds to Alby Hub", "", 120)
if err != nil {
logger.Logger.WithField("amount", amount).WithError(err).Error("Failed to make invoice")
return err
}

err = svc.SendPayment(ctx, transaction.Invoice)
if err != nil {
logger.Logger.WithField("amount", amount).WithError(err).Error("Failed to pay invoice from shared node")
return err
}
return nil
}

func (svc *albyOAuthService) SendPayment(ctx context.Context, invoice string) error {
token, err := svc.fetchUserToken(ctx)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions alby/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"github.com/getAlby/nostr-wallet-connect/events"
"github.com/getAlby/nostr-wallet-connect/lnclient"
)

type AlbyOAuthService interface {
Expand All @@ -17,6 +18,7 @@ type AlbyOAuthService interface {
GetBalance(ctx context.Context) (*AlbyBalance, error)
GetMe(ctx context.Context) (*AlbyMe, error)
SendPayment(ctx context.Context, invoice string) error
DrainSharedWallet(ctx context.Context, lnClient lnclient.LNClient) error
}

type AlbyBalanceResponse struct {
Expand Down
73 changes: 66 additions & 7 deletions frontend/src/screens/channels/Channels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "src/components/ui/dropdown-menu.tsx";
import { LoadingButton } from "src/components/ui/loading-button.tsx";
import {
Table,
TableBody,
Expand All @@ -43,7 +44,7 @@ import {
TableHeader,
TableRow,
} from "src/components/ui/table.tsx";
import { toast } from "src/components/ui/use-toast.ts";
import { useToast } from "src/components/ui/use-toast.ts";
import { ONCHAIN_DUST_SATS } from "src/constants.ts";
import { useAlbyBalance } from "src/hooks/useAlbyBalance.ts";
import { useBalances } from "src/hooks/useBalances.ts";
Expand All @@ -68,11 +69,14 @@ export default function Channels() {
const { data: channels, mutate: reloadChannels } = useChannels();
const { data: nodeConnectionInfo } = useNodeConnectionInfo();
const { data: balances } = useBalances();
const { data: albyBalance } = useAlbyBalance();
const { data: albyBalance, mutate: reloadAlbyBalance } = useAlbyBalance();
const [nodes, setNodes] = React.useState<Node[]>([]);
const { mutate: reloadInfo } = useInfo();
const { data: csrf } = useCSRF();
const redeemOnchainFunds = useRedeemOnchainFunds();
const { toast } = useToast();
const [drainingAlbySharedFunds, setDrainingAlbySharedFunds] =
React.useState(false);

// TODO: move to NWC backend
const loadNodeStats = React.useCallback(async () => {
Expand Down Expand Up @@ -171,7 +175,10 @@ export default function Channels() {
toast({ title: "Sucessfully closed channel" });
} catch (error) {
console.error(error);
alert("Something went wrong: " + error);
toast({
variant: "destructive",
description: "Something went wrong: " + error,
});
}
}

Expand Down Expand Up @@ -213,7 +220,10 @@ export default function Channels() {
toast({ title: "Sucessfully updated channel" });
} catch (error) {
console.error(error);
alert("Something went wrong: " + error);
toast({
variant: "destructive",
description: "Something went wrong: " + error,
});
}
}

Expand Down Expand Up @@ -241,10 +251,13 @@ export default function Channels() {
},
});
await reloadInfo();
alert(`🎉 Router reset`);
toast({ description: "🎉 Router reset" });
} catch (error) {
console.error(error);
alert("Something went wrong: " + error);
toast({
variant: "destructive",
description: "Something went wrong: " + error,
});
}
}

Expand Down Expand Up @@ -335,7 +348,7 @@ export default function Channels() {
></AppHeader>

<div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
{albyBalance && albyBalance?.sats >= 100 && (
{albyBalance && albyBalance.sats >= 100 && (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Expand All @@ -348,6 +361,52 @@ export default function Channels() {
{new Intl.NumberFormat().format(albyBalance?.sats)} sats
</div>
</CardContent>
<CardFooter className="flex justify-end space-x-1">
<LoadingButton
loading={drainingAlbySharedFunds}
onClick={async () => {
if (
!channels?.some(
(channel) => channel.remoteBalance > albyBalance.sats
)
) {
toast({
title: "Please increase your receiving capacity first",
});
return;
}

setDrainingAlbySharedFunds(true);
try {
if (!csrf) {
throw new Error("csrf not loaded");
}

await request("/api/alby/drain", {
method: "POST",
headers: {
"X-CSRF-Token": csrf,
"Content-Type": "application/json",
},
});
await reloadAlbyBalance();
toast({
description: "🎉 Funds swept from Alby shared wallet!",
});
} catch (error) {
console.error(error);
toast({
variant: "destructive",
description: "Something went wrong: " + error,
});
}
setDrainingAlbySharedFunds(false);
}}
variant="outline"
>
Sweep
</LoadingButton>
</CardFooter>
</Card>
)}
<Card>
Expand Down
29 changes: 24 additions & 5 deletions alby/alby_http_service.go → http/alby_http_service.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
package alby
package http

import (
"fmt"
"net/http"

"github.com/getAlby/nostr-wallet-connect/alby"
"github.com/getAlby/nostr-wallet-connect/config"
"github.com/getAlby/nostr-wallet-connect/logger"
"github.com/getAlby/nostr-wallet-connect/service"
"github.com/labstack/echo/v4"
)

type AlbyHttpService struct {
albyOAuthSvc AlbyOAuthService
albyOAuthSvc alby.AlbyOAuthService
appConfig *config.AppConfig
svc service.Service
}

func NewAlbyHttpService(albyOAuthSvc AlbyOAuthService, appConfig *config.AppConfig) *AlbyHttpService {
func NewAlbyHttpService(svc service.Service, albyOAuthSvc alby.AlbyOAuthService, appConfig *config.AppConfig) *AlbyHttpService {
return &AlbyHttpService{
albyOAuthSvc: albyOAuthSvc,
appConfig: appConfig,
svc: svc,
}
}

Expand All @@ -26,6 +30,7 @@ func (albyHttpSvc *AlbyHttpService) RegisterSharedRoutes(e *echo.Echo, authMiddl
e.GET("/api/alby/me", albyHttpSvc.albyMeHandler, authMiddleware)
e.GET("/api/alby/balance", albyHttpSvc.albyBalanceHandler, authMiddleware)
e.POST("/api/alby/pay", albyHttpSvc.albyPayHandler, authMiddleware)
e.POST("/api/alby/drain", albyHttpSvc.albyDrainHandler, authMiddleware)
e.POST("/api/alby/link-account", albyHttpSvc.albyLinkAccountHandler, authMiddleware)
}

Expand Down Expand Up @@ -75,13 +80,13 @@ func (albyHttpSvc *AlbyHttpService) albyBalanceHandler(c echo.Context) error {
})
}

return c.JSON(http.StatusOK, &AlbyBalanceResponse{
return c.JSON(http.StatusOK, &alby.AlbyBalanceResponse{
Sats: balance.Balance,
})
}

func (albyHttpSvc *AlbyHttpService) albyPayHandler(c echo.Context) error {
var payRequest AlbyPayRequest
var payRequest alby.AlbyPayRequest
if err := c.Bind(&payRequest); err != nil {
return c.JSON(http.StatusBadRequest, ErrorResponse{
Message: fmt.Sprintf("Bad request: %s", err.Error()),
Expand All @@ -99,6 +104,20 @@ func (albyHttpSvc *AlbyHttpService) albyPayHandler(c echo.Context) error {
return c.NoContent(http.StatusNoContent)
}

func (albyHttpSvc *AlbyHttpService) albyDrainHandler(c echo.Context) error {

err := albyHttpSvc.albyOAuthSvc.DrainSharedWallet(c.Request().Context(), albyHttpSvc.svc.GetLNClient())

if err != nil {
logger.Logger.WithError(err).Error("Failed to drain shared wallet")
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: fmt.Sprintf("Failed to drain shared wallet: %s", err.Error()),
})
}

return c.NoContent(http.StatusNoContent)
}

func (albyHttpSvc *AlbyHttpService) albyLinkAccountHandler(c echo.Context) error {
err := albyHttpSvc.albyOAuthSvc.LinkAccount(c.Request().Context())
if err != nil {
Expand Down
5 changes: 2 additions & 3 deletions http/http_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"github.com/labstack/echo/v4/middleware"
"gorm.io/gorm"

"github.com/getAlby/nostr-wallet-connect/alby"
"github.com/getAlby/nostr-wallet-connect/config"
"github.com/getAlby/nostr-wallet-connect/db"
"github.com/getAlby/nostr-wallet-connect/events"
Expand All @@ -27,7 +26,7 @@ import (

type HttpService struct {
api api.API
albyHttpSvc *alby.AlbyHttpService
albyHttpSvc *AlbyHttpService
cfg config.Config
eventPublisher events.EventPublisher
db *gorm.DB
Expand All @@ -41,7 +40,7 @@ const (
func NewHttpService(svc service.Service, eventPublisher events.EventPublisher) *HttpService {
return &HttpService{
api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetKeys(), svc.GetAlbyOAuthSvc(), svc.GetEventPublisher()),
albyHttpSvc: alby.NewAlbyHttpService(svc.GetAlbyOAuthSvc(), svc.GetConfig().GetEnv()),
albyHttpSvc: NewAlbyHttpService(svc, svc.GetAlbyOAuthSvc(), svc.GetConfig().GetEnv()),
cfg: svc.GetConfig(),
eventPublisher: eventPublisher,
db: svc.GetDB(),
Expand Down
6 changes: 6 additions & 0 deletions wails/wails_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string
return WailsRequestRouterResponse{Body: &alby.AlbyBalanceResponse{
Sats: balance.Balance,
}, Error: ""}
case "/api/alby/drain":
err := app.svc.GetAlbyOAuthSvc().DrainSharedWallet(ctx, app.svc.GetLNClient())
if err != nil {
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
}
return WailsRequestRouterResponse{Body: nil, Error: ""}
case "/api/alby/pay":
payRequest := &alby.AlbyPayRequest{}
err := json.Unmarshal([]byte(body), payRequest)
Expand Down

0 comments on commit b83a3a1

Please sign in to comment.