Skip to content

Commit

Permalink
Merge pull request #65 from nickfarrow/inbound
Browse files Browse the repository at this point in the history
Lease inbound capacity as part of the PayJoin
  • Loading branch information
DanGould authored Nov 20, 2022
2 parents cbdd9b3 + 2953eed commit 8976750
Show file tree
Hide file tree
Showing 12 changed files with 255 additions and 72 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ bip78 = { git = "https://github.com/dangould/rust-payjoin", rev = "8d9b7d6", fea
url = "2.2.2"
hyper = "0.14.9"
tonic_lnd = "0.5.0"
hyper-tls = "0.5.0"
tokio = { version = "1.7.1", features = ["rt-multi-thread"] }
rand = "0.8.4"
base64 = "0.13.0"
serde = "1.0.126"
serde_json = "1.0.87"
serde_qs = "0.10.1"
serde_derive = "1.0.126"
ln-types = { version = "0.1.3", features = ["serde", "parse_arg"] }
Expand All @@ -36,7 +38,6 @@ rcgen = "0.10.0"
tempfile = "3"
tonic = "0.6.2"
hyper = { version = "0.14.9", features = ["full"] }
hyper-tls = "0.5.0"
tokio-native-tls = "0.3"

# in dev mode optimize crates that are perf-critical (usually just crypto crates)
Expand Down
57 changes: 48 additions & 9 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ <h2>Queue batches of lightning channels to open in a single transaction</h2>
<main class="center-axyz">
<form action="/schedule" method="post" enctype="application/x-www-form-urlencoded"
x-flex direction="column">
<fieldset>
<legend>Queue Inbound Channel</legend>
<x-grid columns="2">
<label for="inboundCapacity" span="1">Get receiving capacity:</label>
<label span="!"><input type="number" id="inboundCapacity" value="1000000" disabled span="1">sats</label><!-- has no name to exclude from request -->
<div span="1"></div>
<div span="1">
<label for="wantsInbound">Get inbound channel quote (~30,000 sats)</label>
<input type="hidden" id="hiddenWantsInbound" value="false" name="wants_inbound_quote">
<input type="checkbox" id="wantsInbound" name="wants_inbound_quote" value="true" checked>
</div>
</x-grid>
</fieldset>
<fieldset>
<legend>Queue Channels to Open</legend>
<x-grid columns=2>
Expand All @@ -36,7 +49,7 @@ <h2>Queue batches of lightning channels to open in a single transaction</h2>
<x-grid columns=2 id="channels">
<input type="text" name="channels[0][node]" aria-labelledby="destinationLabel" placeholder="node@host:port" required>
<!-- LND enforces minimum 20k sats channel -->
<input type="number" id="input_sats" name="channels[0][amount]" min="20000" step="1" aria-labelledby="capacityLabel" required>
<input type="number" name="channels[0][amount]" min="20000" step="1" aria-labelledby="capacityLabel" required>
</x-grid>
<p>
<x-flex>
Expand All @@ -56,13 +69,16 @@ <h2>Queue batches of lightning channels to open in a single transaction</h2>
</div>
<output id="queued" class="invisible">
<h2>PayJoin here to open these channels</h2>
<p class="warning">⚠ This software is still extremely experimental and has not been vetted, use at your own risk ⚠</code>
<p class="warning">⚠ This software is still extremely experimental and has not been vetted, use at your own risk ⚠</p>
<div id="qrcode" class="center-axyz"></div>
<a href="" id="bip21"></a>
<p>Please use <a href="https://en.bitcoin.it/wiki/PayJoin_adoption" target="_blank">a wallet that supports</a> BIP 78 <a href="https://bitcoinmagazine.com/culture/blockchain-analysis-about-get-harder-p2ep-enters-testing-phase" target="blank">P2EP</a> PayJoins!</p>
<p>Please pay using <a href="https://en.bitcoin.it/wiki/PayJoin_adoption" target="_blank">a wallet that supports</a> BIP 78 <a href="https://bitcoinmagazine.com/culture/blockchain-analysis-about-get-harder-p2ep-enters-testing-phase" target="blank">P2EP</a> PayJoins!</p>
<br>
<div id="lsp-response" class="invisible">
<!-- populated on POST /schedule response -->
</div>
</output>
</form>

</main>
<script type="text/javascript">
function add_channel() {
Expand All @@ -85,8 +101,18 @@ <h2>PayJoin here to open these channels</h2>
channels.removeChild(channels.lastElementChild);
}

document.querySelector("#wantsInbound").addEventListener("input", async (event) => {
document.querySelector("#inboundCapacity").value = event.target.checked ? 1000000 : 0;
});

document.querySelector("form").addEventListener("submit", async (event) => {
event.preventDefault();

// Disable hiddens if checkboxes are checked
if(document.getElementById("wantsInbound").checked) {
document.getElementById('hiddenWantsInbound').disabled = true;
}

let form = event.currentTarget;
let resource = form.action;
let options = {
Expand All @@ -103,14 +129,27 @@ <h2>PayJoin here to open these channels</h2>
throw new Error('Something went wrong.');

let link = document.getElementById("bip21");
let bip21 = await r.text();
link.href = bip21;
link.innerHTML = bip21;
let r_json = await r.json();
link.href = r_json.bip21;
link.innerHTML = r_json.bip21;

var address = bip21.split("bitcoin:")[1].split("?")[0];
document.getElementById("qrcode").innerHTML = `<img src="/qr_codes/${address}.png" width="256px" />`;
document.getElementById("qrcode").innerHTML = `<img src="/qr_codes/${r_json.address}.png" width="256px" />`;
document.getElementById("queue").classList.add("invisible");
document.getElementById("queued").classList.remove("invisible");
let lspResponse = document.getElementById("lsp-response")

if (r_json.quote) {
lspResponse.classList.remove("invisible");
let quoteTemplate =`
<h3>Inbound Channel Quote:</h3>
<p>
This request includes a ${r_json.quote.price} sats transfer to lease ${r_json.quote.size} sats of inbound capacity for ${r_json.quote.duration} ${r_json.quote.duration == 1 ? "month": "months"}.
<br>
A refund address from your lightning node's on-chain wallet is already registered in case of failure.
</p>
`
lspResponse.innerHTML = quoteTemplate;
}
})
.catch((err) => {
alert(err);
Expand Down
1 change: 0 additions & 1 deletion public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ h3 {
}

p {
font-size: 1.5em;
font-weight: 400;
background-image: conic-gradient(
from 0deg at 0% 0%,
Expand Down
2 changes: 1 addition & 1 deletion src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,5 @@ where

// ignore a remaining arguments

Ok(Some(ChannelBatch::new(channels, fee_rate)))
Ok(Some(ChannelBatch::new(channels, false, fee_rate)))
}
27 changes: 21 additions & 6 deletions src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Method, Request, Response, Server, StatusCode};
use qrcode_generator::QrCodeEcc;

use crate::lsp::Quote;
use crate::scheduler::{ChannelBatch, Scheduler, SchedulerError};

#[cfg(not(feature = "test_paths"))]
Expand Down Expand Up @@ -100,6 +101,13 @@ async fn handle_pj(scheduler: Scheduler, req: Request<Body>) -> Result<Response<
Ok(Response::new(Body::from(proposal_psbt)))
}

#[derive(Clone, Deserialize, Serialize, Debug)]
pub struct ScheduleResponse {
bip21: String,
address: String,
quote: Option<Quote>,
}

async fn handle_schedule(
scheduler: Scheduler,
req: Request<Body>,
Expand All @@ -109,10 +117,15 @@ async fn handle_schedule(
let conf = serde_qs::Config::new(5, false); // 5 is default max_depth
let request: ChannelBatch = conf.deserialize_bytes(&bytes)?;

let (uri, address) = scheduler.schedule_payjoin(request).await?;
let mut response = Response::new(Body::from(uri.clone()));
let (uri, address, quote) = scheduler.schedule_payjoin(request).await?;

let schedule_response =
ScheduleResponse { bip21: uri.clone(), address: address.to_string(), quote };
let mut response = Response::new(Body::from(
serde_json::to_string(&schedule_response).map_err(HttpError::SerdeJson)?,
));
create_qr_code(&uri, &address.to_string());
response.headers_mut().insert(hyper::header::CONTENT_TYPE, "text/plain".parse()?);
response.headers_mut().insert(hyper::header::CONTENT_TYPE, "application/json".parse()?);
Ok(response)
}

Expand All @@ -128,7 +141,8 @@ pub enum HttpError {
Http(hyper::http::Error),
InvalidHeaderValue(hyper::header::InvalidHeaderValue),
Scheduler(SchedulerError),
Serde(serde_qs::Error),
SerdeQs(serde_qs::Error),
SerdeJson(serde_json::Error),
}

impl HttpError {
Expand All @@ -149,7 +163,8 @@ impl HttpError {
| Self::Http(_)
| Self::InvalidHeaderValue(_)
| Self::Scheduler(_)
| Self::Serde(_) => StatusCode::BAD_REQUEST,
| Self::SerdeQs(_)
| Self::SerdeJson(_) => StatusCode::BAD_REQUEST,
};

// TODO: Avoid writing error directly to HTTP response (bad security if public facing)
Expand Down Expand Up @@ -184,5 +199,5 @@ impl From<SchedulerError> for HttpError {
}

impl From<serde_qs::Error> for HttpError {
fn from(e: serde_qs::Error) -> Self { Self::Serde(e) }
fn from(e: serde_qs::Error) -> Self { Self::SerdeQs(e) }
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ extern crate configure_me;
pub mod args;
pub mod http;
pub mod lnd;
pub mod lsp;
pub mod scheduler;
configure_me::include_config!();
15 changes: 15 additions & 0 deletions src/lnd.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::convert::TryFrom;
use std::fmt;
use std::num::TryFromIntError;
use std::str::FromStr;
use std::sync::Arc;

use bitcoin::consensus::Decodable;
Expand Down Expand Up @@ -99,6 +100,17 @@ impl LndClient {
response.get_ref().address.parse::<Address>().map_err(LndError::ParseBitcoinAddressFailed)
}

pub async fn get_p2p_address(&self) -> Result<P2PAddress, LndError> {
let mut client = self.0.lock().await;
let response = client
.lightning()
.get_info(tonic_lnd::lnrpc::GetInfoRequest { ..Default::default() })
.await?;
let p2p_address = P2PAddress::from_str(&response.into_inner().uris[0])
.map_err(LndError::ParseP2PAddressFailed)?;
Ok(p2p_address)
}

/// Requests to open a channel with remote node, returning the psbt of the funding transaction.
pub async fn open_channel(
&self,
Expand Down Expand Up @@ -194,6 +206,7 @@ pub enum LndError {
Decode(bitcoin::consensus::encode::Error),
ParseBitcoinAddressFailed(bitcoin::util::address::Error),
ParseAsSatFailed(TryFromIntError),
ParseP2PAddressFailed(ln_types::p2p_address::ParseError),
VersionRequestFailed(tonic_lnd::Error),
UnexpectedUpdate(tonic_lnd::lnrpc::open_status_update::Update),
ParseVersionFailed { version: String, error: std::num::ParseIntError },
Expand All @@ -208,6 +221,7 @@ impl fmt::Display for LndError {
LndError::Decode(e) => e.fmt(f),
LndError::ParseBitcoinAddressFailed(e) => e.fmt(f),
LndError::ParseAsSatFailed(err) => err.fmt(f),
LndError::ParseP2PAddressFailed(e) => e.fmt(f),
LndError::VersionRequestFailed(_) => write!(f, "failed to get LND version"),
LndError::UnexpectedUpdate(e) => write!(f, "Unexpected channel update {:?}", e),
LndError::ParseVersionFailed { version, error: _ } => {
Expand All @@ -230,6 +244,7 @@ impl std::error::Error for LndError {
LndError::Decode(e) => Some(e),
LndError::ParseBitcoinAddressFailed(e) => Some(e),
LndError::ParseAsSatFailed(_) => None,
LndError::ParseP2PAddressFailed(e) => Some(e),
LndError::VersionRequestFailed(e) => Some(e),
Self::UnexpectedUpdate(_) => None,
LndError::ParseVersionFailed { version: _, error } => Some(error),
Expand Down
54 changes: 54 additions & 0 deletions src/lsp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use bitcoin::Address;
use hyper::{Body, Client, Uri};
use ln_types::P2PAddress;
use serde_derive::{Deserialize, Serialize};

async fn body_to_string(body: Body) -> Result<String, LspError> {
let body_bytes = hyper::body::to_bytes(body).await.map_err(InternalLspError::Hyper)?;
Ok(String::from_utf8(body_bytes.to_vec()).map_err(InternalLspError::FromUtf8)?)
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Quote {
pub price: u32,
size: u32,
duration: u32,
pub address: String,
}

pub async fn request_quote(
p2p_address: &P2PAddress,
refund_address: &Address,
) -> Result<Quote, LspError> {
let https = hyper_tls::HttpsConnector::new();
let client = Client::builder().build::<_, hyper::Body>(https);
// get address
// suggest capacity
let base_uri = format!(
"https://nolooking.chaincase.app/api/request-inbound?nodeid={}&capacity={}&duration={}&refund_address={}",
p2p_address, 1000000, 1, refund_address
); // TODO confirm p2p_address is urlencoded
let url: Uri = base_uri.parse().map_err(InternalLspError::Uri)?;
let req =
hyper::Request::post(url).body(hyper::Body::empty()).map_err(InternalLspError::Http)?;
let res = client.request(req).await.map_err(InternalLspError::Hyper)?;
let body_str = body_to_string(res.into_body()).await?;
let quote: Quote = serde_json::from_str(&body_str).map_err(InternalLspError::SerdeJson)?;
Ok(quote)
}

#[derive(Debug)]
pub struct LspError(InternalLspError);

#[derive(Debug)]
pub(crate) enum InternalLspError {
Uri(hyper::http::uri::InvalidUri),
FromUtf8(std::string::FromUtf8Error),
Hyper(hyper::Error),
Http(hyper::http::Error),
SerdeJson(serde_json::Error),
}

impl From<InternalLspError> for LspError {
fn from(value: InternalLspError) -> Self { LspError(value) }
}
3 changes: 2 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod args;
mod http;
mod lnd;
mod lsp;
pub mod scheduler;

use scheduler::Scheduler;
Expand All @@ -23,7 +24,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let scheduler = Scheduler::from_config(&config).await?;

if let Some(batch) = channel_batch {
let (bip21, _) = scheduler.schedule_payjoin(batch).await?;
let (bip21, _, _) = scheduler.schedule_payjoin(batch).await?;
println!("{}", bip21);
}

Expand Down
Loading

0 comments on commit 8976750

Please sign in to comment.