feat: rebalance through node pubkey (#1731)

* feat: rebalance through node pubkey (WIP)

* fix: check if remote channel policy exists
This commit is contained in:
Roland 2025-09-19 12:30:51 +07:00 committed by GitHub
parent 3a4d04bc20
commit a294e1b169
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 101 additions and 30 deletions

View file

@ -26,7 +26,7 @@ func (api *api) RebalanceChannel(ctx context.Context, rebalanceChannelRequest *R
"receive_through": rebalanceChannelRequest.ReceiveThroughNodePubkey,
}
receiveInvoice, err := api.svc.GetTransactionsService().MakeInvoice(ctx, rebalanceChannelRequest.AmountSat*1000, "Alby Hub Rebalance through "+rebalanceChannelRequest.ReceiveThroughNodePubkey, "", 0, receiveMetadata, api.svc.GetLNClient(), nil, nil)
receiveInvoice, err := api.svc.GetTransactionsService().MakeInvoice(ctx, rebalanceChannelRequest.AmountSat*1000, "Alby Hub Rebalance through "+rebalanceChannelRequest.ReceiveThroughNodePubkey, "", 0, receiveMetadata, api.svc.GetLNClient(), nil, nil, &rebalanceChannelRequest.ReceiveThroughNodePubkey)
if err != nil {
logger.Logger.WithError(err).Error("failed to generate rebalance receive invoice")
return nil, err

View file

@ -16,7 +16,7 @@ func (api *api) CreateInvoice(ctx context.Context, amount uint64, description st
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")
}
transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amount, description, "", 0, nil, api.svc.GetLNClient(), nil, nil)
transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amount, description, "", 0, nil, api.svc.GetLNClient(), nil, nil, nil)
if err != nil {
return nil, err
}
@ -144,7 +144,7 @@ func (api *api) Transfer(ctx context.Context, fromAppId *uint, toAppId *uint, am
}
}
transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amountMsat, "transfer", "", 0, nil, api.svc.GetLNClient(), toAppId, nil)
transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amountMsat, "transfer", "", 0, nil, api.svc.GetLNClient(), toAppId, nil, nil)
if err != nil {
return err

View file

@ -105,7 +105,7 @@ func (cs *CashuService) SendKeysend(amount uint64, destination string, custom_re
return nil, errors.New("keysend not supported")
}
func (cs *CashuService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *lnclient.Transaction, err error) {
func (cs *CashuService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, throughNodePubkey *string) (transaction *lnclient.Transaction, err error) {
// TODO: support expiry
if expiry == 0 {
expiry = lnclient.DEFAULT_INVOICE_EXPIRY

View file

@ -717,7 +717,7 @@ func (ls *LDKService) getMaxSpendable() uint64 {
return spendable
}
func (ls *LDKService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *lnclient.Transaction, err error) {
func (ls *LDKService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, throughNodePubkey *string) (transaction *lnclient.Transaction, err error) {
if time.Duration(expiry)*time.Second > maxInvoiceExpiry {
return nil, errors.New("expiry is too long")

View file

@ -599,7 +599,7 @@ func (svc *LNDService) getPaymentResult(stream routerrpc.Router_SendPaymentV2Cli
}
}
func (svc *LNDService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *lnclient.Transaction, err error) {
func (svc *LNDService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, throughNodePubkey *string) (transaction *lnclient.Transaction, err error) {
var descriptionHashBytes []byte
if descriptionHash != "" {
@ -628,6 +628,75 @@ func (svc *LNDService) MakeInvoice(ctx context.Context, amount int64, descriptio
for _, channel := range channels {
if channel.Active && channel.Public {
hasPublicChannels = true
break
}
}
var hints []*lnrpc.RouteHint
if !hasPublicChannels && throughNodePubkey != nil {
channelsRes, err := svc.client.ListChannels(ctx, &lnrpc.ListChannelsRequest{
PrivateOnly: true,
})
if err != nil {
return nil, err
}
for _, channel := range channelsRes.Channels {
if channel.RemotePubkey != *throughNodePubkey {
continue
}
chanInfo, err := svc.client.GetChanInfo(ctx, &lnrpc.ChanInfoRequest{
ChanId: channel.ChanId,
})
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"channel_id": channel.ChanId,
}).WithError(err).Error("Unable to get channel info")
continue
}
var remotePolicy *lnrpc.RoutingPolicy
if chanInfo.Node1Pub == channel.RemotePubkey {
remotePolicy = chanInfo.Node1Policy
} else {
remotePolicy = chanInfo.Node2Policy
}
if remotePolicy == nil {
logger.Logger.WithFields(logrus.Fields{
"channel_id": channel.ChanId,
}).WithError(err).Error("Remote channel policy does not exist")
continue
}
channelId := chanInfo.ChannelId
if channel.PeerScidAlias != 0 {
channelId = channel.PeerScidAlias
}
hint := &lnrpc.RouteHint{
HopHints: []*lnrpc.HopHint{
{
NodeId: channel.RemotePubkey,
ChanId: channelId,
FeeBaseMsat: uint32(remotePolicy.FeeBaseMsat),
FeeProportionalMillionths: uint32(remotePolicy.FeeRateMilliMsat),
CltvExpiryDelta: remotePolicy.TimeLockDelta,
},
},
}
hints = append(hints, hint)
if len(hints) == 3 {
// limit to 3 channels
// NOTE: there is no check that the channels are online or have enough receiving capacity.
break
}
}
if len(hints) == 0 {
return nil, errors.New("no channel found for given throughNodePubkey")
}
}
@ -636,6 +705,7 @@ func (svc *LNDService) MakeInvoice(ctx context.Context, amount int64, descriptio
Memo: description,
DescriptionHash: descriptionHashBytes,
Expiry: expiry,
RouteHints: hints,
Private: !hasPublicChannels, // use private channel hints in the invoice
}

View file

@ -61,7 +61,7 @@ type LNClient interface {
SendKeysend(amount uint64, destination string, customRecords []TLVRecord, preimage string) (*PayKeysendResponse, error)
GetPubkey() string
GetInfo(ctx context.Context) (info *NodeInfo, err error)
MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *Transaction, err error)
MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, throughNodePubkey *string) (transaction *Transaction, err error)
MakeHoldInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, paymentHash string) (transaction *Transaction, err error)
SettleHoldInvoice(ctx context.Context, preimage string) (err error)
CancelHoldInvoice(ctx context.Context, paymentHash string) (err error)

View file

@ -276,7 +276,7 @@ func (svc *PhoenixService) ListChannels(ctx context.Context) ([]lnclient.Channel
return channels, nil
}
func (svc *PhoenixService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *lnclient.Transaction, err error) {
func (svc *PhoenixService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, throughNodePubkey *string) (transaction *lnclient.Transaction, err error) {
// TODO: support expiry
if expiry == 0 {
expiry = lnclient.DEFAULT_INVOICE_EXPIRY

View file

@ -41,7 +41,7 @@ func (controller *nip47Controller) HandleMakeInvoiceEvent(ctx context.Context, n
expiry := makeInvoiceParams.Expiry
transaction, err := controller.transactionsService.MakeInvoice(ctx, makeInvoiceParams.Amount, makeInvoiceParams.Description, makeInvoiceParams.DescriptionHash, expiry, makeInvoiceParams.Metadata, controller.lnClient, &appId, &requestEventId)
transaction, err := controller.transactionsService.MakeInvoice(ctx, makeInvoiceParams.Amount, makeInvoiceParams.Description, makeInvoiceParams.DescriptionHash, expiry, makeInvoiceParams.Metadata, controller.lnClient, &appId, &requestEventId, nil)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"request_event_id": requestEventId,

View file

@ -400,7 +400,7 @@ func (svc *swapsService) SwapOut(amount uint64, destination string, useExactRece
func (svc *swapsService) SwapIn(amount uint64, autoSwap bool) (*SwapResponse, error) {
amountMSat := amount * 1000
invoice, err := svc.transactionsService.MakeInvoice(svc.ctx, amountMSat, "On-chain to lightning swap", "", 0, nil, svc.lnClient, nil, nil)
invoice, err := svc.transactionsService.MakeInvoice(svc.ctx, amountMSat, "On-chain to lightning swap", "", 0, nil, svc.lnClient, nil, nil, nil)
if err != nil {
return nil, err
}

View file

@ -116,7 +116,7 @@ func (mln *MockLn) GetInfo(ctx context.Context) (info *lnclient.NodeInfo, err er
return &MockNodeInfo, nil
}
func (mln *MockLn) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *lnclient.Transaction, err error) {
func (mln *MockLn) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, throughNodePubkey *string) (transaction *lnclient.Transaction, err error) {
return MockLNClientTransaction, nil
}

View file

@ -1323,8 +1323,8 @@ func (_c *MockLNClient_MakeHoldInvoice_Call) RunAndReturn(run func(ctx context.C
}
// MakeInvoice provides a mock function for the type MockLNClient
func (_mock *MockLNClient) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (*lnclient.Transaction, error) {
ret := _mock.Called(ctx, amount, description, descriptionHash, expiry)
func (_mock *MockLNClient) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, throughNodePubkey *string) (*lnclient.Transaction, error) {
ret := _mock.Called(ctx, amount, description, descriptionHash, expiry, throughNodePubkey)
if len(ret) == 0 {
panic("no return value specified for MakeInvoice")
@ -1332,18 +1332,18 @@ func (_mock *MockLNClient) MakeInvoice(ctx context.Context, amount int64, descri
var r0 *lnclient.Transaction
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, int64, string, string, int64) (*lnclient.Transaction, error)); ok {
return returnFunc(ctx, amount, description, descriptionHash, expiry)
if returnFunc, ok := ret.Get(0).(func(context.Context, int64, string, string, int64, *string) (*lnclient.Transaction, error)); ok {
return returnFunc(ctx, amount, description, descriptionHash, expiry, throughNodePubkey)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, int64, string, string, int64) *lnclient.Transaction); ok {
r0 = returnFunc(ctx, amount, description, descriptionHash, expiry)
if returnFunc, ok := ret.Get(0).(func(context.Context, int64, string, string, int64, *string) *lnclient.Transaction); ok {
r0 = returnFunc(ctx, amount, description, descriptionHash, expiry, throughNodePubkey)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*lnclient.Transaction)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, int64, string, string, int64) error); ok {
r1 = returnFunc(ctx, amount, description, descriptionHash, expiry)
if returnFunc, ok := ret.Get(1).(func(context.Context, int64, string, string, int64, *string) error); ok {
r1 = returnFunc(ctx, amount, description, descriptionHash, expiry, throughNodePubkey)
} else {
r1 = ret.Error(1)
}
@ -1361,13 +1361,14 @@ type MockLNClient_MakeInvoice_Call struct {
// - description
// - descriptionHash
// - expiry
func (_e *MockLNClient_Expecter) MakeInvoice(ctx interface{}, amount interface{}, description interface{}, descriptionHash interface{}, expiry interface{}) *MockLNClient_MakeInvoice_Call {
return &MockLNClient_MakeInvoice_Call{Call: _e.mock.On("MakeInvoice", ctx, amount, description, descriptionHash, expiry)}
// - throughNodePubkey
func (_e *MockLNClient_Expecter) MakeInvoice(ctx interface{}, amount interface{}, description interface{}, descriptionHash interface{}, expiry interface{}, throughNodePubkey interface{}) *MockLNClient_MakeInvoice_Call {
return &MockLNClient_MakeInvoice_Call{Call: _e.mock.On("MakeInvoice", ctx, amount, description, descriptionHash, expiry, throughNodePubkey)}
}
func (_c *MockLNClient_MakeInvoice_Call) Run(run func(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64)) *MockLNClient_MakeInvoice_Call {
func (_c *MockLNClient_MakeInvoice_Call) Run(run func(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, throughNodePubkey *string)) *MockLNClient_MakeInvoice_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(int64), args[2].(string), args[3].(string), args[4].(int64))
run(args[0].(context.Context), args[1].(int64), args[2].(string), args[3].(string), args[4].(int64), args[5].(*string))
})
return _c
}
@ -1377,7 +1378,7 @@ func (_c *MockLNClient_MakeInvoice_Call) Return(transaction *lnclient.Transactio
return _c
}
func (_c *MockLNClient_MakeInvoice_Call) RunAndReturn(run func(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (*lnclient.Transaction, error)) *MockLNClient_MakeInvoice_Call {
func (_c *MockLNClient_MakeInvoice_Call) RunAndReturn(run func(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, throughNodePubkey *string) (*lnclient.Transaction, error)) *MockLNClient_MakeInvoice_Call {
_c.Call.Return(run)
return _c
}

View file

@ -26,7 +26,7 @@ func TestMakeInvoice_NoApp(t *testing.T) {
txMetadata["randomkey"] = strings.Repeat("a", constants.INVOICE_METADATA_MAX_LENGTH-16) // json encoding adds 16 characters - {"randomkey":""}
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.MakeInvoice(ctx, 1234, "Hello world", "", 0, txMetadata, svc.LNClient, nil, nil)
transaction, err := transactionsService.MakeInvoice(ctx, 1234, "Hello world", "", 0, txMetadata, svc.LNClient, nil, nil, nil)
assert.NoError(t, err)
var metadata map[string]interface{}
@ -50,7 +50,7 @@ func TestMakeInvoice_MetadataTooLarge(t *testing.T) {
metadata["randomkey"] = strings.Repeat("a", constants.INVOICE_METADATA_MAX_LENGTH-15) // json encoding adds 16 characters
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.MakeInvoice(ctx, 1234, "Hello world", "", 0, metadata, svc.LNClient, nil, nil)
transaction, err := transactionsService.MakeInvoice(ctx, 1234, "Hello world", "", 0, metadata, svc.LNClient, nil, nil, nil)
assert.Error(t, err)
assert.Equal(t, fmt.Sprintf("encoded invoice metadata provided is too large. Limit: %d Received: %d", constants.INVOICE_METADATA_MAX_LENGTH, constants.INVOICE_METADATA_MAX_LENGTH+1), err.Error())
@ -72,7 +72,7 @@ func TestMakeInvoice_App(t *testing.T) {
assert.NoError(t, err)
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.MakeInvoice(ctx, 1234, "Hello world", "", 0, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
transaction, err := transactionsService.MakeInvoice(ctx, 1234, "Hello world", "", 0, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID, nil)
assert.NoError(t, err)
assert.Equal(t, uint64(tests.MockLNClientTransaction.Amount), transaction.AmountMsat)

View file

@ -35,7 +35,7 @@ type transactionsService struct {
type TransactionsService interface {
events.EventSubscriber
MakeInvoice(ctx context.Context, amount uint64, description string, descriptionHash string, expiry uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error)
MakeInvoice(ctx context.Context, amount 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)
SendPaymentSync(payReq string, amountMsat *uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error)
@ -139,7 +139,7 @@ func NewTransactionsService(db *gorm.DB, eventPublisher events.EventPublisher) *
}
}
func (svc *transactionsService) MakeInvoice(ctx context.Context, amount uint64, description string, descriptionHash string, expiry uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) {
func (svc *transactionsService) MakeInvoice(ctx context.Context, amount uint64, description string, descriptionHash string, expiry uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint, throughNodePubkey *string) (*Transaction, error) {
logger.Logger.WithFields(logrus.Fields{
"app_id": appId,
"request_event_id": requestEventId,
@ -173,7 +173,7 @@ func (svc *transactionsService) MakeInvoice(ctx context.Context, amount uint64,
appId = &overwriteAppId
}
lnClientTransaction, err := lnClient.MakeInvoice(ctx, int64(amount), description, descriptionHash, int64(expiry))
lnClientTransaction, err := lnClient.MakeInvoice(ctx, int64(amount), description, descriptionHash, int64(expiry), throughNodePubkey)
if err != nil {
logger.Logger.WithError(err).Error("Failed to create transaction")
return nil, err