| name | litd-grpc |
| description | Reference for litd (Lightning Terminal) gRPC API in Go: managing accounts, baking macaroons, listing payments, and creating LNC sessions. |
litd gRPC โ Accounts & LNC Sessions
Reference for interacting with litd (Lightning Terminal daemon) gRPC API: managing accounts and LNC sessions, baking account-scoped macaroons, and listing account payments.
Connection Setup
litd exposes a single TLS gRPC endpoint (default localhost:8443). Authentication uses macaroon hex in the macaroon request metadata header.
Bootstrap connection (supermacaroon)
import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"github.com/lightninglabs/lightning-terminal/litrpc"
"github.com/lightningnetwork/lnd/lnrpc"
)
tlsCreds, _ := credentials.NewClientTLSFromFile("~/.lit/tls.cert", "")
creds := &macaroonCredentials{hex: hex.EncodeToString(macBytes)}
conn, _ := grpc.NewClient(
"localhost:8443",
grpc.WithTransportCredentials(tlsCreds),
grpc.WithPerRPCCredentials(creds),
)
proxy := litrpc.NewProxyClient(conn)
resp, _ := proxy.BakeSuperMacaroon(ctx, &litrpc.BakeSuperMacaroonRequest{})
creds.update(resp.Macaroon)
lightning := lnrpc.NewLightningClient(conn)
accounts := litrpc.NewAccountsClient(conn)
sessions := litrpc.NewSessionsClient(conn)
macaroonCredentials helper (thread-safe, swappable)
type macaroonCredentials struct {
mu sync.RWMutex
hex string
}
func (m *macaroonCredentials) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) {
m.mu.RLock(); defer m.mu.RUnlock()
return map[string]string{"macaroon": m.hex}, nil
}
func (m *macaroonCredentials) RequireTransportSecurity() bool { return true }
func (m *macaroonCredentials) update(h string) { m.mu.Lock(); m.hex = h; m.mu.Unlock() }
Account Management
All account RPCs go through litrpc.AccountsClient.
List all accounts
resp, err := accounts.ListAccounts(ctx, &litrpc.ListAccountsRequest{})
Get single account
resp, err := accounts.AccountInfo(ctx, &litrpc.AccountInfoRequest{Id: accountID})
Create account
resp, err := accounts.CreateAccount(ctx, &litrpc.CreateAccountRequest{
AccountBalance: 100_000,
Label: "My Budget",
ExpirationDate: 0,
})
Update account (balance, expiry, label)
UpdateAccountRequest uses sentinel -1 for AccountBalance to mean "do not change balance".
Always pass the current ExpirationDate when only changing the label (protobuf 0 = unset = may reset expiry).
resp, err := accounts.CreditAccount(ctx, &litrpc.CreditAccountRequest{
Account: &litrpc.AccountIdentifier{
Identifier: &litrpc.AccountIdentifier_Id{Id: accountID},
},
Amount: 50_000,
})
resp, err := accounts.DebitAccount(ctx, &litrpc.DebitAccountRequest{
Account: &litrpc.AccountIdentifier{
Identifier: &litrpc.AccountIdentifier_Id{Id: accountID},
},
Amount: 10_000,
})
resp, err := accounts.UpdateAccount(ctx, &litrpc.UpdateAccountRequest{
Id: accountID,
AccountBalance: -1,
ExpirationDate: newExpiry,
})
resp, err := accounts.UpdateAccount(ctx, &litrpc.UpdateAccountRequest{
Id: accountID,
AccountBalance: -1,
ExpirationDate: currentAccount.ExpirationDate,
Label: "New Label",
})
Remove account
_, err := accounts.RemoveAccount(ctx, &litrpc.RemoveAccountRequest{Id: accountID})
Macaroon Baking (Account-Scoped)
Macaroons are baked via lnrpc.LightningClient.BakeMacaroon, then a first-party caveat ties them to an account:
resp, _ := lightning.BakeMacaroon(ctx, &lnrpc.BakeMacaroonRequest{
Permissions: []*lnrpc.MacaroonPermission{
{Entity: "info", Action: "read"},
{Entity: "invoices", Action: "read"},
{Entity: "offchain", Action: "read"},
{Entity: "onchain", Action: "read"},
},
AllowExternalPermissions: true,
})
macBytes, _ := hex.DecodeString(resp.Macaroon)
mac, _ := macaroon.New(nil, nil, "", macaroon.LatestVersion)
mac.UnmarshalBinary(macBytes)
mac.AddFirstPartyCaveat([]byte("lnd-custom account " + accountID))
constrained, _ := mac.MarshalBinary()
accountMacHex := hex.EncodeToString(constrained)
Predefined permission sets:
| Type | Permissions |
|---|
| Account (full) | info:read, invoices:r/w, offchain:r/w, onchain:read, address:r/w |
| Readonly | info:read, invoices:read, offchain:read, onchain:read |
| Invoice | invoices:r/w, address:r/w |
Account-Scoped Payment Listing
Critical: Do NOT use grpc.PerRPCCredentials(...) as a per-call option on an existing connection โ litd will reject the request with "expected 1 macaroon, got 2" because both the connection-level supermacaroon AND the per-call credential are sent.
Correct pattern: Open a dedicated short-lived connection with only the account macaroon:
func ListAccountPayments(ctx context.Context, accountMacHex, rpcServer string, tlsCreds credentials.TransportCredentials) ([]*lnrpc.Payment, []*lnrpc.Invoice, error) {
creds := &macaroonCredentials{hex: accountMacHex}
conn, err := grpc.NewClient(
rpcServer,
grpc.WithTransportCredentials(tlsCreds),
grpc.WithPerRPCCredentials(creds),
)
if err != nil {
return nil, nil, err
}
defer conn.Close()
lightning := lnrpc.NewLightningClient(conn)
payResp, err := lightning.ListPayments(ctx, &lnrpc.ListPaymentsRequest{
Reversed: true,
IncludeIncomplete: true,
})
invResp, err := lightning.ListInvoices(ctx, &lnrpc.ListInvoiceRequest{
Reversed: true,
})
return payResp.Payments, invResp.Invoices, nil
}
The litd middleware intercepts these calls, validates the lnd-custom account <id> caveat, and returns only the payments/invoices belonging to that account.
Payment fields of interest
p.PaymentHash
p.ValueSat
p.FeeSat
p.CreationTimeNs
p.Status
p.PaymentRequest
p.PaymentPreimage
p.FailureReason
inv.RHash
inv.Value
inv.AmtPaidSat
inv.CreationDate
inv.SettleDate
inv.State
inv.Memo
inv.PaymentRequest
Decoding bolt11 memo (zpay32)
import (
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightningnetwork/lnd/zpay32"
)
func decodeMemo(payReq, network string) string {
if payReq == "" { return "" }
params := &chaincfg.MainNetParams
inv, err := zpay32.Decode(payReq, params)
if err != nil || inv.Description == nil { return "" }
return *inv.Description
}
LNC Session Management
All session RPCs go through litrpc.SessionsClient.
List sessions
resp, err := sessions.ListSessions(ctx, &litrpc.ListSessionsRequest{})
s.Label
s.LocalPublicKey
s.RemotePublicKey
s.SessionType
s.SessionState
s.ExpiryTimestampSeconds
s.PairingSecretMnemonic
s.AccountId
Session types
litrpc.SessionType_TYPE_MACAROON_READONLY
litrpc.SessionType_TYPE_MACAROON_ADMIN
litrpc.SessionType_TYPE_MACAROON_CUSTOM
litrpc.SessionType_TYPE_MACAROON_ACCOUNT
litrpc.SessionType_TYPE_AUTOPILOT
Create a general session (Admin / Readonly / Custom)
req := &litrpc.AddSessionRequest{
Label: "My Wallet",
SessionType: litrpc.SessionType_TYPE_MACAROON_READONLY,
MailboxServerAddr: "mailbox.terminal.lightning.today:443",
ExpiryTimestampSeconds: uint64(time.Now().Add(365 * 24 * time.Hour).Unix()),
MacaroonCustomPermissions: []*litrpc.MacaroonPermission{
{Entity: "offchain", Action: "read"},
{Entity: "invoices", Action: "write"},
},
}
resp, err := sessions.AddSession(ctx, req)
Invoice session (predefined custom permissions):
invoicePerms := []*litrpc.MacaroonPermission{
{Entity: "address", Action: "read"},
{Entity: "address", Action: "write"},
{Entity: "invoices", Action: "read"},
{Entity: "invoices", Action: "write"},
{Entity: "onchain", Action: "read"},
}
req := &litrpc.AddSessionRequest{
Label: "Invoice Session",
SessionType: litrpc.SessionType_TYPE_MACAROON_CUSTOM,
MailboxServerAddr: "mailbox.terminal.lightning.today:443",
ExpiryTimestampSeconds: expiryUnix,
MacaroonCustomPermissions: invoicePerms,
}
Create an account-tied session
resp, err := sessions.AddSession(ctx, &litrpc.AddSessionRequest{
Label: "Alice's Wallet",
SessionType: litrpc.SessionType_TYPE_MACAROON_ACCOUNT,
AccountId: accountID,
MailboxServerAddr: "mailbox.terminal.lightning.today:443",
ExpiryTimestampSeconds: expiryUnix,
})
Revoke session
_, err := sessions.RevokeSession(ctx, &litrpc.RevokeSessionRequest{
LocalPublicKey: session.LocalPublicKey,
})
Check session state
connected := len(session.RemotePublicKey) > 0
active := session.SessionState == litrpc.SessionState_STATE_CREATED ||
session.SessionState == litrpc.SessionState_STATE_IN_USE
Parsing Custom Permissions
Parse a comma-separated "entity:action" string into []*litrpc.MacaroonPermission:
func ParsePermissions(s string) ([]*litrpc.MacaroonPermission, error) {
var perms []*litrpc.MacaroonPermission
for _, part := range strings.Split(s, ",") {
part = strings.TrimSpace(part)
if part == "" { continue }
kv := strings.SplitN(part, ":", 2)
if len(kv) != 2 || kv[0] == "" || kv[1] == "" {
return nil, fmt.Errorf("invalid permission %q: want entity:action", part)
}
perms = append(perms, &litrpc.MacaroonPermission{
Entity: strings.TrimSpace(kv[0]),
Action: strings.TrimSpace(kv[1]),
})
}
if len(perms) == 0 {
return nil, fmt.Errorf("no permissions provided")
}
return perms, nil
}
go.mod Requirements
require (
github.com/lightninglabs/lightning-terminal/litrpc v1.0.2
github.com/lightningnetwork/lnd v0.20.1-beta
github.com/btcsuite/btcd v0.24.x // for chaincfg (bolt11 decoding)
gopkg.in/macaroon.v2 v2.x // for adding caveats
google.golang.org/grpc vX.Y.Z
)
// Critical โ lnd and litrpc use a protobuf fork:
replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display
Common Pitfalls
| Pitfall | Fix |
|---|
"expected 1 macaroon, got 2" | Never add a per-call grpc.PerRPCCredentials option to a connection that already has WithPerRPCCredentials. Open a new dedicated connection instead. |
| Label update clears expiry | UpdateAccountRequest.ExpirationDate = 0 may reset expiry. Always pass the account's current ExpirationDate when updating other fields. |
AccountBalance: 0 in UpdateAccount | Use -1 to signal "do not change balance". 0 may zero out the balance. |
| bolt11 memo empty | The Memo field on lnrpc.Invoice may be populated directly, or the description may only be in the bolt11 PaymentRequest. Try both. |
| Session pairing phrase visibility | Only show PairingSecretMnemonic when len(session.RemotePublicKey) == 0. Once a wallet has connected the phrase is no longer needed and exposing it is misleading. |
CreationTimeNs vs CreationDate | lnrpc.Payment.CreationTimeNs is nanoseconds; lnrpc.Invoice.CreationDate / SettleDate are seconds. |