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

Get Quote and Pay for Inbound Channel #65

Merged
merged 8 commits into from
Nov 20, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
@@ -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"] }
@@ -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)
57 changes: 48 additions & 9 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -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>
@@ -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>
@@ -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() {
@@ -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 = {
@@ -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);
1 change: 0 additions & 1 deletion public/style.css
Original file line number Diff line number Diff line change
@@ -50,7 +50,6 @@ h3 {
}

p {
font-size: 1.5em;
font-weight: 400;
background-image: conic-gradient(
from 0deg at 0% 0%,
2 changes: 1 addition & 1 deletion src/args.rs
Original file line number Diff line number Diff line change
@@ -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
@@ -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"))]
@@ -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>,
}
Comment on lines +105 to +109
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should really be strongly typed with bitcoin::Amount, bitcoin::Amount, std::time::Duration (I have NO clue this u32 duration is. ms? seconds? minutes? months? you get my point) and bitcoin::Address but this PR works and we need to ship. It's not a regression to our codebase but I know we do cleaner work than this.


async fn handle_schedule(
scheduler: Scheduler,
req: Request<Body>,
@@ -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)
}

@@ -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 {
@@ -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)
@@ -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
@@ -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;
@@ -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,
@@ -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 },
@@ -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: _ } => {
@@ -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),
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={}",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should really be a config string but it's viable for this PR

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;
@@ -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);
}

159 changes: 108 additions & 51 deletions src/scheduler.rs
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ use url::Url;

use crate::args::ArgError;
use crate::lnd::{LndClient, LndError};
use crate::lsp::{LspError, Quote};

#[derive(Clone, serde_derive::Deserialize, Debug)]
pub struct ScheduledChannel {
@@ -37,47 +38,56 @@ impl ScheduledChannel {
#[derive(Clone, serde_derive::Deserialize, Debug)]
pub struct ChannelBatch {
channels: Vec<ScheduledChannel>,
wants_inbound_quote: bool,
fee_rate: u64,
}

impl ChannelBatch {
pub fn new(channels: Vec<ScheduledChannel>, fee_rate: u64) -> Self {
Self { channels, fee_rate }
pub fn new(channels: Vec<ScheduledChannel>, wants_inbound_quote: bool, fee_rate: u64) -> Self {
Self { channels, wants_inbound_quote, fee_rate }
}

pub fn channels(&self) -> &Vec<ScheduledChannel> { &self.channels }

pub fn wants_inbound_quote(&self) -> bool { self.wants_inbound_quote }
pub fn fee_rate(&self) -> u64 { self.fee_rate }
}

/// A prepared channel batch.
/// wallet_amount = RequiredReserve from LND set just before returning a bip21 uri.
/// reserve_deposit = RequiredReserve from LND set just before returning a bip21 uri.
#[derive(Clone, serde_derive::Deserialize, Debug)]
pub struct ScheduledPayJoin {
#[serde(with = "bitcoin::util::amount::serde::as_sat")]
wallet_amount: bitcoin::Amount,
reserve_deposit: bitcoin::Amount,
channels: Vec<ScheduledChannel>,
fee_rate: u64,
quote: Option<crate::lsp::Quote>,
}

impl ScheduledPayJoin {
pub fn new(wallet_amount: bitcoin::Amount, batch: ChannelBatch) -> Self {
Self { wallet_amount, channels: batch.channels().clone(), fee_rate: batch.fee_rate() }
pub fn new(
reserve_deposit: bitcoin::Amount,
batch: ChannelBatch,
quote: Option<crate::lsp::Quote>,
) -> Self {
Self {
reserve_deposit,
channels: batch.channels().clone(),
fee_rate: batch.fee_rate(),
quote,
}
}

fn total_amount(&self) -> bitcoin::Amount {
let fees = calculate_fees(
self.channels.len() as u64,
self.fee_rate,
self.wallet_amount != bitcoin::Amount::ZERO,
);

self.channels
.iter()
.map(|channel| channel.amount)
.fold(bitcoin::Amount::ZERO, std::ops::Add::add)
+ self.wallet_amount
+ fees
+ self.reserve_deposit
+ match &self.quote {
Some(quote) => Amount::from_sat(quote.price.into()),
None => Amount::ZERO,
}
+ self.fees()
}

/// Check that amounts make sense for original(ish) psbt.
@@ -89,21 +99,41 @@ impl ScheduledPayJoin {
.iter()
.map(|channel| channel.amount)
.fold(bitcoin::Amount::ZERO, std::ops::Add::add);
// TODO: replace with sheduled_payjoin.fees()
let fees = calculate_fees(
self.channels.len() as u64,
self.fee_rate,
self.wallet_amount() != bitcoin::Amount::ZERO,
);
let wallet_amount = self.wallet_amount();
let reserve_deposit = self.reserve_deposit();
let quote_amount = match &self.quote {
Some(quote) => Amount::from_sat(quote.price.into()),
None => Amount::ZERO,
};

(total_channel_amount + reserve_deposit + quote_amount + self.fees()).as_sat()
== our_output.value
}

let owned_txout_value = our_output.value;
/// This externally exposes [ScheduledPayJoin]::reserve_deposit.
pub fn reserve_deposit(&self) -> bitcoin::Amount { self.reserve_deposit }

(total_channel_amount + fees + wallet_amount).as_sat() == owned_txout_value
}
/// Calculate the absolute miner fee this [ScheduledPayJoin] pays
fn fees(&self) -> bitcoin::Amount {
let channel_count = self.channels.len() as u64;
let has_reserve_deposit = self.reserve_deposit != bitcoin::Amount::ZERO;

/// This externally exposes [ScheduledPayJoin]::wallet_amount.
pub fn wallet_amount(&self) -> bitcoin::Amount { self.wallet_amount }
let mut additional_vsize = if has_reserve_deposit {
// <8 invariant bytes = 4 version + 4 locktime>
// + 2 variant bytes for input.len + output.len such that each len < 252
// + OP_0 OP_PUSHBYTES_32 <32 byte script>
channel_count * (8 + 1 + 1 + 34)
} else {
// substitute 1 p2wsh channel (34 bytes) open for 1 p2wpkh reserve output (22 bytes)
// that's + 12 bytes
(channel_count - 1) * (8 + 1 + 1 + 34) + 12
};

if self.quote.is_some() {
additional_vsize = additional_vsize + (8 + 1 + 1 + 22); // P2WPKH (OP_0 OP_PUSHBYTES_20 <20 byte script)
}

bitcoin::Amount::from_sat(self.fee_rate * additional_vsize)
}

pub async fn multi_open_channel(
&self,
@@ -187,10 +217,10 @@ impl ScheduledPayJoin {
}

// gen_funding_created AKA
fn add_channels_to_psbt<I>(
fn substitue_psbt_outputs<I>(
&self,
original_psbt: PartiallySignedTransaction,
owned_vout: usize,
owned_vout: usize, // the original vout paying us. This is the one we can substitute
funding_txos: I,
) -> PartiallySignedTransaction
where
@@ -200,13 +230,14 @@ impl ScheduledPayJoin {
let funding_txout = iter.next().unwrap(); // we assume there is at least 1.

let mut proposal_psbt = original_psbt.clone();
// determine whether we replace original psbt's owned output
// or whether we change the value to be wallet amount
if self.wallet_amount() == bitcoin::Amount::ZERO {

// determine whether we substitute channel opens for the original psbt's ownedoutput to us
if self.reserve_deposit() == bitcoin::Amount::ZERO {
assert_eq!(funding_txout.value, self.channels[0].amount.as_sat());
proposal_psbt.unsigned_tx.output[owned_vout] = funding_txout;
} else {
proposal_psbt.unsigned_tx.output[owned_vout].value = self.wallet_amount().as_sat();
// or keep it and adjust the amount for the on-chain reserve deposit
proposal_psbt.unsigned_tx.output[owned_vout].value = self.reserve_deposit().as_sat();
proposal_psbt.unsigned_tx.output.push(funding_txout)
}

@@ -249,33 +280,55 @@ impl Scheduler {
&self,
batch: ChannelBatch,
// TODO return bip21::Url Seems broken or incompatible with bip78 now
) -> Result<(String, Address), SchedulerError> {
) -> Result<(String, Address, Option<Quote>), SchedulerError> {
self.test_connections(&batch.channels()).await?;
let bitcoin_addr = self.lnd.get_new_bech32_address().await?;

let required_reserve = self.lnd.required_reserve(batch.channels().len() as u32).await?;
let wallet_balance = self.lnd.wallet_balance().await?;
// Only add reserve if the wallet needs it
let missing_reserve = required_reserve.checked_sub(wallet_balance).unwrap_or_default();
let pj = &ScheduledPayJoin::new(missing_reserve, batch);
let inbound_quote = if batch.wants_inbound_quote() {
match self.request_quote().await {
Ok(quote) => Some(quote),
Err(e) => return Err(e),
}
} else {
None
};
let pj = &ScheduledPayJoin::new(required_reserve, batch, inbound_quote.clone());

if self.insert_payjoin(&bitcoin_addr, pj) {
Ok((
format_bip21(bitcoin_addr.clone(), pj.total_amount(), self.endpoint.clone()),
bitcoin_addr,
inbound_quote,
))
} else {
Err(SchedulerError::Internal("lnd provided duplicate bitcoin addresses"))
}
}

/// Get a quote for an inbound channel from the nolooking service.
/// If the service is unavailable, just return None.
async fn request_quote(&self) -> Result<crate::lsp::Quote, SchedulerError> {
let p2p_address = self.lnd.get_p2p_address().await?;
let refund_address = self.lnd.get_new_bech32_address().await?;
let quote = crate::lsp::request_quote(&p2p_address, &refund_address)
.await
.map_err(SchedulerError::Lsp)?;
Ok(quote)
}

/// Given an Original PSBT request, respond with a PayJoin Proposal,
/// returning a base64-encoded proposal PSBT (BIP-0078).
/// Check the receiver PayJoin checklist as per spec (https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#receivers-original-psbt-checklist)
pub async fn propose_payjoin(
&self,
original_req: UncheckedProposal,
) -> Result<String, SchedulerError> {
use std::str::FromStr;

if original_req.is_output_substitution_disabled() {
return Err(SchedulerError::OutputSubstitutionDisabled);
}
@@ -310,11 +363,26 @@ impl Scheduler {
// initiate multiple `open_channel` requests and return the vector:
// Vec<(temporary_channel_id:, funding_txout:)>
let open_chan_results = pj.multi_open_channel(&self.lnd).await?;
let funding_txouts = open_chan_results.iter().map(|(_, txo)| txo.clone());
let mut txouts_to_substitute: Vec<TxOut> =
open_chan_results.iter().map(|(_, txo)| txo.clone()).collect();
let temporary_chan_ids = open_chan_results.iter().map(|(id, _)| *id);

// add the output paying for inbound
if let Some(quote) = &pj.quote {
let inbound_txo = bitcoin::blockdata::transaction::TxOut {
value: quote.price.into(),
script_pubkey: bitcoin::Address::from_str(&quote.address)
.map_err(|_| SchedulerError::Internal("Could not parse address from LSP quote. Try again or don't request an inbound channel"))?
.script_pubkey(),
};
txouts_to_substitute.push(inbound_txo);
};

// TODO ensure privacy preserving txo ordering. should be responsibility of payjoin lib

// create and send `funding_created` to all responding lightning nodes
let proposal_psbt = pj.add_channels_to_psbt(original_psbt, owned_vout, funding_txouts);
let proposal_psbt =
pj.substitue_psbt_outputs(original_psbt, owned_vout, txouts_to_substitute);

let mut raw_psbt = Vec::new();
proposal_psbt.consensus_encode(&mut raw_psbt)?;
@@ -367,20 +435,6 @@ impl Scheduler {
}
}

pub fn calculate_fees(
channel_count: u64,
fee_rate: u64,
has_additional_output: bool,
) -> bitcoin::Amount {
let additional_vsize = if has_additional_output {
channel_count * (8 + 1 + 1 + 32)
} else {
(channel_count - 1) * (8 + 1 + 1 + 32) + 12
};

bitcoin::Amount::from_sat(fee_rate * additional_vsize)
}

pub fn format_bip21(address: Address, amount: Amount, endpoint: url::Url) -> String {
let bip21_str = format!(
"bitcoin:{}?amount={}&pj={}pj",
@@ -393,6 +447,9 @@ pub fn format_bip21(address: Address, amount: Amount, endpoint: url::Url) -> Str

#[derive(Debug)]
pub enum SchedulerError {
/// Error at the lightning service provider controller
Lsp(LspError),
/// Error at the lightning node controller
Lnd(LndError),
/// Internal error that should not be shared
Internal(&'static str),
4 changes: 2 additions & 2 deletions tests/integration.rs
Original file line number Diff line number Diff line change
@@ -162,9 +162,9 @@ mod integration {
let fee_rate = 1;
let mut channels = Vec::with_capacity(1);
channels.push(ScheduledChannel::new(peer_address, channel_capacity));
let batch = ChannelBatch::new(channels, fee_rate);
let batch = ChannelBatch::new(channels, false, fee_rate);
let scheduler = Scheduler::new(LndClient::new(merchant_client).await.unwrap(), endpoint);
let (bip21, _) = scheduler.schedule_payjoin(batch).await.unwrap();
let (bip21, _, _) = scheduler.schedule_payjoin(batch).await.unwrap();
println!("{}", &bip21);

let loop_til_open_channel = tokio::spawn(async move {