Skip to content

Commit affd9d9

Browse files
authored
Sender generic over typestate (#728)
This commit updates the `v2::Sender` to be generic over its typestate (e.g., `V2GetContext`, `V2PostContext`, etc.). The motivation is outlined in arminsabouri@c8b860d This commit renames the original `Sender` struct to `WithReplyKey`. This type now represents the v2::sender session once the HPKE context has been created. The `Sender` name is re-purposed for the new generic wrapper over typestate. For the UniFFI exported receiver we monomorphize the exported structs over a specific typestate. UniFFI doesn't support exporting generic structs, so we expose a concrete type to the FFI.
2 parents 5fe13e5 + 722bfa9 commit affd9d9

File tree

12 files changed

+189
-107
lines changed

12 files changed

+189
-107
lines changed

payjoin-cli/src/app/v2/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use payjoin::receive::v2::{
99
WithContext,
1010
};
1111
use payjoin::receive::{Error, ReplyableError};
12-
use payjoin::send::v2::{Sender, SenderBuilder};
12+
use payjoin::send::v2::{Sender, SenderBuilder, WithReplyKey};
1313
use payjoin::{ImplementationError, Uri};
1414
use tokio::sync::watch;
1515

@@ -137,7 +137,7 @@ impl AppTrait for App {
137137

138138
impl App {
139139
#[allow(clippy::incompatible_msrv)]
140-
async fn spawn_payjoin_sender(&self, mut req_ctx: Sender) -> Result<()> {
140+
async fn spawn_payjoin_sender(&self, mut req_ctx: Sender<WithReplyKey>) -> Result<()> {
141141
let mut interrupt = self.interrupt.clone();
142142
tokio::select! {
143143
res = self.long_poll_post(&mut req_ctx) => {
@@ -200,7 +200,7 @@ impl App {
200200
Ok(())
201201
}
202202

203-
async fn long_poll_post(&self, req_ctx: &mut Sender) -> Result<Psbt> {
203+
async fn long_poll_post(&self, req_ctx: &mut Sender<WithReplyKey>) -> Result<Psbt> {
204204
let ohttp_relay = self.unwrap_relay_or_else_fetch().await?;
205205

206206
match req_ctx.extract_v2(ohttp_relay.clone()) {

payjoin-cli/src/db/v2.rs

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

@@ -14,10 +14,13 @@ impl SenderPersister {
1414
pub fn new(db: Arc<Database>) -> Self { Self(db) }
1515
}
1616

17-
impl Persister<Sender> for SenderPersister {
17+
impl Persister<Sender<WithReplyKey>> for SenderPersister {
1818
type Token = SenderToken;
1919
type Error = crate::db::error::Error;
20-
fn save(&mut self, value: Sender) -> std::result::Result<SenderToken, Self::Error> {
20+
fn save(
21+
&mut self,
22+
value: Sender<WithReplyKey>,
23+
) -> std::result::Result<SenderToken, Self::Error> {
2124
let send_tree = self.0 .0.open_tree("send_sessions")?;
2225
let key = value.key();
2326
let value = serde_json::to_vec(&value).map_err(Error::Serialize)?;
@@ -26,7 +29,7 @@ impl Persister<Sender> for SenderPersister {
2629
Ok(key)
2730
}
2831

29-
fn load(&self, key: SenderToken) -> std::result::Result<Sender, Self::Error> {
32+
fn load(&self, key: SenderToken) -> std::result::Result<Sender<WithReplyKey>, Self::Error> {
3033
let send_tree = self.0 .0.open_tree("send_sessions")?;
3134
let value = send_tree.get(key.as_ref())?.ok_or(Error::NotFound(key.to_string()))?;
3235
serde_json::from_slice(&value).map_err(Error::Deserialize)
@@ -79,21 +82,23 @@ impl Database {
7982
Ok(())
8083
}
8184

82-
pub(crate) fn get_send_sessions(&self) -> Result<Vec<Sender>> {
85+
pub(crate) fn get_send_sessions(&self) -> Result<Vec<Sender<WithReplyKey>>> {
8386
let send_tree: Tree = self.0.open_tree("send_sessions")?;
8487
let mut sessions = Vec::new();
8588
for item in send_tree.iter() {
8689
let (_, value) = item?;
87-
let session: Sender = serde_json::from_slice(&value).map_err(Error::Deserialize)?;
90+
let session: Sender<WithReplyKey> =
91+
serde_json::from_slice(&value).map_err(Error::Deserialize)?;
8892
sessions.push(session);
8993
}
9094
Ok(sessions)
9195
}
9296

93-
pub(crate) fn get_send_session(&self, pj_url: &Url) -> Result<Option<Sender>> {
97+
pub(crate) fn get_send_session(&self, pj_url: &Url) -> Result<Option<Sender<WithReplyKey>>> {
9498
let send_tree = self.0.open_tree("send_sessions")?;
9599
if let Some(val) = send_tree.get(pj_url.as_str())? {
96-
let session: Sender = serde_json::from_slice(&val).map_err(Error::Deserialize)?;
100+
let session: Sender<WithReplyKey> =
101+
serde_json::from_slice(&val).map_err(Error::Deserialize)?;
97102
Ok(Some(session))
98103
} else {
99104
Ok(None)

payjoin-ffi/python/test/test_payjoin_integration_test.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,15 @@ def __init__(self):
5050
super().__init__()
5151
self.senders = {}
5252

53-
def save(self, sender: Sender) -> SenderToken:
53+
def save(self, sender: WithReplyKey) -> SenderToken:
5454
self.senders[str(sender.key())] = sender.to_json()
5555
return sender.key()
5656

57-
def load(self, token: SenderToken) -> Sender:
57+
def load(self, token: SenderToken) -> WithReplyKey:
5858
token = str(token)
5959
if token not in self.senders.keys():
6060
raise ValueError(f"Token not found: {token}")
61-
return Sender.from_json(self.senders[token])
61+
return WithReplyKey.from_json(self.senders[token])
6262

6363
class TestPayjoin(unittest.IsolatedAsyncioTestCase):
6464
@classmethod
@@ -106,7 +106,7 @@ async def test_integration_v2_to_v2(self):
106106
new_sender = SenderBuilder(psbt, pj_uri).build_recommended(1000)
107107
persister = InMemorySenderPersister()
108108
token = new_sender.persist(persister)
109-
req_ctx: Sender = Sender.load(token, persister)
109+
req_ctx: WithReplyKey = WithReplyKey.load(token, persister)
110110
request: RequestV2PostContext = req_ctx.extract_v2(ohttp_relay.as_string())
111111
response = await agent.post(
112112
url=request.request.url.as_string(),

payjoin-ffi/python/test/test_payjoin_unit_test.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,15 @@ class InMemorySenderPersister(payjoin.payjoin_ffi.SenderPersister):
6464
def __init__(self):
6565
self.senders = {}
6666

67-
def save(self, sender: payjoin.Sender) -> payjoin.SenderToken:
67+
def save(self, sender: payjoin.WithReplyKey) -> payjoin.SenderToken:
6868
self.senders[str(sender.key())] = sender.to_json()
6969
return sender.key()
7070

71-
def load(self, token: payjoin.SenderToken) -> payjoin.Sender:
71+
def load(self, token: payjoin.SenderToken) -> payjoin.WithReplyKey:
7272
token = str(token)
7373
if token not in self.senders.keys():
7474
raise ValueError(f"Token not found: {token}")
75-
return payjoin.Sender.from_json(self.senders[token])
75+
return payjoin.WithReplyKey.from_json(self.senders[token])
7676

7777
class TestSenderPersistence(unittest.TestCase):
7878
def test_sender_persistence(self):
@@ -93,7 +93,7 @@ def test_sender_persistence(self):
9393
psbt = "cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA="
9494
new_sender = payjoin.SenderBuilder(psbt, uri).build_recommended(1000)
9595
token = new_sender.persist(persister)
96-
payjoin.Sender.load(token, persister)
96+
payjoin.WithReplyKey.load(token, persister)
9797

9898
if __name__ == "__main__":
9999
unittest.main()

payjoin-ffi/src/send/mod.rs

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ impl SenderBuilder {
2929
/// Prepare an HTTP request and request context to process the response
3030
///
3131
/// Call [`SenderBuilder::build_recommended()`] or other `build` methods
32-
/// to create a [`Sender`]
32+
/// to create a [`WithReplyKey`]
3333
pub fn new(psbt: String, uri: PjUri) -> Result<Self, BuildSenderError> {
3434
let psbt = payjoin::bitcoin::psbt::Psbt::from_str(psbt.as_str())?;
3535
Ok(payjoin::send::v2::SenderBuilder::new(psbt, uri.into()).into())
@@ -114,7 +114,7 @@ impl From<payjoin::send::v2::NewSender> for NewSender {
114114
}
115115

116116
impl NewSender {
117-
pub fn persist<P: Persister<payjoin::send::v2::Sender>>(
117+
pub fn persist<P: Persister<payjoin::send::v2::Sender<payjoin::send::v2::WithReplyKey>>>(
118118
&self,
119119
persister: &mut P,
120120
) -> Result<P::Token, ImplementationError> {
@@ -123,18 +123,20 @@ impl NewSender {
123123
}
124124

125125
#[derive(Clone)]
126-
pub struct Sender(payjoin::send::v2::Sender);
126+
pub struct WithReplyKey(payjoin::send::v2::Sender<payjoin::send::v2::WithReplyKey>);
127127

128-
impl From<payjoin::send::v2::Sender> for Sender {
129-
fn from(value: payjoin::send::v2::Sender) -> Self { Self(value) }
128+
impl From<payjoin::send::v2::Sender<payjoin::send::v2::WithReplyKey>> for WithReplyKey {
129+
fn from(value: payjoin::send::v2::Sender<payjoin::send::v2::WithReplyKey>) -> Self {
130+
Self(value)
131+
}
130132
}
131133

132-
impl From<Sender> for payjoin::send::v2::Sender {
133-
fn from(value: Sender) -> Self { value.0 }
134+
impl From<WithReplyKey> for payjoin::send::v2::Sender<payjoin::send::v2::WithReplyKey> {
135+
fn from(value: WithReplyKey) -> Self { value.0 }
134136
}
135137

136-
impl Sender {
137-
pub fn load<P: Persister<payjoin::send::v2::Sender>>(
138+
impl WithReplyKey {
139+
pub fn load<P: Persister<payjoin::send::v2::Sender<payjoin::send::v2::WithReplyKey>>>(
138140
token: P::Token,
139141
persister: &P,
140142
) -> Result<Self, ImplementationError> {
@@ -164,7 +166,9 @@ impl Sender {
164166
}
165167

166168
pub fn from_json(json: &str) -> Result<Self, SerdeJsonError> {
167-
serde_json::from_str::<payjoin::send::v2::Sender>(json).map_err(Into::into).map(Into::into)
169+
serde_json::from_str::<payjoin::send::v2::Sender<payjoin::send::v2::WithReplyKey>>(json)
170+
.map_err(Into::into)
171+
.map(Into::into)
168172
}
169173

170174
pub fn key(&self) -> SenderToken { self.0.key() }
@@ -190,35 +194,43 @@ impl V1Context {
190194
}
191195
}
192196

193-
pub struct V2PostContext(Mutex<Option<payjoin::send::v2::V2PostContext>>);
197+
pub struct V2PostContext(
198+
Mutex<Option<payjoin::send::v2::Sender<payjoin::send::v2::V2PostContext>>>,
199+
);
194200

195201
impl V2PostContext {
196202
/// Decodes and validates the response.
197203
/// Call this method with response from receiver to continue BIP-??? flow. A successful response can either be None if the relay has not response yet or Some(Psbt).
198204
/// If the response is some valid PSBT you should sign and broadcast.
199205
pub fn process_response(&self, response: &[u8]) -> Result<V2GetContext, EncapsulationError> {
200-
<&V2PostContext as Into<payjoin::send::v2::V2PostContext>>::into(self)
201-
.process_response(response)
202-
.map(Into::into)
203-
.map_err(Into::into)
206+
<&V2PostContext as Into<payjoin::send::v2::Sender<payjoin::send::v2::V2PostContext>>>::into(
207+
self,
208+
)
209+
.process_response(response)
210+
.map(Into::into)
211+
.map_err(Into::into)
204212
}
205213
}
206214

207-
impl From<&V2PostContext> for payjoin::send::v2::V2PostContext {
215+
impl From<&V2PostContext> for payjoin::send::v2::Sender<payjoin::send::v2::V2PostContext> {
208216
fn from(value: &V2PostContext) -> Self {
209217
let mut data_guard = value.0.lock().unwrap();
210218
Option::take(&mut *data_guard).expect("ContextV2 moved out of memory")
211219
}
212220
}
213221

214-
impl From<payjoin::send::v2::V2PostContext> for V2PostContext {
215-
fn from(value: payjoin::send::v2::V2PostContext) -> Self { Self(Mutex::new(Some(value))) }
222+
impl From<payjoin::send::v2::Sender<payjoin::send::v2::V2PostContext>> for V2PostContext {
223+
fn from(value: payjoin::send::v2::Sender<payjoin::send::v2::V2PostContext>) -> Self {
224+
Self(Mutex::new(Some(value)))
225+
}
216226
}
217227

218-
pub struct V2GetContext(payjoin::send::v2::V2GetContext);
228+
pub struct V2GetContext(payjoin::send::v2::Sender<payjoin::send::v2::V2GetContext>);
219229

220-
impl From<payjoin::send::v2::V2GetContext> for V2GetContext {
221-
fn from(value: payjoin::send::v2::V2GetContext) -> Self { Self(value) }
230+
impl From<payjoin::send::v2::Sender<payjoin::send::v2::V2GetContext>> for V2GetContext {
231+
fn from(value: payjoin::send::v2::Sender<payjoin::send::v2::V2GetContext>) -> Self {
232+
Self(value)
233+
}
222234
}
223235

224236
impl V2GetContext {

payjoin-ffi/src/send/uni.rs

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ impl SenderBuilder {
2222
/// Prepare an HTTP request and request context to process the response
2323
///
2424
/// Call [`SenderBuilder::build_recommended()`] or other `build` methods
25-
/// to create a [`Sender`]
25+
/// to create a [`WithReplyKey`]
2626
#[uniffi::constructor]
2727
pub fn new(psbt: String, uri: Arc<PjUri>) -> Result<Self, BuildSenderError> {
2828
super::SenderBuilder::new(psbt, (*uri).clone()).map(Into::into)
@@ -107,24 +107,24 @@ impl NewSender {
107107
}
108108

109109
#[derive(Clone, uniffi::Object)]
110-
pub struct Sender(super::Sender);
110+
pub struct WithReplyKey(super::WithReplyKey);
111111

112-
impl From<super::Sender> for Sender {
113-
fn from(value: super::Sender) -> Self { Self(value) }
112+
impl From<super::WithReplyKey> for WithReplyKey {
113+
fn from(value: super::WithReplyKey) -> Self { Self(value) }
114114
}
115115

116-
impl From<Sender> for super::Sender {
117-
fn from(value: Sender) -> Self { value.0 }
116+
impl From<WithReplyKey> for super::WithReplyKey {
117+
fn from(value: WithReplyKey) -> Self { value.0 }
118118
}
119119

120120
#[uniffi::export]
121-
impl Sender {
121+
impl WithReplyKey {
122122
#[uniffi::constructor]
123123
pub fn load(
124124
token: Arc<SenderToken>,
125125
persister: Arc<dyn SenderPersister>,
126126
) -> Result<Self, ImplementationError> {
127-
Ok(super::Sender::from(
127+
Ok(super::WithReplyKey::from(
128128
(*persister.load(token).map_err(|e| ImplementationError::from(e.to_string()))?).clone(),
129129
)
130130
.into())
@@ -154,7 +154,7 @@ impl Sender {
154154

155155
#[uniffi::constructor]
156156
pub fn from_json(json: &str) -> Result<Self, SerdeJsonError> {
157-
super::Sender::from_json(json).map(Into::into)
157+
super::WithReplyKey::from_json(json).map(Into::into)
158158
}
159159

160160
pub fn key(&self) -> SenderToken { self.0.key().into() }
@@ -248,8 +248,8 @@ impl V2GetContext {
248248

249249
#[uniffi::export(with_foreign)]
250250
pub trait SenderPersister: Send + Sync {
251-
fn save(&self, sender: Arc<Sender>) -> Result<Arc<SenderToken>, ForeignError>;
252-
fn load(&self, token: Arc<SenderToken>) -> Result<Arc<Sender>, ForeignError>;
251+
fn save(&self, sender: Arc<WithReplyKey>) -> Result<Arc<SenderToken>, ForeignError>;
252+
fn load(&self, token: Arc<SenderToken>) -> Result<Arc<WithReplyKey>, ForeignError>;
253253
}
254254

255255
// The adapter to use the save and load callbacks
@@ -262,16 +262,24 @@ impl CallbackPersisterAdapter {
262262
}
263263

264264
// Implement the Persister trait for the adapter
265-
impl payjoin::persist::Persister<payjoin::send::v2::Sender> for CallbackPersisterAdapter {
265+
impl payjoin::persist::Persister<payjoin::send::v2::Sender<payjoin::send::v2::WithReplyKey>>
266+
for CallbackPersisterAdapter
267+
{
266268
type Token = SenderToken; // Define the token type
267269
type Error = ForeignError; // Define the error type
268270

269-
fn save(&mut self, sender: payjoin::send::v2::Sender) -> Result<Self::Token, Self::Error> {
270-
let sender = Sender(super::Sender::from(sender));
271+
fn save(
272+
&mut self,
273+
sender: payjoin::send::v2::Sender<payjoin::send::v2::WithReplyKey>,
274+
) -> Result<Self::Token, Self::Error> {
275+
let sender = WithReplyKey(super::WithReplyKey::from(sender));
271276
self.callback_persister.save(sender.into()).map(|token| (*token).clone())
272277
}
273278

274-
fn load(&self, token: Self::Token) -> Result<payjoin::send::v2::Sender, Self::Error> {
279+
fn load(
280+
&self,
281+
token: Self::Token,
282+
) -> Result<payjoin::send::v2::Sender<payjoin::send::v2::WithReplyKey>, Self::Error> {
275283
// Use the callback to load the sender
276284
self.callback_persister.load(token.into()).map(|sender| (*sender).clone().0 .0)
277285
}
@@ -285,8 +293,10 @@ impl std::fmt::Display for SenderToken {
285293
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) }
286294
}
287295

288-
impl From<payjoin::send::v2::Sender> for SenderToken {
289-
fn from(value: payjoin::send::v2::Sender) -> Self { SenderToken(value.into()) }
296+
impl From<payjoin::send::v2::Sender<payjoin::send::v2::WithReplyKey>> for SenderToken {
297+
fn from(value: payjoin::send::v2::Sender<payjoin::send::v2::WithReplyKey>) -> Self {
298+
SenderToken(value.into())
299+
}
290300
}
291301

292302
impl From<payjoin::send::v2::SenderToken> for SenderToken {

payjoin-ffi/tests/bdk_integration_test.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ mod v2 {
220220
use bdk::wallet::AddressIndex;
221221
use bitcoin_ffi::{Address, Network};
222222
use payjoin_ffi::receive::{NewReceiver, PayjoinProposal, UncheckedProposal, WithContext};
223-
use payjoin_ffi::send::{Sender, SenderBuilder};
223+
use payjoin_ffi::send::{WithReplyKey, SenderBuilder};
224224
use payjoin_ffi::uri::Uri;
225225
use payjoin_ffi::{NoopPersister, Request};
226226
use payjoin_test_utils::TestServices;
@@ -288,7 +288,7 @@ mod v2 {
288288
let new_sender = SenderBuilder::new(psbt.to_string(), pj_uri)?
289289
.build_recommended(payjoin::bitcoin::FeeRate::BROADCAST_MIN.to_sat_per_kwu())?;
290290
let sender_token = new_sender.persist(&mut NoopPersister)?;
291-
let req_ctx = Sender::load(sender_token, &NoopPersister)?;
291+
let req_ctx = WithReplyKey::load(sender_token, &NoopPersister)?;
292292
let (request, context) = req_ctx.extract_v2(ohttp_relay.to_owned().into())?;
293293
let response = agent
294294
.post(request.url.as_string())

0 commit comments

Comments
 (0)