Skip to content

Commit 9e137e3

Browse files
committed
Persist sender when initialized
This commit introduces a persistence flow for constructing `v2::Sender`. We enforce that senders must be persisted before initiating their type state. To support this, callers will first create a `NewSender`, which must be persisted. Once persisted, a storage token is returned that can be passed to `v2::Sender::load(...)` to construct the full `v2::Sender`. For use cases that do not require persistence—such as testing— can use the `NoopPersister` provided in `persist/mod.rs`. Issue: payjoin#336
1 parent 261ddcc commit 9e137e3

File tree

6 files changed

+180
-48
lines changed

6 files changed

+180
-48
lines changed

payjoin-cli/src/app/v2.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use super::config::Config;
1414
use super::wallet::BitcoindWallet;
1515
use super::App as AppTrait;
1616
use crate::app::{handle_interrupt, http_agent};
17-
use crate::db::v2::ReceiverPersister;
17+
use crate::db::v2::{ReceiverPersister, SenderPersister};
1818
use crate::db::Database;
1919

2020
#[derive(Clone)]
@@ -53,11 +53,15 @@ impl AppTrait for App {
5353
Some(send_session) => send_session,
5454
None => {
5555
let psbt = self.create_original_psbt(&uri, fee_rate)?;
56-
let mut req_ctx = SenderBuilder::new(psbt, uri.clone())
56+
let mut persister = SenderPersister::new(self.db.clone());
57+
let new_sender = SenderBuilder::new(psbt, uri.clone())
5758
.build_recommended(fee_rate)
5859
.with_context(|| "Failed to build payjoin request")?;
59-
self.db.insert_send_session(&mut req_ctx, url)?;
60-
req_ctx
60+
let storage_token = new_sender
61+
.persist(&mut persister)
62+
.map_err(|e| anyhow!("Failed to persist sender: {}", e))?;
63+
Sender::load(storage_token, &persister)
64+
.map_err(|e| anyhow!("Failed to load sender: {}", e))?
6165
}
6266
};
6367
self.spawn_payjoin_sender(req_ctx).await

payjoin-cli/src/db/v2.rs

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,36 @@ use std::sync::Arc;
33
use bitcoincore_rpc::jsonrpc::serde_json;
44
use payjoin::persist::{Persister, Value};
55
use payjoin::receive::v2::{Receiver, ReceiverToken};
6-
use payjoin::send::v2::Sender;
6+
use payjoin::send::v2::{Sender, SenderToken};
77
use sled::Tree;
88
use url::Url;
99

1010
use super::*;
1111

12+
pub(crate) struct SenderPersister(Arc<Database>);
13+
impl SenderPersister {
14+
pub fn new(db: Arc<Database>) -> Self { Self(db) }
15+
}
16+
17+
impl Persister<Sender> for SenderPersister {
18+
type Token = SenderToken;
19+
type Error = crate::db::error::Error;
20+
fn save(&mut self, value: Sender) -> std::result::Result<SenderToken, Self::Error> {
21+
let send_tree = self.0 .0.open_tree("send_sessions")?;
22+
let key = value.key();
23+
let value = serde_json::to_vec(&value).map_err(Error::Serialize)?;
24+
send_tree.insert(key.clone(), value.as_slice())?;
25+
send_tree.flush()?;
26+
Ok(key)
27+
}
28+
29+
fn load(&self, key: SenderToken) -> std::result::Result<Sender, Self::Error> {
30+
let send_tree = self.0 .0.open_tree("send_sessions")?;
31+
let value = send_tree.get(key.as_ref())?.ok_or(Error::NotFound(key.to_string()))?;
32+
serde_json::from_slice(&value).map_err(Error::Deserialize)
33+
}
34+
}
35+
1236
pub(crate) struct ReceiverPersister(Arc<Database>);
1337
impl ReceiverPersister {
1438
pub fn new(db: Arc<Database>) -> Self { Self(db) }
@@ -51,14 +75,6 @@ impl Database {
5175
Ok(())
5276
}
5377

54-
pub(crate) fn insert_send_session(&self, session: &mut Sender, pj_url: &Url) -> Result<()> {
55-
let send_tree: Tree = self.0.open_tree("send_sessions")?;
56-
let value = serde_json::to_string(session).map_err(Error::Serialize)?;
57-
send_tree.insert(pj_url.to_string(), IVec::from(value.as_str()))?;
58-
send_tree.flush()?;
59-
Ok(())
60-
}
61-
6278
pub(crate) fn get_send_sessions(&self) -> Result<Vec<Sender>> {
6379
let send_tree: Tree = self.0.open_tree("send_sessions")?;
6480
let mut sessions = Vec::new();
@@ -72,7 +88,7 @@ impl Database {
7288

7389
pub(crate) fn get_send_session(&self, pj_url: &Url) -> Result<Option<Sender>> {
7490
let send_tree = self.0.open_tree("send_sessions")?;
75-
if let Some(val) = send_tree.get(pj_url.to_string())? {
91+
if let Some(val) = send_tree.get(pj_url.as_str())? {
7692
let session: Sender = serde_json::from_slice(&val).map_err(Error::Deserialize)?;
7793
Ok(Some(session))
7894
} else {
@@ -82,7 +98,7 @@ impl Database {
8298

8399
pub(crate) fn clear_send_session(&self, pj_url: &Url) -> Result<()> {
84100
let send_tree: Tree = self.0.open_tree("send_sessions")?;
85-
send_tree.remove(pj_url.to_string())?;
101+
send_tree.remove(pj_url.as_str())?;
86102
send_tree.flush()?;
87103
Ok(())
88104
}

payjoin/src/send/multiparty/mod.rs

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::fmt::{self, Display};
2+
13
use bitcoin::{FeeRate, Psbt};
24
use error::{
35
CreateRequestError, FinalizeResponseError, FinalizedError, InternalCreateRequestError,
@@ -11,8 +13,8 @@ use super::{serialize_url, AdditionalFeeContribution, BuildSenderError, Internal
1113
use crate::hpke::decrypt_message_b;
1214
use crate::ohttp::ohttp_decapsulate;
1315
use crate::output_substitution::OutputSubstitution;
14-
use crate::receive::ImplementationError;
15-
use crate::send::v2::V2PostContext;
16+
use crate::persist::{self, Persister};
17+
use crate::send::v2::{ImplementationError, V2PostContext};
1618
use crate::uri::UrlExt;
1719
use crate::{PjUri, Request};
1820

@@ -23,17 +25,58 @@ pub struct SenderBuilder<'a>(v2::SenderBuilder<'a>);
2325

2426
impl<'a> SenderBuilder<'a> {
2527
pub fn new(psbt: Psbt, uri: PjUri<'a>) -> Self { Self(v2::SenderBuilder::new(psbt, uri)) }
26-
pub fn build_recommended(self, min_fee_rate: FeeRate) -> Result<Sender, BuildSenderError> {
27-
let v2 = v2::SenderBuilder::new(self.0 .0.psbt, self.0 .0.uri)
28-
.build_recommended(min_fee_rate)?;
29-
Ok(Sender(v2))
28+
29+
pub fn build_recommended(self, min_fee_rate: FeeRate) -> Result<NewSender, BuildSenderError> {
30+
let sender = self.0.build_recommended(min_fee_rate)?;
31+
Ok(NewSender(sender))
3032
}
3133
}
3234

35+
pub struct NewSender(v2::NewSender);
36+
37+
impl NewSender {
38+
pub fn persist<P: Persister<Sender>>(
39+
&self,
40+
persister: &mut P,
41+
) -> Result<P::Token, ImplementationError> {
42+
let sender =
43+
Sender(v2::Sender { v1: self.0.v1.clone(), reply_key: self.0.reply_key.clone() });
44+
persister.save(sender).map_err(ImplementationError::from)
45+
}
46+
}
47+
48+
#[derive(Clone, Debug, PartialEq, Eq)]
49+
pub struct SenderToken(Url);
50+
51+
impl Display for SenderToken {
52+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) }
53+
}
54+
55+
impl From<Sender> for SenderToken {
56+
fn from(sender: Sender) -> Self { SenderToken(sender.0.endpoint().clone()) }
57+
}
58+
59+
impl AsRef<[u8]> for SenderToken {
60+
fn as_ref(&self) -> &[u8] { self.0.as_str().as_bytes() }
61+
}
62+
3363
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
3464
pub struct Sender(v2::Sender);
3565

66+
impl persist::Value for Sender {
67+
type Key = SenderToken;
68+
69+
fn key(&self) -> Self::Key { SenderToken(self.0.endpoint().clone()) }
70+
}
71+
3672
impl Sender {
73+
pub fn load<P: Persister<Sender>>(
74+
token: P::Token,
75+
persister: &P,
76+
) -> Result<Self, ImplementationError> {
77+
let sender = persister.load(token).map_err(ImplementationError::from)?;
78+
Ok(sender)
79+
}
3780
pub fn extract_v2(
3881
&self,
3982
ohttp_relay: Url,

payjoin/src/send/v2/error.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use core::fmt;
22

33
use crate::uri::url_ext::ParseReceiverPubkeyParamError;
44

5+
pub type ImplementationError = Box<dyn std::error::Error + Send + Sync>;
56
/// Error returned when request could not be created.
67
///
78
/// This error can currently only happen due to programmer mistake.

payjoin/src/send/v2/mod.rs

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
//! [`bitmask-core`](https://github.com/diba-io/bitmask-core) BDK integration. Bring your own
2222
//! wallet and http client.
2323
24+
use std::fmt::{self, Display};
25+
2426
use bitcoin::hashes::{sha256, Hash};
25-
pub use error::{CreateRequestError, EncapsulationError};
27+
pub use error::{CreateRequestError, EncapsulationError, ImplementationError};
2628
use error::{InternalCreateRequestError, InternalEncapsulationError};
2729
use ohttp::ClientResponse;
2830
use serde::{Deserialize, Serialize};
@@ -32,6 +34,7 @@ use super::error::BuildSenderError;
3234
use super::*;
3335
use crate::hpke::{decrypt_message_b, encrypt_message_a, HpkeSecretKey};
3436
use crate::ohttp::{ohttp_decapsulate, ohttp_encapsulate};
37+
use crate::persist::{Persister, Value};
3538
use crate::send::v1;
3639
use crate::uri::{ShortId, UrlExt};
3740
use crate::{HpkeKeyPair, HpkePublicKey, IntoUrl, OhttpKeys, PjUri, Request};
@@ -64,11 +67,12 @@ impl<'a> SenderBuilder<'a> {
6467
// The minfeerate parameter is set if the contribution is available in change.
6568
//
6669
// This method fails if no recommendation can be made or if the PSBT is malformed.
67-
pub fn build_recommended(self, min_fee_rate: FeeRate) -> Result<Sender, BuildSenderError> {
68-
Ok(Sender {
70+
pub fn build_recommended(self, min_fee_rate: FeeRate) -> Result<NewSender, BuildSenderError> {
71+
let sender = NewSender {
6972
v1: self.0.build_recommended(min_fee_rate)?,
7073
reply_key: HpkeKeyPair::gen_keypair().0,
71-
})
74+
};
75+
Ok(sender)
7276
}
7377

7478
/// Offer the receiver contribution to pay for his input.
@@ -90,16 +94,17 @@ impl<'a> SenderBuilder<'a> {
9094
change_index: Option<usize>,
9195
min_fee_rate: FeeRate,
9296
clamp_fee_contribution: bool,
93-
) -> Result<Sender, BuildSenderError> {
94-
Ok(Sender {
97+
) -> Result<NewSender, BuildSenderError> {
98+
let sender = NewSender {
9599
v1: self.0.build_with_additional_fee(
96100
max_fee_contribution,
97101
change_index,
98102
min_fee_rate,
99103
clamp_fee_contribution,
100104
)?,
101105
reply_key: HpkeKeyPair::gen_keypair().0,
102-
})
106+
};
107+
Ok(sender)
103108
}
104109

105110
/// Perform Payjoin without incentivizing the payee to cooperate.
@@ -109,11 +114,28 @@ impl<'a> SenderBuilder<'a> {
109114
pub fn build_non_incentivizing(
110115
self,
111116
min_fee_rate: FeeRate,
112-
) -> Result<Sender, BuildSenderError> {
113-
Ok(Sender {
117+
) -> Result<NewSender, BuildSenderError> {
118+
let sender = NewSender {
114119
v1: self.0.build_non_incentivizing(min_fee_rate)?,
115120
reply_key: HpkeKeyPair::gen_keypair().0,
116-
})
121+
};
122+
Ok(sender)
123+
}
124+
}
125+
126+
#[derive(Debug)]
127+
pub struct NewSender {
128+
pub(crate) v1: v1::Sender,
129+
pub(crate) reply_key: HpkeSecretKey,
130+
}
131+
132+
impl NewSender {
133+
pub fn persist<P: Persister<Sender>>(
134+
&self,
135+
persister: &mut P,
136+
) -> Result<P::Token, ImplementationError> {
137+
let sender = Sender { v1: self.v1.clone(), reply_key: self.reply_key.clone() };
138+
Ok(persister.save(sender)?)
117139
}
118140
}
119141

@@ -125,7 +147,35 @@ pub struct Sender {
125147
pub(crate) reply_key: HpkeSecretKey,
126148
}
127149

150+
/// Opaque key type for the sender
151+
#[derive(Debug, Clone, PartialEq, Eq)]
152+
pub struct SenderToken(Url);
153+
154+
impl Display for SenderToken {
155+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) }
156+
}
157+
158+
impl From<Sender> for SenderToken {
159+
fn from(sender: Sender) -> Self { SenderToken(sender.endpoint().clone()) }
160+
}
161+
162+
impl AsRef<[u8]> for SenderToken {
163+
fn as_ref(&self) -> &[u8] { self.0.as_str().as_bytes() }
164+
}
165+
166+
impl Value for Sender {
167+
type Key = SenderToken;
168+
169+
fn key(&self) -> Self::Key { SenderToken(self.endpoint().clone()) }
170+
}
171+
128172
impl Sender {
173+
pub fn load<P: Persister<Sender>>(
174+
token: P::Token,
175+
persister: &P,
176+
) -> Result<Self, ImplementationError> {
177+
persister.load(token).map_err(ImplementationError::from)
178+
}
129179
/// Extract serialized V1 Request and Context from a Payjoin Proposal
130180
pub fn extract_v1(&self) -> (Request, v1::V1Context) { self.v1.extract_v1() }
131181

0 commit comments

Comments
 (0)