feat: greenlight LNClient WIP

This commit is contained in:
Roland Bewick 2024-02-09 15:05:11 +07:00
parent 444953e89d
commit 096039cf4d
10 changed files with 361 additions and 28 deletions

View file

@ -18,6 +18,7 @@ Ideally the app runs 24/7 (on a node, VPS or always-online desktop/laptop machin
- LND (see: lnd.go)
- Breez (see: breez.go)
- Greenlight (see: greenlight.go)
- want more? please open an issue.
## Installation
@ -32,10 +33,16 @@ As data storage SQLite is used.
# edit the config for your needs
vim .env
To get a new random Nostr key use `openssl rand -hex 32` or similar.
#### Optional Requirements
See [Greenlight](./README_GREENLIGHT.md)
## Development
### Required Software
- Go
### Server (HTTP mode)
1. Create a Lightning Polar setup with two LND nodes and uncomment the Polar LND section in your `.env` file.
@ -94,7 +101,7 @@ Breez SDK requires gcc to build the Breez bindings. Run `choco install mingw` an
- `COOKIE_SECRET`: a randomly generated secret string. (only needed in http mode)
- `DATABASE_URI`: a sqlite filename. Default: .data/nwc.db
- `PORT`: the port on which the app should listen on (default: 8080)
- `LN_BACKEND_TYPE`: LND or BREEZ
- `LN_BACKEND_TYPE`: LND, BREEZ or GREENLIGHT
- `WORK_DIR`: directory to store NWC data files. Default: .data
-
@ -106,12 +113,6 @@ _For cert and macaroon, either hex or file options can be used._
- `LND_CERT_FILE`: the location where LND's `tls.cert` file can be found (used with the LND backend)
- `LND_MACAROON_FILE`: the location where LND's `admin.macaroon` file can be found (used with the LND backend)
### BREEZ Backend parameters
- `BREEZ_MNEMONIC`: your bip39 mnemonic key phrase e.g. "define limit soccer guilt trim mechanic beyond outside best give south shine"
- `BREEZ_API_KEY`: contact breez for more info
- `GREENLIGHT_INVITE_CODE`: contact blockstream for more info
## Application deeplink options
### `/apps/new` deeplink options

21
README_GREENLIGHT.md Normal file
View file

@ -0,0 +1,21 @@
# Greenlight
To enable the GREENLIGHT LNClient some additional steps are required.
## Required Software
- Python 3 + pip
- [Greenlight Client](https://github.com/Blockstream/greenlight/tree/main?tab=readme-ov-file#install-and-updating-glcli-and-python-api) with [fix commit](https://github.com/Blockstream/greenlight/commit/2dc5a94668d41baef7275dae860c09b4a5dba198)
## Setup
1. Wallet
1. Existing mnemonic (possible through NWC UI)
2. New mnemonic: (WIP)
1. glcli scheduler register --network=bitcoin --invite=YOUR_INVITE_CODE
2. glcli getinfo - "warning_lightningd_sync" should disappear after < 30 seconds
2. Get Liquidity
1. peer with blocktank (TBC): `glcli connect 0296b2db342fcf87ea94d981757fdf4d3e545bd5cef4919f58b5d38dfdd73bf5c9 130.211.95.29 9735`
2. run `glcli scheduler schedule` to get your node ID and GRPC uri
3. go to blocktank and pay for a channel. After paying invoice, claim manually. Format is without the https: `pubkey@domain_name:9735`
4. Unless you paid for some outgoing liquidity, you'll need to deposit and reserve at least 1% of the channel balance to send outgoing payments.

4
api.go
View file

@ -231,8 +231,8 @@ func (api *API) Setup(setupRequest *models.SetupRequest) error {
if setupRequest.BreezAPIKey != "" {
api.svc.cfg.SetUpdate("BreezAPIKey", setupRequest.BreezAPIKey, setupRequest.UnlockPassword)
}
if setupRequest.BreezMnemonic != "" {
api.svc.cfg.SetUpdate("BreezMnemonic", setupRequest.BreezMnemonic, setupRequest.UnlockPassword)
if setupRequest.Mnemonic != "" {
api.svc.cfg.SetUpdate("Mnemonic", setupRequest.Mnemonic, setupRequest.UnlockPassword)
}
if setupRequest.GreenlightInviteCode != "" {
api.svc.cfg.SetUpdate("GreenlightInviteCode", setupRequest.GreenlightInviteCode, setupRequest.UnlockPassword)

View file

@ -12,11 +12,12 @@ import (
)
const (
LNDBackendType = "LND"
BreezBackendType = "BREEZ"
SessionCookieName = "session"
SessionCookieAuthKey = "authenticated"
UnlockPasswordCheck = "THIS STRING SHOULD MATCH IF PASSWORD IS CORRECT"
LNDBackendType = "LND"
GreenlightBackendType = "GREENLIGHT"
BreezBackendType = "BREEZ"
SessionCookieName = "session"
SessionCookieAuthKey = "authenticated"
UnlockPasswordCheck = "THIS STRING SHOULD MATCH IF PASSWORD IS CORRECT"
)
type AppConfig struct {

View file

@ -9,7 +9,8 @@ import { handleRequestError } from "src/utils/handleRequestError";
import { request } from "src/utils/request"; // build the project for this to appear
export function SetupNode() {
const [backendType, setBackendType] = React.useState<BackendType>("BREEZ");
const [backendType, setBackendType] =
React.useState<BackendType>("GREENLIGHT");
const { unlockPassword } = useSetupStore();
const [isConnecting, setConnecting] = React.useState(false);
const navigate = useNavigate();
@ -70,6 +71,7 @@ export function SetupNode() {
id="backend-type"
className="dark:bg-surface-00dp mb-4 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:ring-2 focus:ring-purple-700 dark:border-gray-700 dark:text-white dark:placeholder-gray-400 dark:ring-offset-gray-800 dark:focus:ring-purple-600"
>
<option value={"GREENLIGHT"}>Greenlight</option>
<option value={"BREEZ"}>Breez</option>
<option value={"LND"}>LND</option>
</select>
@ -77,6 +79,12 @@ export function SetupNode() {
{backendType === "BREEZ" && (
<BreezForm handleSubmit={handleSubmit} isConnecting={isConnecting} />
)}
{backendType === "GREENLIGHT" && (
<GreenlightForm
handleSubmit={handleSubmit}
isConnecting={isConnecting}
/>
)}
{backendType === "LND" && (
<LNDForm handleSubmit={handleSubmit} isConnecting={isConnecting} />
)}
@ -93,18 +101,18 @@ function BreezForm({ isConnecting, handleSubmit }: SetupFormProps) {
const [greenlightInviteCode, setGreenlightInviteCode] =
React.useState<string>("");
const [breezApiKey, setBreezApiKey] = React.useState<string>("");
const [breezMnemonic, setBreezMnemonic] = React.useState<string>("");
const [mnemonic, setMnemonic] = React.useState<string>("");
function onSubmit(e: React.FormEvent) {
e.preventDefault();
if (!greenlightInviteCode || !breezMnemonic) {
if (!greenlightInviteCode || !mnemonic) {
alert("please fill out all fields");
return;
}
handleSubmit({
greenlightInviteCode,
breezApiKey,
breezMnemonic,
mnemonic,
});
}
@ -147,8 +155,62 @@ function BreezForm({ isConnecting, handleSubmit }: SetupFormProps) {
</label>
<input
name="mnemonic"
onChange={(e) => setBreezMnemonic(e.target.value)}
value={breezMnemonic}
onChange={(e) => setMnemonic(e.target.value)}
value={mnemonic}
type="password"
id="mnemonic"
className="dark:bg-surface-00dp block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:ring-2 focus:ring-purple-700 dark:border-gray-700 dark:text-white dark:placeholder-gray-400 dark:ring-offset-gray-800 dark:focus:ring-purple-600"
/>
</>
<ConnectButton isConnecting={isConnecting} />
</form>
);
}
function GreenlightForm({ isConnecting, handleSubmit }: SetupFormProps) {
const [greenlightInviteCode, setGreenlightInviteCode] =
React.useState<string>("");
const [mnemonic, setMnemonic] = React.useState<string>("");
function onSubmit(e: React.FormEvent) {
e.preventDefault();
if (!greenlightInviteCode || !mnemonic) {
alert("please fill out all fields");
return;
}
handleSubmit({
greenlightInviteCode,
mnemonic,
});
}
return (
<form onSubmit={onSubmit}>
<>
<label
htmlFor="greenlight-invite-code"
className="block font-medium text-gray-900 dark:text-white"
>
Greenlight Invite Code
</label>
<input
name="greenlight-invite-code"
onChange={(e) => setGreenlightInviteCode(e.target.value)}
value={greenlightInviteCode}
type="password"
id="greenlight-invite-code"
className="dark:bg-surface-00dp block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:ring-2 focus:ring-purple-700 dark:border-gray-700 dark:text-white dark:placeholder-gray-400 dark:ring-offset-gray-800 dark:focus:ring-purple-600"
/>
<label
htmlFor="mnemonic"
className="mt-4 block font-medium text-gray-900 dark:text-white"
>
BIP39 Mnemonic
</label>
<input
name="mnemonic"
onChange={(e) => setMnemonic(e.target.value)}
value={mnemonic}
type="password"
id="mnemonic"
className="dark:bg-surface-00dp block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:ring-2 focus:ring-purple-700 dark:border-gray-700 dark:text-white dark:placeholder-gray-400 dark:ring-offset-gray-800 dark:focus:ring-purple-600"

View file

@ -5,7 +5,7 @@ export const NIP_47_MAKE_INVOICE_METHOD = "make_invoice";
export const NIP_47_LOOKUP_INVOICE_METHOD = "lookup_invoice";
export const NIP_47_LIST_TRANSACTIONS_METHOD = "list_transactions";
export type BackendType = "LND" | "BREEZ";
export type BackendType = "LND" | "BREEZ" | "GREENLIGHT";
export type RequestMethodType =
| "pay_invoice"

210
greenlight.go Normal file
View file

@ -0,0 +1,210 @@
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"log"
"math/rand"
"os"
"os/exec"
"path/filepath"
"strconv"
"time"
"github.com/breez/breez-sdk-go/breez_sdk"
models "github.com/getAlby/nostr-wallet-connect/models/greenlight"
)
type GreenlightService struct {
workdir string
hsmdCmd *exec.Cmd
//svc *breez_sdk.BlockingGreenlightServices
}
func NewGreenlightService(mnemonic, inviteCode, workDir string) (result LNClient, err error) {
if mnemonic == "" || inviteCode == "" || workDir == "" {
return nil, errors.New("One or more required greenlight configuration are missing")
}
//create dir if not exists
newpath := filepath.Join(".", workDir)
err = os.MkdirAll(newpath, os.ModePerm)
if err != nil {
log.Printf("Failed to create greenlight working dir: %v", err)
return nil, err
}
seed, err := breez_sdk.MnemonicToSeed(mnemonic)
if err != nil {
log.Printf("Failed to convert mnemonic to seed: %v", err)
return nil, err
}
hsmSecretPath := filepath.Join(newpath, "hsm_secret")
err = os.WriteFile(hsmSecretPath, seed[0:32], 0644)
if err != nil {
log.Printf("Failed to write hsm secret: %v", err)
return nil, err
}
gs := GreenlightService{
workdir: newpath,
//listener: &listener,
//svc: svc,
}
err = gs.recover()
if err != nil {
log.Fatalf("failed to recover: %v", err)
}
nodeInfo := models.NodeInfo{}
err = gs.execJSONCommand(&nodeInfo, "getinfo")
if err != nil {
return nil, err
}
if err == nil {
log.Printf("Node info: %v", nodeInfo)
}
// TODO: schedule
gs.hsmdCmd = gs.createCommand("hsmd")
if err := gs.hsmdCmd.Start(); err != nil {
log.Fatalf("Failed to start hsmd: %v", err)
}
return &gs, nil
}
func (gs *GreenlightService) recover() error {
output, err := gs.execCommand("scheduler", "recover")
log.Printf("scheduler recover: %v", string(output))
return err
}
func (gs *GreenlightService) createCommand(args ...string) *exec.Cmd {
cmd := exec.Command("glcli", args...)
cmd.Dir = gs.workdir
return cmd
}
func (gs *GreenlightService) execCommand(args ...string) ([]byte, error) {
cmd := gs.createCommand(args...)
var outputBuffer bytes.Buffer
var errorBuffer bytes.Buffer
cmd.Stdout = &outputBuffer
cmd.Stderr = &errorBuffer
err := cmd.Run()
if err != nil {
errorOutput := errorBuffer.String()
log.Printf("Failed to exec command %v: %v %v", args, err, errorOutput)
return nil, err
}
output := outputBuffer.Bytes()
return output, err
}
func (gs *GreenlightService) execJSONCommand(dest any, args ...string) error {
output, err := gs.execCommand(args...)
if err != nil {
return err
}
err = json.Unmarshal(output, dest)
if err != nil {
log.Printf("Failed to unmarshal command output %v: %v", string(output), err)
}
return err
}
func (gs *GreenlightService) Shutdown() error {
if gs.hsmdCmd != nil {
if err := gs.hsmdCmd.Process.Kill(); err != nil {
log.Printf("Failed to kill hsmd process: %v", err)
return err
}
}
return nil
//return bs.svc.Disconnect()
}
func (gs *GreenlightService) SendPaymentSync(ctx context.Context, payReq string) (preimage string, err error) {
//glcli pay BOLT11_INVOICE_HERE
payResponse := models.PayResponse{}
err = gs.execJSONCommand(&payResponse, "pay", payReq)
if err != nil {
log.Printf("SendPaymentSync failed: %v", err)
return "", err
}
return payResponse.Preimage, nil
}
func (gs *GreenlightService) SendKeysend(ctx context.Context, amount int64, destination, preimage string, custom_records []TLVRecord) (preImage string, err error) {
log.Println("TODO: SendKeysend")
return "", nil
}
func (gs *GreenlightService) GetBalance(ctx context.Context) (balance int64, err error) {
/*info, err := bs.svc.NodeInfo()
if err != nil {
return 0, err
}
return int64(info.ChannelsBalanceMsat) / 1000, nil*/
log.Println("TODO: GetBalance")
return 0, nil
}
func (gs *GreenlightService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *Nip47Transaction, err error) {
//glcli invoice example_label3 21000msat
invoice := models.Invoice{}
err = gs.execJSONCommand(&invoice, "invoice", "label_"+strconv.Itoa(rand.Int()), strconv.FormatInt(amount, 10)+"msat")
if err != nil {
log.Println("MakeInvoice failed: %v", err)
return nil, err
}
transaction = &Nip47Transaction{
Type: "incoming",
Invoice: invoice.Bolt11,
PaymentHash: invoice.PaymentHash,
Amount: amount,
CreatedAt: time.Now().Unix(),
ExpiresAt: &invoice.ExpiresAt,
}
return transaction, nil
}
func (gs *GreenlightService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *Nip47Transaction, err error) {
log.Println("TODO: LookupInvoice")
return nil, errors.New("TODO: LookupInvoice")
}
func (gs *GreenlightService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []Nip47Transaction, err error) {
log.Println("TODO: ListTransactions")
transactions = []Nip47Transaction{}
return transactions, nil
}
func (gs *GreenlightService) GetInfo(ctx context.Context) (info *NodeInfo, err error) {
log.Println("TODO: GetInfo")
return &NodeInfo{
Alias: "greenlight",
Color: "",
Pubkey: "",
Network: "mainnet",
BlockHeight: 0,
BlockHash: "",
}, nil
}

View file

@ -42,10 +42,14 @@ type UnlockRequest struct {
type SetupRequest struct {
LNBackendType string `json:"backendType"`
// Breez fields
BreezMnemonic string `json:"breezMnemonic"`
BreezAPIKey string `json:"breezApiKey"`
// Breez / Greenlight
Mnemonic string `json:"mnemonic"`
GreenlightInviteCode string `json:"greenlightInviteCode"`
// Breez fields
BreezAPIKey string `json:"breezApiKey"`
// LND fields
LNDAddress string `json:"lndAddress"`
LNDCertFile string `json:"lndCertFile"`

View file

@ -0,0 +1,28 @@
package greenlight
type NodeInfo struct {
ID string `json:"id"`
Alias string `json:"alias"`
// ...other fields
}
type Invoice struct {
Bolt11 string `json:"bolt11"`
PaymentHash string `json:"payment_hash"`
Preimage string `json:"payment_secret"`
ExpiresAt int64 `json:"expires_at"`
// ...other fields
}
type MsatValue struct {
Msat uint `json:"msat"`
}
type PayResponse struct {
PaymentHash string `json:"payment_hash"`
Preimage string `json:"payment_preimage"`
CreatedAt float64 `json:"created_at"`
AmountMsat MsatValue `json:"amount_msat"`
AmountSentMsat MsatValue `json:"amount_sent_msat"`
// ...other fields
}

View file

@ -134,13 +134,19 @@ func (svc *Service) launchLNBackend(encryptionKey string) error {
LNDCertHex, _ := svc.cfg.Get("LNDCertHex", encryptionKey)
LNDMacaroonHex, _ := svc.cfg.Get("LNDMacaroonHex", encryptionKey)
lnClient, err = NewLNDService(svc, LNDAddress, LNDCertHex, LNDMacaroonHex)
case lndBackend:
BreezMnemonic, _ := svc.cfg.Get("BreezMnemonic", encryptionKey)
case GreenlightBackendType:
Mnemonic, _ := svc.cfg.Get("Mnemonic", encryptionKey)
GreenlightInviteCode, _ := svc.cfg.Get("GreenlightInviteCode", encryptionKey)
GreenlightWorkdir := path.Join(svc.cfg.Env.Workdir, "greenlightcli")
lnClient, err = NewGreenlightService(Mnemonic, GreenlightInviteCode, GreenlightWorkdir)
case BreezBackendType:
Mnemonic, _ := svc.cfg.Get("Mnemonic", encryptionKey)
BreezAPIKey, _ := svc.cfg.Get("BreezAPIKey", encryptionKey)
GreenlightInviteCode, _ := svc.cfg.Get("GreenlightInviteCode", encryptionKey)
BreezWorkdir := path.Join(svc.cfg.Env.Workdir, "breez")
lnClient, err = NewBreezService(BreezMnemonic, BreezAPIKey, GreenlightInviteCode, BreezWorkdir)
lnClient, err = NewBreezService(Mnemonic, BreezAPIKey, GreenlightInviteCode, BreezWorkdir)
default:
svc.Logger.Fatalf("Unsupported LNBackendType: %v", lndBackend)
}