Skip to content

Commit

Permalink
feat(daemon): Seed Lndhub
Browse files Browse the repository at this point in the history
Multi-account lndhub wallet support
  • Loading branch information
juligasa authored Nov 7, 2024
1 parent 1927be7 commit ba12fa0
Show file tree
Hide file tree
Showing 62 changed files with 6,219 additions and 10,044 deletions.
11 changes: 7 additions & 4 deletions backend/api/apis.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
documentsv3 "seed/backend/api/documents/v3alpha"
entities "seed/backend/api/entities/v1alpha"
networking "seed/backend/api/networking/v1alpha"
payments "seed/backend/api/payments/v1alpha"
"seed/backend/blob"
"seed/backend/core"
"seed/backend/logging"
Expand All @@ -27,8 +28,10 @@ type Server struct {
Activity *activity.Server
Syncing *syncing.Service
DocumentsV3 *documentsv3.Server
Payments *payments.Server
}

// Storage holds all the storing functionality.
type Storage interface {
DB() *sqlitex.Pool
KeyStore() core.KeyStore
Expand All @@ -38,24 +41,23 @@ type Storage interface {

// New creates a new API server.
func New(
ctx context.Context,
repo Storage,
idx *blob.Index,
node *mttnet.Node,
wallet daemon.Wallet,
sync *syncing.Service,
activity *activity.Server,
LogLevel string,
isMainnet bool,
) Server {
db := repo.DB()

return Server{
Activity: activity,
Daemon: daemon.NewServer(repo, wallet, &p2pNodeSubset{node: node, sync: sync}),
Daemon: daemon.NewServer(repo, &p2pNodeSubset{node: node, sync: sync}),
Networking: networking.NewServer(node, db, logging.New("seed/networking", LogLevel)),
Entities: entities.NewServer(idx, sync),
DocumentsV3: documentsv3.NewServer(repo.KeyStore(), idx, db, logging.New("seed/documents", LogLevel)),
Syncing: sync,
Payments: payments.NewServer(logging.New("seed/payments", LogLevel), db, node, repo.KeyStore(), isMainnet),
}
}

Expand All @@ -66,6 +68,7 @@ func (s Server) Register(srv *grpc.Server) {
s.Networking.RegisterServer(srv)
s.Entities.RegisterServer(srv)
s.DocumentsV3.RegisterServer(srv)
s.Payments.RegisterServer(srv)
}

type p2pNodeSubset struct {
Expand Down
21 changes: 1 addition & 20 deletions backend/api/daemon/v1alpha/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ package daemon

import (
context "context"
"fmt"
"seed/backend/core"
daemon "seed/backend/genproto/daemon/v1alpha"
sync "sync"
Expand All @@ -24,11 +23,6 @@ type Storage interface {
KeyStore() core.KeyStore
}

// Wallet is a subset of the wallet service used by this server.
type Wallet interface {
ConfigureSeedLNDHub(context.Context, core.KeyPair) error
}

// Node is a subset of the p2p node.
type Node interface {
ForceSync() error
Expand All @@ -40,19 +34,14 @@ type Node interface {
type Server struct {
store Storage
startTime time.Time
wallet Wallet

p2p Node

mu sync.Mutex // we only want one register request at a time.
}

// NewServer creates a new Server.
func NewServer(store Storage, w Wallet, n Node) *Server {
if w == nil {
panic("BUG: wallet is required")
}

func NewServer(store Storage, n Node) *Server {
if n == nil {
panic("BUG: p2p node is required")
}
Expand Down Expand Up @@ -166,14 +155,6 @@ func (srv *Server) RegisterAccount(ctx context.Context, name string, kp core.Key
return err
}

// TODO(hm24): we don't need to do this here since now we have the keys always accessible, unless the user
// chooses not to store the keys... Do this at the time of creating the seed wallet (new method not insert
// wallet which is an external wallet)
if srv.wallet != nil {
if err := srv.wallet.ConfigureSeedLNDHub(ctx, kp); err != nil {
return fmt.Errorf("failed to configure wallet when registering: %w", err)
}
}
return nil
}

Expand Down
8 changes: 1 addition & 7 deletions backend/api/daemon/v1alpha/daemon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,7 @@ func newTestServer(t *testing.T, name string) *Server {
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, store.Close()) })

return NewServer(store, &mockedWallet{}, &mockedP2PNode{})
}

type mockedWallet struct{}

func (w *mockedWallet) ConfigureSeedLNDHub(context.Context, core.KeyPair) error {
return nil
return NewServer(store, &mockedP2PNode{})
}

type mockedP2PNode struct{}
Expand Down
2 changes: 0 additions & 2 deletions backend/api/networking/v1alpha/networking_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,12 @@ func TestNetworkingGetPeerInfo(t *testing.T) {
ctx := context.Background()

pid := alice.Device.PeerID()
acc := alice.Account.Principal()

pinfo, err := api.GetPeerInfo(ctx, &networking.GetPeerInfoRequest{
DeviceId: pid.String(),
})
require.NoError(t, err)
require.NotNil(t, pinfo)
require.Equal(t, acc.String(), pinfo.AccountId, "account ids must match")
}

func makeTestServer(t *testing.T, u coretest.Tester) *Server {
Expand Down
235 changes: 235 additions & 0 deletions backend/api/payments/v1alpha/invoices.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
// Package payments handles lightning payments.
package payments

import (
"context"
"errors"
"fmt"
"seed/backend/core"
payments "seed/backend/genproto/payments/v1alpha"
"seed/backend/lndhub"
"seed/backend/lndhub/lndhubsql"
"seed/backend/wallet/walletsql"
"strings"

"google.golang.org/protobuf/types/known/emptypb"

"go.uber.org/zap"
)

// ListPaidInvoices returns the invoices that the wallet represented by walletID has paid.
func (srv *Server) ListPaidInvoices(ctx context.Context, in *payments.ListInvoicesRequest) (*payments.ListInvoicesResponse, error) {
conn, release, err := srv.pool.Conn(ctx)
if err != nil {
return nil, err
}
defer release()

w, err := walletsql.GetWallet(conn, in.Id)
if err != nil {
srv.log.Debug("couldn't list wallets: " + err.Error())
return nil, fmt.Errorf("couldn't list wallets")
}
if strings.ToLower(w.Type) != lndhubsql.LndhubWalletType && strings.ToLower(w.Type) != lndhubsql.LndhubGoWalletType {
err = fmt.Errorf("Couldn't get invoices form wallet type %s", w.Type)
srv.log.Debug(err.Error())
return nil, err
}
invoices, err := srv.lightningClient.Lndhub.ListPaidInvoices(ctx, in.Id)
if err != nil {
srv.log.Debug("couldn't list outgoing invoices: " + err.Error())
return nil, err
}

ret := &payments.ListInvoicesResponse{}
for _, invoice := range invoices {
ret.Invoices = append(ret.Invoices, &payments.Invoice{
PaymentHash: invoice.PaymentHash,
PaymentRequest: invoice.PaymentRequest,
Description: invoice.Description,
DescriptionHash: invoice.DescriptionHash,
PaymentPreimage: invoice.PaymentPreimage,
Destination: invoice.Destination,
Amount: invoice.Amount,
Fee: invoice.Fee,
Status: invoice.Status,
Type: invoice.Type,
ErrorMessage: invoice.ErrorMessage,
SettledAt: invoice.SettledAt,
ExpiresAt: invoice.ExpiresAt,
IsPaid: invoice.IsPaid,
Keysend: invoice.Keysend,
})
}
return ret, nil
}

// ListReceivednvoices returns the incoming invoices that the wallet represented by walletID has received.
func (srv *Server) ListReceivednvoices(ctx context.Context, in *payments.ListInvoicesRequest) (*payments.ListInvoicesResponse, error) {
conn, release, err := srv.pool.Conn(ctx)
if err != nil {
return nil, err
}
defer release()

w, err := walletsql.GetWallet(conn, in.Id)
if err != nil {
srv.log.Debug("couldn't list wallets: " + err.Error())
return nil, fmt.Errorf("couldn't list wallets: %w", err)
}
if strings.ToLower(w.Type) != lndhubsql.LndhubWalletType && strings.ToLower(w.Type) != lndhubsql.LndhubGoWalletType {
err = fmt.Errorf("Couldn't get invoices form wallet type %s", w.Type)
srv.log.Debug(err.Error())
return nil, err
}
invoices, err := srv.lightningClient.Lndhub.ListReceivedInvoices(ctx, in.Id)
if err != nil {
srv.log.Debug("couldn't list incoming invoices: " + err.Error())
return nil, err
}
ret := &payments.ListInvoicesResponse{}
for _, invoice := range invoices {
ret.Invoices = append(ret.Invoices, &payments.Invoice{
PaymentHash: invoice.PaymentHash,
PaymentRequest: invoice.PaymentRequest,
Description: invoice.Description,
DescriptionHash: invoice.DescriptionHash,
PaymentPreimage: invoice.PaymentPreimage,
Destination: invoice.Destination,
Amount: invoice.Amount,
Fee: invoice.Fee,
Status: invoice.Status,
Type: invoice.Type,
ErrorMessage: invoice.ErrorMessage,
SettledAt: invoice.SettledAt,
ExpiresAt: invoice.ExpiresAt,
IsPaid: invoice.IsPaid,
Keysend: invoice.Keysend,
})
}
return ret, nil
}

// RequestLud6Invoice asks a remote peer to issue an invoice. The remote user can be either a lnaddres or a Seed account ID
// First an lndhub invoice request is attempted. If it fails, then a P2P its used to transmit the invoice. In that case,
// Any of the devices associated with the accountID can issue the invoice. The memo field is optional and can be left nil.
func (srv *Server) RequestLud6Invoice(ctx context.Context, in *payments.RequestLud6InvoiceRequest) (*payments.Payreq, error) {
payReq := &payments.Payreq{}
var err error
payReq.Payreq, err = srv.lightningClient.Lndhub.RequestLud6Invoice(ctx, in.URL, in.User, in.Amount, in.Memo)
//err = fmt.Errorf("force p2p transmission")
if err != nil {
srv.log.Debug("couldn't get invoice via lndhub, trying p2p...", zap.Error(err))
account, err := core.DecodePrincipal(in.User)
if err != nil {
publicErr := fmt.Errorf("couldn't parse accountID string [%s], If using p2p transmission, User must be a valid accountID", in.User)
srv.log.Debug("error decoding cid "+publicErr.Error(), zap.Error(err))
return payReq, publicErr
}
payReq.Payreq, err = srv.P2PInvoiceRequest(ctx, account,
InvoiceRequest{
AmountSats: in.Amount,
Memo: in.Memo,
HoldInvoice: false, // TODO: Add support hold invoices
PreimageHash: []byte{}, // Only applicable to hold invoices
})
if err != nil {
srv.log.Debug("couldn't get invoice via p2p", zap.Error(err))
return payReq, fmt.Errorf("After trying to get the invoice locally Could not request invoice via P2P")
}
}

return payReq, nil
}

// CreateInvoice tries to generate an invoice locally.
func (srv *Server) CreateInvoice(ctx context.Context, in *payments.CreateInvoiceRequest) (*payments.Payreq, error) {
ret := &payments.Payreq{}
wallet, err := srv.GetWallet(ctx, &payments.WalletRequest{Id: in.Id})
if in.Id == "" && in.Account != "" {
wallet, err = srv.GetDefaultWallet(ctx, &payments.GetDefaultWalletRequest{Account: in.Account})
if err != nil {
return ret, fmt.Errorf("could not get default wallet to ask for a local invoice")
}
}
if err != nil {
return ret, fmt.Errorf("could not get default wallet to ask for a local invoice")
}
if wallet.Type != lndhubsql.LndhubWalletType && wallet.Type != lndhubsql.LndhubGoWalletType {
err = fmt.Errorf("Wallet type %s not compatible with local invoice creation", wallet.Type)
srv.log.Debug("couldn't create local invoice: " + err.Error())
return ret, err
}

ret.Payreq, err = srv.lightningClient.Lndhub.CreateLocalInvoice(ctx, wallet.Id, in.Amount, in.Memo)
if err != nil {
srv.log.Debug("couldn't create local invoice: " + err.Error())
return ret, err
}
return ret, nil
}

// PayInvoice tries to pay the provided invoice. If a walletID is provided, that wallet will be used instead of the default one
// If amountSats is provided, the invoice will be paid with that amount. This amount should be equal to the amount on the invoice
// unless the amount on the invoice is 0.
func (srv *Server) PayInvoice(ctx context.Context, in *payments.PayInvoiceRequest) (*emptypb.Empty, error) {
walletToPay := &payments.Wallet{}
var err error
var amountToPay int64

conn, release, err := srv.pool.Conn(ctx)
if err != nil {
return nil, err
}
defer release()

if in.Id != "" {
w, err := walletsql.GetWallet(conn, in.Id)
if err != nil {
publicErr := fmt.Errorf("couldn't get wallet %s", in.Id)
srv.log.Debug("Could not get wallet", zap.Error(publicErr))
return nil, publicErr
}
walletToPay.Account = w.Account
walletToPay.Address = w.Address
walletToPay.Id = w.ID
walletToPay.Name = w.Name
walletToPay.Type = w.Type
} else {
walletToPay, err = srv.GetDefaultWallet(ctx, &payments.GetDefaultWalletRequest{Account: in.Account})
if err != nil {
return nil, fmt.Errorf("couldn't get default wallet to pay")
}
}

if !isSupported(walletToPay.Type) {
err = fmt.Errorf("wallet type [%s] not supported to pay. Currently supported: [%v]", walletToPay.Type, supportedWallets)
srv.log.Debug(err.Error())
return nil, err
}

if in.Amount == 0 {
invoice, err := lndhub.DecodeInvoice(in.Payreq)
if err != nil {
publicError := fmt.Errorf("couldn't decode invoice [%s], please make sure it is a bolt-11 compatible invoice", in.Payreq)
srv.log.Debug(publicError.Error(), zap.String("msg", err.Error()))
return nil, publicError
}
amountToPay = int64(invoice.MilliSat.ToSatoshis())
} else {
amountToPay = in.Amount
}

if err = srv.lightningClient.Lndhub.PayInvoice(ctx, walletToPay.Id, in.Payreq, amountToPay); err != nil {
if strings.Contains(err.Error(), walletsql.NotEnoughBalance) {
return nil, fmt.Errorf("couldn't pay invoice with wallet [%s]: %w", walletToPay.Name, lndhubsql.ErrNotEnoughBalance)
}
if errors.Is(err, lndhubsql.ErrQtyMissmatch) {
return nil, fmt.Errorf("couldn't pay invoice, quantity in invoice differs from amount to pay [%d] :%w", amountToPay, lndhubsql.ErrQtyMissmatch)
}
srv.log.Debug("couldn't pay invoice", zap.String("msg", err.Error()))
return nil, fmt.Errorf("couldn't pay invoice")
}

return nil, nil
}
Loading

0 comments on commit ba12fa0

Please sign in to comment.