mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
feat: track forwards to display on a home page widget (#1623)
* feat: track forwards to display on a home page widget * fix: test * chore: improve copy * feat: configure PPM routing fees
This commit is contained in:
parent
0ac8cc5e25
commit
16eff34b8a
20 changed files with 331 additions and 14 deletions
|
|
@ -565,6 +565,7 @@ Internally Alby Hub uses a basic implementation of the pubsub messaging pattern
|
|||
- `nwc_alby_account_connected` - user connects alby account for first time
|
||||
- `nwc_swap_succeeded` - successfully made a boltz swap
|
||||
- `nwc_rebalance_succeeded` - successfully rebalanced channels
|
||||
- `nwc_payment_forwarded` - successfully forwarded a payment and earned routing fees
|
||||
|
||||
### NIP-47 Handlers
|
||||
|
||||
|
|
|
|||
25
api/api.go
25
api/api.go
|
|
@ -554,6 +554,7 @@ func (api *api) ListChannels(ctx context.Context) ([]Channel, error) {
|
|||
Confirmations: channel.Confirmations,
|
||||
ConfirmationsRequired: channel.ConfirmationsRequired,
|
||||
ForwardingFeeBaseMsat: channel.ForwardingFeeBaseMsat,
|
||||
ForwardingFeeProportionalMillionths: channel.ForwardingFeeProportionalMillionths,
|
||||
UnspendablePunishmentReserve: channel.UnspendablePunishmentReserve,
|
||||
CounterpartyUnspendablePunishmentReserve: channel.CounterpartyUnspendablePunishmentReserve,
|
||||
Error: channel.Error,
|
||||
|
|
@ -1582,3 +1583,27 @@ func (api *api) parseExpiresAt(expiresAtString string) (*time.Time, error) {
|
|||
}
|
||||
return expiresAt, nil
|
||||
}
|
||||
|
||||
func (api *api) GetForwards() (*GetForwardsResponse, error) {
|
||||
var forwards []db.Forward
|
||||
err := api.db.Find(&forwards).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var totalOutboundAmount uint64
|
||||
var totalFeeEarned uint64
|
||||
|
||||
for _, forward := range forwards {
|
||||
totalOutboundAmount += forward.OutboundAmountForwardedMsat
|
||||
totalFeeEarned += forward.TotalFeeEarnedMsat
|
||||
}
|
||||
|
||||
numForwards := len(forwards)
|
||||
|
||||
return &GetForwardsResponse{
|
||||
OutboundAmountForwardedMsat: totalOutboundAmount,
|
||||
TotalFeeEarnedMsat: totalFeeEarned,
|
||||
NumForwards: uint64(numForwards),
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ type API interface {
|
|||
GetCustomNodeCommands() (*CustomNodeCommandsResponse, error)
|
||||
ExecuteCustomNodeCommand(ctx context.Context, command string) (interface{}, error)
|
||||
SendEvent(event string)
|
||||
GetForwards() (*GetForwardsResponse, error)
|
||||
}
|
||||
|
||||
type App struct {
|
||||
|
|
@ -489,6 +490,7 @@ type Channel struct {
|
|||
Confirmations *uint32 `json:"confirmations"`
|
||||
ConfirmationsRequired *uint32 `json:"confirmationsRequired"`
|
||||
ForwardingFeeBaseMsat uint32 `json:"forwardingFeeBaseMsat"`
|
||||
ForwardingFeeProportionalMillionths uint32 `json:"forwardingFeeProportionalMillionths"`
|
||||
UnspendablePunishmentReserve uint64 `json:"unspendablePunishmentReserve"`
|
||||
CounterpartyUnspendablePunishmentReserve uint64 `json:"counterpartyUnspendablePunishmentReserve"`
|
||||
Error *string `json:"error"`
|
||||
|
|
@ -544,3 +546,9 @@ type CustomNodeCommandsResponse struct {
|
|||
type ExecuteCustomNodeCommandRequest struct {
|
||||
Command string `json:"command"`
|
||||
}
|
||||
|
||||
type GetForwardsResponse struct {
|
||||
OutboundAmountForwardedMsat uint64 `json:"outboundAmountForwardedMsat"`
|
||||
TotalFeeEarnedMsat uint64 `json:"totalFeeEarnedMsat"`
|
||||
NumForwards uint64 `json:"numForwards"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ var expectedTables = []string{
|
|||
"swaps",
|
||||
"user_configs",
|
||||
"migrations",
|
||||
"forwards",
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
|
|
|||
36
db/migrations/202508192137_forwards.go
Normal file
36
db/migrations/202508192137_forwards.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"text/template"
|
||||
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const forwardsMigration = `
|
||||
CREATE TABLE forwards(
|
||||
id {{ .AutoincrementPrimaryKey }},
|
||||
outbound_amount_forwarded_msat bigint,
|
||||
total_fee_earned_msat bigint,
|
||||
created_at {{ .Timestamp }},
|
||||
updated_at {{ .Timestamp }}
|
||||
);
|
||||
`
|
||||
|
||||
var forwardsMigrationTmpl = template.Must(template.New("forwardsMigration").Parse(forwardsMigration))
|
||||
|
||||
var _202508192137_forwards = &gormigrate.Migration{
|
||||
ID: "202508192137_forwards",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
|
||||
if err := exec(tx, forwardsMigrationTmpl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
|
@ -36,6 +36,7 @@ func Migrate(gormDB *gorm.DB) error {
|
|||
_202508041738_app_last_used,
|
||||
_202508041739_response_events_index,
|
||||
_202508151405_swap_xpub,
|
||||
_202508192137_forwards,
|
||||
})
|
||||
|
||||
return m.Migrate()
|
||||
|
|
|
|||
|
|
@ -114,6 +114,14 @@ type Swap struct {
|
|||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type Forward struct {
|
||||
ID uint
|
||||
OutboundAmountForwardedMsat uint64
|
||||
TotalFeeEarnedMsat uint64
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
const (
|
||||
REQUEST_EVENT_STATE_HANDLER_EXECUTING = "executing"
|
||||
REQUEST_EVENT_STATE_HANDLER_EXECUTED = "executed"
|
||||
|
|
|
|||
|
|
@ -22,18 +22,26 @@ type Props = {
|
|||
};
|
||||
|
||||
export function RoutingFeeDialogContent({ channel }: Props) {
|
||||
const currentFee: number = React.useMemo(() => {
|
||||
return Math.floor(channel.forwardingFeeBaseMsat / 1000);
|
||||
}, [channel.forwardingFeeBaseMsat]);
|
||||
const [forwardingFee, setForwardingFee] = React.useState(
|
||||
currentFee ? currentFee.toString() : ""
|
||||
const currentBaseFeeSats: number = Math.floor(
|
||||
channel.forwardingFeeBaseMsat / 1000
|
||||
);
|
||||
const currentFeePPM: number = channel.forwardingFeeProportionalMillionths;
|
||||
|
||||
const [baseFeeSats, setBaseFeeSats] = React.useState(
|
||||
currentBaseFeeSats !== undefined ? currentBaseFeeSats.toString() : ""
|
||||
);
|
||||
const [
|
||||
forwardingFeeProportionalMillionths,
|
||||
setForwardingFeeProportionalMillionths,
|
||||
] = React.useState(
|
||||
currentFeePPM !== undefined ? currentFeePPM.toString() : ""
|
||||
);
|
||||
const { toast } = useToast();
|
||||
const { mutate: reloadChannels } = useChannels();
|
||||
|
||||
async function updateFee() {
|
||||
try {
|
||||
const forwardingFeeBaseMsat = +forwardingFee * 1000;
|
||||
const forwardingFeeBaseMsat = +baseFeeSats * 1000;
|
||||
|
||||
console.info(
|
||||
`🎬 Updating channel ${channel.id} with ${channel.remotePubkey}`
|
||||
|
|
@ -48,6 +56,8 @@ export function RoutingFeeDialogContent({ channel }: Props) {
|
|||
},
|
||||
body: JSON.stringify({
|
||||
forwardingFeeBaseMsat: forwardingFeeBaseMsat,
|
||||
forwardingFeeProportionalMillionths:
|
||||
+forwardingFeeProportionalMillionths,
|
||||
} as UpdateChannelRequest),
|
||||
}
|
||||
);
|
||||
|
|
@ -74,7 +84,7 @@ export function RoutingFeeDialogContent({ channel }: Props) {
|
|||
unwanted routing. No matter the fee, you can still receive payments.{" "}
|
||||
</p>
|
||||
<Label htmlFor="fee" className="block mb-2">
|
||||
Routing Fee (sats)
|
||||
Base Routing Fee (sats)
|
||||
</Label>
|
||||
<Input
|
||||
id="fee"
|
||||
|
|
@ -83,9 +93,24 @@ export function RoutingFeeDialogContent({ channel }: Props) {
|
|||
required
|
||||
autoFocus
|
||||
min={0}
|
||||
value={forwardingFee}
|
||||
value={baseFeeSats}
|
||||
onChange={(e) => {
|
||||
setForwardingFee(e.target.value.trim());
|
||||
setBaseFeeSats(e.target.value.trim());
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="fee" className="block mt-4 mb-2">
|
||||
PPM Fee (1 PPM = 1 per 1 million sats)
|
||||
</Label>
|
||||
<Input
|
||||
id="fee"
|
||||
name="fee"
|
||||
type="number"
|
||||
required
|
||||
autoFocus
|
||||
min={0}
|
||||
value={forwardingFeeProportionalMillionths}
|
||||
onChange={(e) => {
|
||||
setForwardingFeeProportionalMillionths(e.target.value.trim());
|
||||
}}
|
||||
/>
|
||||
<ExternalLink
|
||||
|
|
@ -100,7 +125,11 @@ export function RoutingFeeDialogContent({ channel }: Props) {
|
|||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={(parseInt(forwardingFee) || 0) == currentFee}
|
||||
disabled={
|
||||
(parseInt(baseFeeSats) || 0) === currentBaseFeeSats &&
|
||||
(parseInt(forwardingFeeProportionalMillionths) || 0) ===
|
||||
currentFeePPM
|
||||
}
|
||||
onClick={updateFee}
|
||||
>
|
||||
Confirm
|
||||
|
|
|
|||
69
frontend/src/components/home/widgets/ForwardsWidget.tsx
Normal file
69
frontend/src/components/home/widgets/ForwardsWidget.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "src/components/ui/card";
|
||||
import { ExternalLinkButton } from "src/components/ui/custom/external-link-button";
|
||||
import { useForwards } from "src/hooks/useForwards";
|
||||
|
||||
export function ForwardsWidget() {
|
||||
const { data: forwards } = useForwards();
|
||||
|
||||
if (!forwards) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Routing</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs">Fees Earned</p>
|
||||
<p className="text-xl font-semibold">
|
||||
{new Intl.NumberFormat().format(
|
||||
Math.floor(forwards.totalFeeEarnedMsat / 1000)
|
||||
)}{" "}
|
||||
sats
|
||||
<FormattedFiatAmount
|
||||
amount={Math.floor(forwards.totalFeeEarnedMsat / 1000)}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs">Total Routed</p>
|
||||
<p className="text-xl font-semibold">
|
||||
{new Intl.NumberFormat().format(
|
||||
Math.floor(forwards.outboundAmountForwardedMsat / 1000)
|
||||
)}{" "}
|
||||
sats
|
||||
<FormattedFiatAmount
|
||||
amount={Math.floor(forwards.outboundAmountForwardedMsat / 1000)}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs">Transactions Routed</p>
|
||||
<p className="text-xl font-semibold">{forwards.numForwards}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end mt-4 items-end">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Earn and support the lightning network by routing payments. To route
|
||||
payments you need public channels and set competitive fees.
|
||||
</p>
|
||||
<ExternalLinkButton
|
||||
variant="secondary"
|
||||
to="https://guides.getalby.com/user-guide/alby-hub/faq/how-can-i-change-routing-fees#changing-the-routing-fee"
|
||||
>
|
||||
Learn more
|
||||
</ExternalLinkButton>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
8
frontend/src/hooks/useForwards.ts
Normal file
8
frontend/src/hooks/useForwards.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import useSWR from "swr";
|
||||
|
||||
import { GetForwardsResponse } from "src/types";
|
||||
import { swrFetcher } from "src/utils/swr";
|
||||
|
||||
export function useForwards() {
|
||||
return useSWR<GetForwardsResponse>("/api/forwards", swrFetcher);
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ import albyGo from "src/assets/suggested-apps/alby-go.png";
|
|||
import zapplanner from "src/assets/suggested-apps/zapplanner.png";
|
||||
import { AppOfTheDayWidget } from "src/components/home/widgets/AppOfTheDayWidget";
|
||||
import { BlockHeightWidget } from "src/components/home/widgets/BlockHeightWidget";
|
||||
import { ForwardsWidget } from "src/components/home/widgets/ForwardsWidget";
|
||||
import { LatestUsedAppsWidget } from "src/components/home/widgets/LatestUsedAppsWidget";
|
||||
import { LightningMessageboardWidget } from "src/components/home/widgets/LightningMessageboardWidget";
|
||||
import { NodeStatusWidget } from "src/components/home/widgets/NodeStatusWidget";
|
||||
|
|
@ -204,6 +205,7 @@ function Home() {
|
|||
<NodeStatusWidget />
|
||||
<BlockHeightWidget />
|
||||
<OnchainFeesWidget />
|
||||
<ForwardsWidget />
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -297,6 +297,7 @@ export type Channel = {
|
|||
confirmations?: number;
|
||||
confirmationsRequired?: number;
|
||||
forwardingFeeBaseMsat: number;
|
||||
forwardingFeeProportionalMillionths: number;
|
||||
unspendablePunishmentReserve: number;
|
||||
counterpartyUnspendablePunishmentReserve: number;
|
||||
error?: string;
|
||||
|
|
@ -647,3 +648,9 @@ export type NewChannelOrder = OnchainOrder | LightningOrder;
|
|||
export type AuthTokenResponse = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
export type GetForwardsResponse = {
|
||||
outboundAmountForwardedMsat: number;
|
||||
totalFeeEarnedMsat: number;
|
||||
numForwards: number;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) {
|
|||
restrictedApiGroup.POST("/autoswap", httpSvc.enableAutoSwapOutHandler)
|
||||
restrictedApiGroup.DELETE("/autoswap", httpSvc.disableAutoSwapOutHandler)
|
||||
restrictedApiGroup.POST("/node/alias", httpSvc.setNodeAliasHandler)
|
||||
restrictedApiGroup.GET("/forwards", httpSvc.forwardsHandler)
|
||||
|
||||
httpSvc.albyHttpSvc.RegisterSharedRoutes(restrictedApiGroup, e)
|
||||
}
|
||||
|
|
@ -1491,3 +1492,14 @@ func (httpSvc *HttpService) setNodeAliasHandler(c echo.Context) error {
|
|||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (httpSvc *HttpService) forwardsHandler(c echo.Context) error {
|
||||
forwards, err := httpSvc.api.GetForwards()
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Message: fmt.Sprintf("Failed to get forwards: %s", err.Error()),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, forwards)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -972,6 +972,7 @@ func (ls *LDKService) ListChannels(ctx context.Context) ([]lnclient.Channel, err
|
|||
Confirmations: ldkChannel.Confirmations,
|
||||
ConfirmationsRequired: ldkChannel.ConfirmationsRequired,
|
||||
ForwardingFeeBaseMsat: ldkChannel.Config.ForwardingFeeBaseMsat,
|
||||
ForwardingFeeProportionalMillionths: ldkChannel.Config.ForwardingFeeProportionalMillionths,
|
||||
UnspendablePunishmentReserve: unspendablePunishmentReserve,
|
||||
CounterpartyUnspendablePunishmentReserve: ldkChannel.CounterpartyUnspendablePunishmentReserve,
|
||||
Error: channelError,
|
||||
|
|
@ -1121,6 +1122,7 @@ func (ls *LDKService) UpdateChannel(ctx context.Context, updateChannelRequest *l
|
|||
|
||||
existingConfig := foundChannel.Config
|
||||
existingConfig.ForwardingFeeBaseMsat = updateChannelRequest.ForwardingFeeBaseMsat
|
||||
existingConfig.ForwardingFeeProportionalMillionths = updateChannelRequest.ForwardingFeeProportionalMillionths
|
||||
|
||||
if updateChannelRequest.MaxDustHtlcExposureFromFeeRateMultiplier > 0 {
|
||||
existingConfig.MaxDustHtlcExposure = ldk_node.MaxDustHtlcExposureFeeRateMultiplier{
|
||||
|
|
@ -1765,6 +1767,20 @@ func (ls *LDKService) handleLdkEvent(event *ldk_node.Event) {
|
|||
"total_fee_earned_msat": eventType.TotalFeeEarnedMsat,
|
||||
"outbound_amount_forwarded_msat": eventType.OutboundAmountForwardedMsat,
|
||||
}).Info("LDK Payment forwarded")
|
||||
if eventType.TotalFeeEarnedMsat == nil || eventType.OutboundAmountForwardedMsat == nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"earned_msat": eventType.TotalFeeEarnedMsat,
|
||||
"outbound_amount_forwarded_msat": eventType.OutboundAmountForwardedMsat,
|
||||
}).Error("forwarded payment has missing required fields")
|
||||
return
|
||||
}
|
||||
ls.eventPublisher.Publish(&events.Event{
|
||||
Event: "nwc_payment_forwarded",
|
||||
Properties: &lnclient.PaymentForwardedEventProperties{
|
||||
TotalFeeEarnedMsat: *eventType.TotalFeeEarnedMsat,
|
||||
OutboundAmountForwardedMsat: *eventType.OutboundAmountForwardedMsat,
|
||||
},
|
||||
})
|
||||
|
||||
case ldk_node.EventPaymentClaimable:
|
||||
if eventType.ClaimDeadline == nil {
|
||||
|
|
|
|||
|
|
@ -96,12 +96,45 @@ func NewLNDService(ctx context.Context, eventPublisher events.EventPublisher, ln
|
|||
go lndService.subscribeInvoices(lndCtx)
|
||||
go lndService.subscribeChannelEvents(lndCtx)
|
||||
go lndService.subscribeOpenHoldInvoices(lndCtx)
|
||||
go lndService.trackForwardedPayments(lndCtx)
|
||||
|
||||
logger.Logger.WithField("alias", nodeInfo.Alias).Info("Connected to LND")
|
||||
|
||||
return lndService, nil
|
||||
}
|
||||
|
||||
func (svc *LNDService) trackForwardedPayments(ctx context.Context) {
|
||||
// NOTE: this only tracks payments when hub is online and attached
|
||||
lastTime := time.Now()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
time.Sleep(1 * time.Minute)
|
||||
nextTime := time.Now()
|
||||
forwardedPayments, err := svc.client.ForwardingHistory(ctx, &lnrpc.ForwardingHistoryRequest{
|
||||
StartTime: uint64(lastTime.Unix()),
|
||||
EndTime: uint64(nextTime.Unix()),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("failed to read forwarding history")
|
||||
continue
|
||||
}
|
||||
for _, forwardingEvent := range forwardedPayments.ForwardingEvents {
|
||||
svc.eventPublisher.Publish(&events.Event{
|
||||
Event: "nwc_payment_forwarded",
|
||||
Properties: &lnclient.PaymentForwardedEventProperties{
|
||||
TotalFeeEarnedMsat: forwardingEvent.FeeMsat,
|
||||
OutboundAmountForwardedMsat: forwardingEvent.AmtOutMsat,
|
||||
},
|
||||
})
|
||||
}
|
||||
lastTime = nextTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *LNDService) subscribePayments(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
|
|
@ -867,7 +900,8 @@ func (svc *LNDService) ListChannels(ctx context.Context) ([]lnclient.Channel, er
|
|||
channelOpeningBlockHeight := lndChannel.ChanId >> 40
|
||||
confirmations := nodeInfo.BlockHeight - uint32(channelOpeningBlockHeight) + 1
|
||||
|
||||
var forwardingFee uint32
|
||||
var forwardingFeeBaseMsat uint32
|
||||
var forwardingFeeProportionalMillionths uint32
|
||||
if !lndChannel.Private {
|
||||
channelEdge, err := svc.client.GetChanInfo(ctx, &lnrpc.ChanInfoRequest{
|
||||
ChanId: lndChannel.ChanId,
|
||||
|
|
@ -883,7 +917,8 @@ func (svc *LNDService) ListChannels(ctx context.Context) ([]lnclient.Channel, er
|
|||
policy = channelEdge.Node2Policy
|
||||
}
|
||||
if policy != nil {
|
||||
forwardingFee = uint32(policy.FeeBaseMsat)
|
||||
forwardingFeeBaseMsat = uint32(policy.FeeBaseMsat)
|
||||
forwardingFeeProportionalMillionths = uint32(policy.FeeRateMilliMsat)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -903,7 +938,8 @@ func (svc *LNDService) ListChannels(ctx context.Context) ([]lnclient.Channel, er
|
|||
UnspendablePunishmentReserve: lndChannel.LocalConstraints.ChanReserveSat,
|
||||
CounterpartyUnspendablePunishmentReserve: lndChannel.RemoteConstraints.ChanReserveSat,
|
||||
IsOutbound: lndChannel.Initiator,
|
||||
ForwardingFeeBaseMsat: forwardingFee,
|
||||
ForwardingFeeBaseMsat: forwardingFeeBaseMsat,
|
||||
ForwardingFeeProportionalMillionths: forwardingFeeProportionalMillionths,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1101,7 +1137,7 @@ func (svc *LNDService) UpdateChannel(ctx context.Context, updateChannelRequest *
|
|||
ChanPoint: channelPoint,
|
||||
},
|
||||
BaseFeeMsat: int64(updateChannelRequest.ForwardingFeeBaseMsat),
|
||||
FeeRatePpm: uint32(nodePolicy.FeeRateMilliMsat),
|
||||
FeeRatePpm: updateChannelRequest.ForwardingFeeProportionalMillionths,
|
||||
TimeLockDelta: nodePolicy.TimeLockDelta,
|
||||
MaxHtlcMsat: nodePolicy.MaxHtlcMsat,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -243,3 +243,7 @@ func (wrapper *LNDWrapper) DisconnectPeer(ctx context.Context, req *lnrpc.Discon
|
|||
func (wrapper *LNDWrapper) SubscribeChannelEvents(ctx context.Context, in *lnrpc.ChannelEventSubscription, options ...grpc.CallOption) (lnrpc.Lightning_SubscribeChannelEventsClient, error) {
|
||||
return wrapper.client.SubscribeChannelEvents(ctx, in, options...)
|
||||
}
|
||||
|
||||
func (wrapper *LNDWrapper) ForwardingHistory(ctx context.Context, in *lnrpc.ForwardingHistoryRequest, options ...grpc.CallOption) (*lnrpc.ForwardingHistoryResponse, error) {
|
||||
return wrapper.client.ForwardingHistory(ctx, in, options...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ type Channel struct {
|
|||
Confirmations *uint32
|
||||
ConfirmationsRequired *uint32
|
||||
ForwardingFeeBaseMsat uint32
|
||||
ForwardingFeeProportionalMillionths uint32
|
||||
UnspendablePunishmentReserve uint64
|
||||
CounterpartyUnspendablePunishmentReserve uint64
|
||||
Error *string
|
||||
|
|
@ -148,6 +149,7 @@ type UpdateChannelRequest struct {
|
|||
ChannelId string `json:"channelId"`
|
||||
NodeId string `json:"nodeId"`
|
||||
ForwardingFeeBaseMsat uint32 `json:"forwardingFeeBaseMsat"`
|
||||
ForwardingFeeProportionalMillionths uint32 `json:"forwardingFeeProportionalMillionths"`
|
||||
MaxDustHtlcExposureFromFeeRateMultiplier uint64 `json:"maxDustHtlcExposureFromFeeRateMultiplier"`
|
||||
}
|
||||
|
||||
|
|
@ -214,6 +216,11 @@ type PaymentFailedEventProperties struct {
|
|||
Reason string
|
||||
}
|
||||
|
||||
type PaymentForwardedEventProperties struct {
|
||||
TotalFeeEarnedMsat uint64
|
||||
OutboundAmountForwardedMsat uint64
|
||||
}
|
||||
|
||||
type CustomNodeCommandArgDef struct {
|
||||
Name string
|
||||
Description string
|
||||
|
|
|
|||
38
service/payment_forwarded_consumer.go
Normal file
38
service/payment_forwarded_consumer.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/getAlby/hub/db"
|
||||
"github.com/getAlby/hub/events"
|
||||
"github.com/getAlby/hub/lnclient"
|
||||
"github.com/getAlby/hub/logger"
|
||||
)
|
||||
|
||||
type paymentForwardedConsumer struct {
|
||||
events.EventSubscriber
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// When a new app is created, subscribe to it on the relay
|
||||
func (c *paymentForwardedConsumer) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) {
|
||||
if event.Event != "nwc_payment_forwarded" {
|
||||
return
|
||||
}
|
||||
|
||||
properties, ok := event.Properties.(*lnclient.PaymentForwardedEventProperties)
|
||||
if !ok {
|
||||
logger.Logger.WithField("event", event).Error("Failed to cast event.Properties to payment forwarded event properties")
|
||||
return
|
||||
}
|
||||
forward := &db.Forward{
|
||||
OutboundAmountForwardedMsat: properties.OutboundAmountForwardedMsat,
|
||||
TotalFeeEarnedMsat: properties.TotalFeeEarnedMsat,
|
||||
}
|
||||
err := c.db.Create(forward).Error
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("failed to save forward to db")
|
||||
}
|
||||
}
|
||||
|
|
@ -137,6 +137,9 @@ func NewService(ctx context.Context) (*service, error) {
|
|||
eventPublisher.RegisterSubscriber(svc.transactionsService)
|
||||
eventPublisher.RegisterSubscriber(svc.nip47Service)
|
||||
eventPublisher.RegisterSubscriber(svc.albyOAuthSvc)
|
||||
eventPublisher.RegisterSubscriber(&paymentForwardedConsumer{
|
||||
db: gormDB,
|
||||
})
|
||||
|
||||
eventPublisher.Publish(&events.Event{
|
||||
Event: "nwc_started",
|
||||
|
|
|
|||
|
|
@ -1258,6 +1258,12 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string
|
|||
}
|
||||
return WailsRequestRouterResponse{Body: nil, Error: ""}
|
||||
}
|
||||
case "/api/forwards":
|
||||
forwards, err := app.api.GetForwards()
|
||||
if err != nil {
|
||||
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
|
||||
}
|
||||
return WailsRequestRouterResponse{Body: forwards, Error: ""}
|
||||
}
|
||||
|
||||
lightningAddressRegex := regexp.MustCompile(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue