Skip to content

Commit 569b391

Browse files
committed
add pj scheduler
1 parent 020a1eb commit 569b391

File tree

1 file changed

+328
-0
lines changed

1 file changed

+328
-0
lines changed

src/payjoin_scheduler.rs

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
use bitcoin::{secp256k1::PublicKey, ScriptBuf, TxOut};
2+
3+
#[derive(Clone)]
4+
pub struct PayjoinScheduler {
5+
channels: Vec<PayjoinChannel>,
6+
}
7+
8+
impl PayjoinScheduler {
9+
/// Create a new empty channel scheduler.
10+
pub fn new() -> Self {
11+
Self { channels: vec![] }
12+
}
13+
/// Schedule a new channel.
14+
///
15+
/// The channel will be created with `ScheduledChannelState::ChannelCreated` state.
16+
pub fn schedule(
17+
&mut self, channel_value_satoshi: bitcoin::Amount, counterparty_node_id: PublicKey,
18+
channel_id: u128,
19+
) {
20+
let channel = PayjoinChannel::new(channel_value_satoshi, counterparty_node_id, channel_id);
21+
match channel.state {
22+
ScheduledChannelState::ChannelCreated => {
23+
self.channels.push(channel);
24+
},
25+
_ => {},
26+
}
27+
}
28+
/// Mark a channel as accepted.
29+
///
30+
/// The channel will be updated to `ScheduledChannelState::ChannelAccepted` state.
31+
pub fn set_channel_accepted(
32+
&mut self, channel_id: u128, output_script: &ScriptBuf, temporary_channel_id: [u8; 32],
33+
) -> bool {
34+
for channel in &mut self.channels {
35+
if channel.channel_id() == channel_id {
36+
channel.state.set_channel_accepted(output_script, temporary_channel_id);
37+
return true;
38+
}
39+
}
40+
false
41+
}
42+
/// Mark a channel as funding tx created.
43+
///
44+
/// The channel will be updated to `ScheduledChannelState::FundingTxCreated` state.
45+
pub fn set_funding_tx_created(
46+
&mut self, channel_id: u128, url: &payjoin::Url, body: Vec<u8>,
47+
) -> bool {
48+
for channel in &mut self.channels {
49+
if channel.channel_id() == channel_id {
50+
return channel.state.set_channel_funding_tx_created(url.clone(), body);
51+
}
52+
}
53+
false
54+
}
55+
/// Mark a channel as funding tx signed.
56+
///
57+
/// The channel will be updated to `ScheduledChannelState::FundingTxSigned` state.
58+
pub fn set_funding_tx_signed(
59+
&mut self, tx: bitcoin::Transaction,
60+
) -> Option<(payjoin::Url, Vec<u8>)> {
61+
for output in tx.output.iter() {
62+
if let Some(mut channel) = self.internal_find_by_tx_out(&output.clone()) {
63+
let info = channel.request_info();
64+
if info.is_some() && channel.state.set_channel_funding_tx_signed(output.clone()) {
65+
return info;
66+
}
67+
}
68+
}
69+
None
70+
}
71+
/// Get the next channel matching the given channel amount.
72+
///
73+
/// The channel must be in the accepted state.
74+
///
75+
/// If more than one channel matches the given channel amount, the channel with the oldest
76+
/// creation date will be returned.
77+
pub fn get_next_channel(
78+
&self, channel_amount: bitcoin::Amount,
79+
) -> Option<(u128, bitcoin::Address, [u8; 32], bitcoin::Amount, bitcoin::secp256k1::PublicKey)>
80+
{
81+
let channel = self
82+
.channels
83+
.iter()
84+
.filter(|channel| {
85+
channel.channel_value_satoshi() == channel_amount
86+
&& channel.is_channel_accepted()
87+
&& channel.output_script().is_some()
88+
&& channel.temporary_channel_id().is_some()
89+
})
90+
.min_by_key(|channel| channel.created_at());
91+
92+
if let Some(channel) = channel {
93+
let address = bitcoin::Address::from_script(
94+
&channel.output_script().unwrap(),
95+
bitcoin::Network::Regtest, // fixme
96+
);
97+
if let Ok(address) = address {
98+
return Some((
99+
channel.channel_id(),
100+
address,
101+
channel.temporary_channel_id().unwrap(),
102+
channel.channel_value_satoshi(),
103+
channel.counterparty_node_id(),
104+
));
105+
}
106+
};
107+
None
108+
}
109+
110+
/// List all channels.
111+
pub fn list_channels(&self) -> &Vec<PayjoinChannel> {
112+
&self.channels
113+
}
114+
115+
pub fn in_progress(&self) -> bool {
116+
self.channels.iter().any(|channel| !channel.is_channel_accepted())
117+
}
118+
fn internal_find_by_tx_out(&self, txout: &TxOut) -> Option<PayjoinChannel> {
119+
let channel = self.channels.iter().find(|channel| {
120+
return Some(&txout.script_pubkey) == channel.output_script();
121+
});
122+
channel.cloned()
123+
}
124+
}
125+
126+
/// A struct representing a scheduled channel.
127+
#[derive(Clone, Debug)]
128+
pub struct PayjoinChannel {
129+
state: ScheduledChannelState,
130+
channel_value_satoshi: bitcoin::Amount,
131+
channel_id: u128,
132+
counterparty_node_id: PublicKey,
133+
created_at: u64,
134+
}
135+
136+
impl PayjoinChannel {
137+
pub fn new(
138+
channel_value_satoshi: bitcoin::Amount, counterparty_node_id: PublicKey, channel_id: u128,
139+
) -> Self {
140+
Self {
141+
state: ScheduledChannelState::ChannelCreated,
142+
channel_value_satoshi,
143+
channel_id,
144+
counterparty_node_id,
145+
created_at: 0,
146+
}
147+
}
148+
149+
fn is_channel_accepted(&self) -> bool {
150+
match self.state {
151+
ScheduledChannelState::ChannelAccepted(..) => true,
152+
_ => false,
153+
}
154+
}
155+
156+
pub fn channel_value_satoshi(&self) -> bitcoin::Amount {
157+
self.channel_value_satoshi
158+
}
159+
160+
/// Get the user channel id.
161+
pub fn channel_id(&self) -> u128 {
162+
self.channel_id
163+
}
164+
165+
/// Get the counterparty node id.
166+
pub fn counterparty_node_id(&self) -> PublicKey {
167+
self.counterparty_node_id
168+
}
169+
170+
/// Get the output script.
171+
pub fn output_script(&self) -> Option<&ScriptBuf> {
172+
self.state.output_script()
173+
}
174+
175+
/// Get the temporary channel id.
176+
pub fn temporary_channel_id(&self) -> Option<[u8; 32]> {
177+
self.state.temporary_channel_id()
178+
}
179+
180+
/// Get the temporary channel id.
181+
pub fn tx_out(&self) -> Option<&TxOut> {
182+
match &self.state {
183+
ScheduledChannelState::FundingTxSigned(_, txout) => Some(txout),
184+
_ => None,
185+
}
186+
}
187+
188+
pub fn request_info(&self) -> Option<(payjoin::Url, Vec<u8>)> {
189+
match &self.state {
190+
ScheduledChannelState::FundingTxCreated(_, url, body) => {
191+
Some((url.clone(), body.clone()))
192+
},
193+
_ => None,
194+
}
195+
}
196+
197+
fn created_at(&self) -> u64 {
198+
self.created_at
199+
}
200+
}
201+
202+
#[derive(Clone, Debug)]
203+
struct FundingTxParams {
204+
output_script: ScriptBuf,
205+
temporary_channel_id: [u8; 32],
206+
}
207+
208+
impl FundingTxParams {
209+
fn new(output_script: ScriptBuf, temporary_channel_id: [u8; 32]) -> Self {
210+
Self { output_script, temporary_channel_id }
211+
}
212+
}
213+
214+
#[derive(Clone, Debug)]
215+
enum ScheduledChannelState {
216+
ChannelCreated,
217+
ChannelAccepted(FundingTxParams),
218+
FundingTxCreated(FundingTxParams, payjoin::Url, Vec<u8>),
219+
FundingTxSigned(FundingTxParams, TxOut),
220+
}
221+
222+
impl ScheduledChannelState {
223+
fn output_script(&self) -> Option<&ScriptBuf> {
224+
match self {
225+
ScheduledChannelState::ChannelAccepted(funding_tx_params) => {
226+
Some(&funding_tx_params.output_script)
227+
},
228+
ScheduledChannelState::FundingTxCreated(funding_tx_params, _, _) => {
229+
Some(&funding_tx_params.output_script)
230+
},
231+
ScheduledChannelState::FundingTxSigned(funding_tx_params, _) => {
232+
Some(&funding_tx_params.output_script)
233+
},
234+
_ => None,
235+
}
236+
}
237+
238+
fn temporary_channel_id(&self) -> Option<[u8; 32]> {
239+
match self {
240+
ScheduledChannelState::ChannelAccepted(funding_tx_params) => {
241+
Some(funding_tx_params.temporary_channel_id)
242+
},
243+
ScheduledChannelState::FundingTxCreated(funding_tx_params, _, _) => {
244+
Some(funding_tx_params.temporary_channel_id)
245+
},
246+
ScheduledChannelState::FundingTxSigned(funding_tx_params, _) => {
247+
Some(funding_tx_params.temporary_channel_id)
248+
},
249+
_ => None,
250+
}
251+
}
252+
253+
fn set_channel_accepted(
254+
&mut self, output_script: &ScriptBuf, temporary_channel_id: [u8; 32],
255+
) -> bool {
256+
if let ScheduledChannelState::ChannelCreated = self {
257+
*self = ScheduledChannelState::ChannelAccepted(FundingTxParams::new(
258+
output_script.clone(),
259+
temporary_channel_id,
260+
));
261+
return true;
262+
}
263+
return false;
264+
}
265+
266+
fn set_channel_funding_tx_created(&mut self, url: payjoin::Url, body: Vec<u8>) -> bool {
267+
if let ScheduledChannelState::ChannelAccepted(funding_tx_params) = self {
268+
*self = ScheduledChannelState::FundingTxCreated(funding_tx_params.clone(), url, body);
269+
return true;
270+
}
271+
return false;
272+
}
273+
274+
fn set_channel_funding_tx_signed(&mut self, output: TxOut) -> bool {
275+
let mut res = false;
276+
if let ScheduledChannelState::FundingTxCreated(funding_tx_params, _, _) = self {
277+
*self =
278+
ScheduledChannelState::FundingTxSigned(funding_tx_params.clone(), output.clone());
279+
res = true;
280+
}
281+
return res;
282+
}
283+
}
284+
285+
// #[cfg(test)]
286+
// mod tests {
287+
// use std::str::FromStr;
288+
289+
// use super::*;
290+
// use bitcoin::{
291+
// psbt::Psbt,
292+
// secp256k1::{self, Secp256k1},
293+
// };
294+
295+
// #[ignore]
296+
// #[test]
297+
// fn test_channel_scheduler() {
298+
// let create_pubkey = || -> PublicKey {
299+
// let secp = Secp256k1::new();
300+
// PublicKey::from_secret_key(&secp, &secp256k1::SecretKey::from_slice(&[1; 32]).unwrap())
301+
// };
302+
// let channel_value_satoshi = 100;
303+
// let node_id = create_pubkey();
304+
// let channel_id: u128 = 0;
305+
// let mut channel_scheduler = PayjoinScheduler::new();
306+
// channel_scheduler.schedule(
307+
// bitcoin::Amount::from_sat(channel_value_satoshi),
308+
// node_id,
309+
// channel_id,
310+
// );
311+
// assert_eq!(channel_scheduler.channels.len(), 1);
312+
// assert_eq!(channel_scheduler.is_channel_created(channel_id), true);
313+
// channel_scheduler.set_channel_accepted(
314+
// channel_id,
315+
// &ScriptBuf::from(vec![1, 2, 3]),
316+
// [0; 32],
317+
// );
318+
// assert_eq!(channel_scheduler.is_channel_accepted(channel_id), true);
319+
// let str_psbt = "cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA=";
320+
// let mock_transaction = Psbt::from_str(str_psbt).unwrap();
321+
// let _our_txout = mock_transaction.clone().extract_tx().output[0].clone();
322+
// // channel_scheduler.set_funding_tx_created(channel_id, mock_transaction.clone());
323+
// // let tx_id = mock_transaction.extract_tx().txid();
324+
// // assert_eq!(channel_scheduler.is_funding_tx_created(&tx_id), true);
325+
// // channel_scheduler.set_funding_tx_signed(tx_id);
326+
// // assert_eq!(channel_scheduler.is_funding_tx_signed(&tx_id), true);
327+
// }
328+
// }

0 commit comments

Comments
 (0)