Skip to content

Commit

Permalink
feat: sweep alby shared funds balance (#503)
Browse files Browse the repository at this point in the history
  • Loading branch information
rolznz authored Jun 26, 2024
1 parent 8e57739 commit c5939b4
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 14 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
72 changes: 66 additions & 6 deletions frontend/src/screens/channels/Channels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "src/components/ui/dropdown-menu.tsx";
import { LoadingButton } from "src/components/ui/loading-button.tsx";
import { Progress } from "src/components/ui/progress.tsx";
import {
Table,
Expand All @@ -51,7 +52,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "src/components/ui/tooltip.tsx";
import { toast } from "src/components/ui/use-toast.ts";
import { useToast } from "src/components/ui/use-toast.ts";
import {
ALBY_HIDE_HOSTED_BALANCE_BELOW as ALBY_HIDE_HOSTED_BALANCE_LIMIT,
ONCHAIN_DUST_SATS,
Expand Down Expand Up @@ -79,11 +80,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 @@ -182,7 +186,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 @@ -224,7 +231,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 @@ -252,10 +262,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 @@ -367,6 +380,53 @@ 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 from Alby shared wallet moved to self-custody!",
});
} catch (error) {
console.error(error);
toast({
variant: "destructive",
description: "Something went wrong: " + error,
});
}
setDrainingAlbySharedFunds(false);
}}
variant="outline"
>
Take Custody
</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 c5939b4

Please sign in to comment.