Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cli: add pay offer command #91

Merged
merged 8 commits into from
Apr 12, 2024
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ spec = "config_spec.toml"
[dependencies]
async-trait = "0.1.66"
bitcoin = { version = "0.30.2", features = ["rand"] }
clap = { version = "4.4.6", features = ["derive"] }
clap = { version = "4.4.6", features = ["derive", "string"] }
futures = "0.3.26"
home = "0.5.5"
lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "06f9dd7", features = ["max_level_trace", "_test_utils"] }
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ protocol.custom-nodeann=39
protocol.custom-init=39
```

#### Two options for LNDK

Now that we have LND set up properly, there are two key things you can do with LNDK:
1) Forward onion messages. By increasing the number of Lightning nodes out there that can forward onion messages, this increases the anonymity set and helps to bootstrap BOLT 12 for more private payments.
2) Pay BOLT 12 offers, a more private standard for receiving payments over Lightning, which also allows for static invoices.

To accomplish #1, follow the instructions below to get the LNDK binary up and running. For #2, you can [use the CLI](https://github.com/lndk-org/lndk/blob/master/docs/cli_commands.md).

#### Running LNDK

Now we need to set up LNDK. To start:
Expand Down
43 changes: 43 additions & 0 deletions docs/cli_commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# LNDK-CLI

For now, you can use `lndk-cli` separately from `lndk` meaning you don't need to have a `lndk` binary running for `lndk-cli` to work.

## Installation

To use the `lndk-cli` to pay a BOLT 12 offer, follow these instructions:
- To install lndk-cli:
`cargo install --bin=lndk-cli --path .`
- With the above command, `lndk-cli` will be installed to `~/.cargo/bin`. So make sure `~/.cargo/bin` is on your PATH so we can properly run `lndk-cli`.
- Run `lndk-cli -h` to make sure it's working. You'll see output similar to:

```
A cli for interacting with lndk

Usage: lndk-cli [OPTIONS] <COMMAND>

Commands:
decode Decodes a bech32-encoded offer string into a BOLT 12 offer
pay-offer PayOffer pays a BOLT 12 offer, provided as a 'lno'-prefaced offer string
help Print this message or the help of the given subcommand(s)

Options:
-n, --network <NETWORK> Global variables [default: regtest]
-t, --tls-cert <TLS_CERT> [default: /$HOME/.lnd/tls.cert]
-m, --macaroon <MACAROON> [default: /$HOME/.lnd/data/chain/bitcoin/regtest/admin.macaroon]
-a, --address <ADDRESS> [default: https://localhost:10009]
-h, --help Print help
```

## Commands

Once `lndk-cli` is installed, you can use it to pay an offer.

Since `lndk-cli` needs to connect to lnd, you'll need to provide lnd credentials to the binary. If your credentials are not in the default location, you'll need to specify them manually.

If your credentials are in the default location, paying an offer looks like:

`lndk-cli pay-offer <OFFER_STRING> <AMOUNT_MSATS>`

If your credentials are not in the default location, an example command looks like:

`lndk-cli -- --network=mainnet --tls-cert=/credentials/tls.cert --macaroon=/credentials/custom.macaroon --address=https://localhost:10019 pay-offer <OFFER_STRING> <AMOUNT_MSATS>`
70 changes: 70 additions & 0 deletions docs/test_pay_offer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
## Eclair testing instructions

These instructions assume you already have bitcoind and lnd nodes set up on regtest.

The network we'll build will look like this:

`LND --> Eclair --> Eclair2`

Eclair2 will create an offer and we'll make sure lnd can pay the offer with the help of lndk.

For Eclair to create an offer to pay, we need to install the tipjar plugin. Pull down this branch (https://github.com/ACINQ/eclair-plugins/pull/6/files) and build `eclair-plugins`:

`mvn package -DskipTests`

The resulting jar will be located at `eclair-plugins/bolt12-tip-jar/target/bolt12-tip-jar-0.9.1-SNAPSHOT.jar`

Set up two eclair directories, for example at `~/eclair1` and `~/eclair2` with the following configuration within (at `eclair.conf`):

```
eclair {
chain=regtest

server {
port=9995
public-ips = [ "https://localhost" ]
}

bitcoind {
rpcuser="PASSWORD"
rpcpassword="PASSWORD"
rpcport=18443
zmqblock="tcp://127.0.0.1:28334"
zmqtx="tcp://127.0.0.1:29335"
}

api {
enabled = true
port = 8181
password = "testing2"
}

features {
option_route_blinding = optional
}

tip-jar {
description = "donation to eclair"
default-amount-msat = 100000000 // Amount to use if the invoice request does not specify an amount
max-final-expiry-delta = 1000 // How long (in blocks) the route to pay the invoice will be valid
}
}
```

Just:
- Set the correct bitcoind values.
- Make sure that `server.port` and `api.port` are different for each Eclair node.

Install the dependencies for Eclair and install Eclair with `mvn package -DskipTests`. After unzipping the eclair bin (located in `target`), run two eclair nodes, each pointing to the appropriate datadirs we set above, and load in the tipjar plugin:

`./eclair-node.sh /eclair-plugins/bolt12-tip-jar/target/bolt12-tip-jar-0.9.1-SNAPSHOT.jar -Declair.datadir="~/eclair1`

`./eclair-node.sh ../../../../../eclair-plugins/bolt12-tip-jar/target/bolt12-tip-jar-0.9.1-SNAPSHOT.jar -Declair.datadir="~/eclair2`

Using [Eclair's API](https://acinq.github.io/eclair), open two channels: 1) from LND to Eclair1, 2) from Eclair1 to Eclair2. Mine several blocks using bitcoind and make sure those channels are confirmed.

Have Eclair2 create an offer with:

`eclair-cli tipjarshowoffer`

Then pay the offer. Run `lndk-cli pay-offer` using the instructions above.
137 changes: 133 additions & 4 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,60 @@
use clap::{Parser, Subcommand};
use lndk::lndk_offers::decode;
use lndk::lnd::{get_lnd_client, string_to_network, LndCfg};
use lndk::lndk_offers::{decode, get_destination};
use lndk::{Cfg, LifecycleSignals, LndkOnionMessenger, OfferHandler, PayOfferParams};
use log::LevelFilter;
use std::ffi::OsString;
use tokio::select;
use tokio::sync::mpsc;
use tokio::sync::mpsc::{Receiver, Sender};

fn get_cert_path_default() -> OsString {
orbitalturtle marked this conversation as resolved.
Show resolved Hide resolved
home::home_dir()
.unwrap()
.as_path()
.join(".lnd")
.join("tls.cert")
.into_os_string()
}

fn get_macaroon_path_default() -> OsString {
home::home_dir()
.unwrap()
.as_path()
.join(".lnd/data/chain/bitcoin/regtest/admin.macaroon")
.into_os_string()
}

/// A cli for interacting with lndk.
#[derive(Debug, Parser)]
#[command(name = "lndk-cli")]
#[command(about = "A cli for interacting with lndk", long_about = None)]
struct Cli {
/// Global variables
#[arg(
short,
long,
global = true,
required = false,
default_value = "regtest"
)]
network: String,

#[arg(short, long, global = true, required = false, default_value = get_cert_path_default())]
tls_cert: String,

#[arg(short, long, global = true, required = false, default_value = get_macaroon_path_default())]
macaroon: String,

#[arg(
short,
long,
global = true,
required = false,
default_value = "https://localhost:10009"
)]
address: String,

#[command(subcommand)]
command: Commands,
}
Expand All @@ -17,23 +66,103 @@ enum Commands {
/// The offer string to decode.
offer_string: String,
},
/// PayOffer pays a BOLT 12 offer, provided as a 'lno'-prefaced offer string.
PayOffer {
/// The offer string.
offer_string: String,

/// Amount the user would like to pay. If this isn't set, we'll assume the user is paying
/// whatever the offer amount is.
#[arg(required = false)]
amount: Option<u64>,
},
}

fn main() {
#[tokio::main]
async fn main() -> Result<(), ()> {
let args = Cli::parse();
match args.command {
Commands::Decode { offer_string } => {
println!("Decoding offer: {offer_string}.");
match decode(offer_string) {
Ok(offer) => println!("Decoded offer: {:?}.", offer),
Ok(offer) => {
println!("Decoded offer: {:?}.", offer);
Ok(())
}
Err(e) => {
println!(
"ERROR please provide offer starting with lno. Provided offer is \
invalid, failed to decode with error: {:?}.",
e
)
);
Err(())
}
}
}
Commands::PayOffer {
offer_string,
amount,
} => {
let offer = match decode(offer_string) {
Ok(offer) => offer,
Err(e) => {
println!(
"ERROR: please provide offer starting with lno. Provided offer is \
invalid, failed to decode with error: {:?}.",
e
);
return Err(());
}
};

let destination = get_destination(&offer).await;
let network = string_to_network(&args.network).map_err(|e| {
println!("ERROR: invalid network string: {}", e);
})?;
let lnd_cfg = LndCfg::new(args.address, args.tls_cert.into(), args.macaroon.into());
let client = get_lnd_client(lnd_cfg.clone()).map_err(|e| {
println!("ERROR: failed to connect to lnd: {}", e);
})?;

let (shutdown, listener) = triggered::trigger();
let (tx, rx): (Sender<u32>, Receiver<u32>) = mpsc::channel(1);
let signals = LifecycleSignals {
shutdown: shutdown.clone(),
listener,
started: tx,
};
let lndk_cfg = Cfg {
lnd: lnd_cfg,
log_dir: None,
log_level: LevelFilter::Info,
signals,
};

let handler = OfferHandler::new();
let messenger = LndkOnionMessenger::new(handler);
let pay_cfg = PayOfferParams {
offer: offer.clone(),
amount,
network,
client,
destination,
reply_path: None,
};
select! {
_ = messenger.run(lndk_cfg) => {
println!("ERROR: lndk stopped running before pay offer finished.");
},
res = messenger.offer_handler.pay_offer(pay_cfg, rx) => {
match res {
Ok(_) => println!("Successfully paid for offer!"),
Err(err) => println!("Error paying for offer: {err:?}"),
}

shutdown.trigger();
}
}

Ok(())
}
}
}
10 changes: 8 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use std::collections::HashMap;
use std::str::FromStr;
use std::sync::{Mutex, Once};
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::time::{sleep, Duration};
use tokio::time::{sleep, timeout, Duration};
use tonic_lnd::lnrpc::GetInfoRequest;
use tonic_lnd::Client;
use triggered::{Listener, Trigger};
Expand Down Expand Up @@ -240,7 +240,13 @@ impl OfferHandler {
let offer_id = cfg.offer.clone().to_string();
let validated_amount = self.send_invoice_request(cfg, started).await?;

let invoice = self.wait_for_invoice().await;
let invoice = match timeout(Duration::from_secs(100), self.wait_for_invoice()).await {
Ok(invoice) => invoice,
Err(_) => {
error!("Did not receive invoice in 100 seconds.");
return Err(OfferError::InvoiceTimeout);
}
};
{
let mut active_offers = self.active_offers.lock().unwrap();
active_offers.insert(offer_id.clone(), OfferState::InvoiceReceived);
Expand Down
12 changes: 6 additions & 6 deletions src/lnd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const ONION_MESSAGES_REQUIRED: u32 = 38;
pub(crate) const ONION_MESSAGES_OPTIONAL: u32 = 39;

/// get_lnd_client connects to LND's grpc api using the config provided, blocking until a connection is established.
pub(crate) fn get_lnd_client(cfg: LndCfg) -> Result<Client, ConnectError> {
pub fn get_lnd_client(cfg: LndCfg) -> Result<Client, ConnectError> {
block_on(tonic_lnd::connect(cfg.address, cfg.cert, cfg.macaroon))
}

Expand All @@ -49,9 +49,7 @@ impl LndCfg {
}

/// features_support_onion_messages returns a boolean indicating whether a feature set supports onion messaging.
pub(crate) fn features_support_onion_messages(
features: &HashMap<u32, tonic_lnd::lnrpc::Feature>,
) -> bool {
pub fn features_support_onion_messages(features: &HashMap<u32, tonic_lnd::lnrpc::Feature>) -> bool {
features.contains_key(&ONION_MESSAGES_OPTIONAL)
|| features.contains_key(&ONION_MESSAGES_REQUIRED)
}
Expand Down Expand Up @@ -162,7 +160,7 @@ impl<'a> NodeSigner for LndNodeSigner<'a> {

#[derive(Debug)]
/// Error when parsing provided configuration options.
pub(crate) enum NetworkParseError {
pub enum NetworkParseError {
/// Invalid indicates an invalid network was provided.
Invalid(String),
}
Expand All @@ -177,7 +175,9 @@ impl fmt::Display for NetworkParseError {
}
}

pub(crate) fn string_to_network(network_str: &str) -> Result<Network, NetworkParseError> {
pub fn string_to_network(network_str: &str) -> Result<Network, NetworkParseError> {
let network_lowercase = String::from(network_str).to_lowercase();
let network_str = network_lowercase.as_str();
match network_str {
"mainnet" => Ok(Network::Bitcoin),
"testnet" => Ok(Network::Testnet),
Expand Down
3 changes: 3 additions & 0 deletions src/lndk_offers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ pub enum OfferError<Secp256k1Error> {
TrackFailure(Status),
/// Failed to send payment.
PaymentFailure,
/// Failed to receive an invoice back from offer creator before the timeout.
InvoiceTimeout,
}

impl Display for OfferError<Secp256k1Error> {
Expand All @@ -82,6 +84,7 @@ impl Display for OfferError<Secp256k1Error> {
OfferError::RouteFailure(e) => write!(f, "Error routing payment: {e:?}"),
OfferError::TrackFailure(e) => write!(f, "Error tracking payment: {e:?}"),
OfferError::PaymentFailure => write!(f, "Failed to send payment"),
OfferError::InvoiceTimeout => write!(f, "Did not receive invoice in 100 seconds."),
}
}
}
Expand Down
Loading