Skip to content
Closed
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion payjoin-cli/src/app/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ fn handle_subcommands(config: Builder, cli: &Cli) -> Result<Builder, ConfigError
Ok(config)
}
#[cfg(feature = "v2")]
Commands::Resume => Ok(config),
Commands::Resume { .. } => Ok(config),
}
}

Expand Down
8 changes: 4 additions & 4 deletions payjoin-cli/src/app/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,12 @@ impl AsyncBitcoinRpc {
.basic_auth(&self.username, Some(&self.password));

let response =
request.send().await.with_context(|| format!("RPC '{}': connection failed", method))?;
request.send().await.with_context(|| format!("RPC '{method}': connection failed"))?;

let json = response
.json::<RpcResponse<T>>()
.await
.with_context(|| format!("RPC '{}': invalid response", method))?;
.with_context(|| format!("RPC '{method}': invalid response"))?;

match json {
RpcResponse::Success { result, .. } => Ok(result),
Expand Down Expand Up @@ -291,7 +291,7 @@ mod tests {
.await
.expect_err("Should fail due to invalid address");
let error_msg = error.to_string();
println!("{}", error_msg);
println!("{error_msg}");

assert_rpc_error_format(
&error_msg,
Expand Down Expand Up @@ -321,7 +321,7 @@ mod tests {
.await
.expect_err("Should fail due to insufficient funds");
let error_msg = error.to_string();
println!("{}", error_msg);
println!("{error_msg}");

assert_rpc_error_format(
&error_msg,
Expand Down
1 change: 1 addition & 0 deletions payjoin-cli/src/app/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ impl AppTrait for App {
ohttp_keys,
None,
Some(amount),
self.config.max_fee_rate,
)?
.save(&persister)?;
println!("Receive session established");
Expand Down
2 changes: 1 addition & 1 deletion payjoin-cli/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ pub enum Commands {
},
/// Resume pending payjoins (BIP77/v2 only)
#[cfg(feature = "v2")]
Resume,
Resume {},
}

pub fn parse_amount_in_sat(s: &str) -> Result<Amount, ParseAmountError> {
Expand Down
2 changes: 1 addition & 1 deletion payjoin-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ async fn main() -> Result<()> {
app.receive_payjoin(*amount).await?;
}
#[cfg(feature = "v2")]
Commands::Resume => {
Commands::Resume { .. } => {
app.resume_payjoins().await?;
}
};
Expand Down
2 changes: 1 addition & 1 deletion payjoin-ffi/dart/test/test_payjoin_integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ payjoin.Initialized create_receiver_context(
payjoin.OhttpKeys ohttp_keys,
InMemoryReceiverPersister persister) {
var receiver = payjoin.UninitializedReceiver()
.createSession(address, directory, ohttp_keys, null, null)
.createSession(address, directory, ohttp_keys, null, null, null)
.save(persister);
return receiver;
}
Expand Down
2 changes: 2 additions & 0 deletions payjoin-ffi/dart/test/test_payjoin_unit_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ void main() {
payjoin.OhttpKeys.fromString(
"OH1QYPM5JXYNS754Y4R45QWE336QFX6ZR8DQGVQCULVZTV20TFVEYDMFQC"),
null,
null,
null)
.save(persister);
final result = payjoin.replayReceiverEventLog(persister);
Expand All @@ -128,6 +129,7 @@ void main() {
payjoin.OhttpKeys.fromString(
"OH1QYPM5JXYNS754Y4R45QWE336QFX6ZR8DQGVQCULVZTV20TFVEYDMFQC"),
null,
null,
null)
.save(receiver_persister);
var uri = receiver.pjUri();
Expand Down
6 changes: 3 additions & 3 deletions payjoin-ffi/python/test/test_payjoin_integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ async def process_receiver_proposal(self, receiver: ReceiveSession, recv_persist
raise Exception(f"Unknown receiver state: {receiver}")

def create_receiver_context(self, receiver_address: bitcoinffi.Address, directory: Url, ohttp_keys: OhttpKeys, recv_persister: InMemoryReceiverSessionEventLog) -> Initialized:
receiver = UninitializedReceiver().create_session(address=receiver_address, directory=directory.as_string(), ohttp_keys=ohttp_keys, expire_after=None, amount=None).save(recv_persister)
receiver = UninitializedReceiver().create_session(address=receiver_address, directory=directory.as_string(), ohttp_keys=ohttp_keys, expire_after=None, amount=None, max_fee_rate_sat_per_vb=10).save(recv_persister)
return receiver

async def retrieve_receiver_proposal(self, receiver: Initialized, recv_persister: InMemoryReceiverSessionEventLog, ohttp_relay: Url):
Expand Down Expand Up @@ -124,9 +124,9 @@ async def process_wants_outputs(self, proposal: WantsOutputs, recv_persister: In
async def process_wants_inputs(self, proposal: WantsInputs, recv_persister: InMemoryReceiverSessionEventLog):
provisional_proposal = proposal.contribute_inputs(get_inputs(self.receiver)).commit_inputs().save(recv_persister)
return await self.process_wants_fee_range(provisional_proposal, recv_persister)

async def process_wants_fee_range(self, proposal: WantsFeeRange, recv_persister: InMemoryReceiverSessionEventLog):
provisional_proposal = proposal.apply_fee_range(1, 10).save(recv_persister)
provisional_proposal = proposal.apply_fee_range(1, None).save(recv_persister)
return await self.process_provisional_proposal(provisional_proposal, recv_persister)

async def process_provisional_proposal(self, proposal: ProvisionalProposal, recv_persister: InMemoryReceiverSessionEventLog):
Expand Down
6 changes: 4 additions & 2 deletions payjoin-ffi/python/test/test_payjoin_unit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ def test_receiver_persistence(self):
"https://example.com",
payjoin.OhttpKeys.from_string("OH1QYPM5JXYNS754Y4R45QWE336QFX6ZR8DQGVQCULVZTV20TFVEYDMFQC"),
None,
None
None,
None,
).save(persister)
result = payjoin.payjoin_ffi.replay_receiver_event_log(persister)
self.assertTrue(result.state().is_INITIALIZED())
Expand Down Expand Up @@ -85,7 +86,8 @@ def test_sender_persistence(self):
"https://example.com",
payjoin.OhttpKeys.from_string("OH1QYPM5JXYNS754Y4R45QWE336QFX6ZR8DQGVQCULVZTV20TFVEYDMFQC"),
None,
None
None,
None,
).save(persister)
uri = receiver.pj_uri()

Expand Down
6 changes: 4 additions & 2 deletions payjoin-ffi/src/receive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,16 +266,18 @@ impl UninitializedReceiver {
ohttp_keys: Arc<OhttpKeys>,
expire_after: Option<u64>,
amount: Option<u64>,
max_fee_rate_sat_per_vb: Option<u64>,
) -> Result<InitialReceiveTransition, IntoUrlError> {
payjoin::receive::v2::Receiver::create_session(
(*address).clone().into(),
directory,
(*ohttp_keys).clone().into(),
expire_after.map(Duration::from_secs),
amount.map(payjoin::bitcoin::Amount::from_sat),
max_fee_rate_sat_per_vb.and_then(FeeRate::from_sat_per_vb),
)
.map(|receiver| InitialReceiveTransition(Arc::new(RwLock::new(Some(receiver)))))
.map_err(IntoUrlError::from)
.map_err(Into::into)
.map(|session| InitialReceiveTransition(Arc::new(RwLock::new(Some(session)))))
}
}

Expand Down
103 changes: 103 additions & 0 deletions payjoin/src/core/receive/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ pub struct SessionContext {
amount: Option<Amount>,
s: HpkeKeyPair,
e: Option<HpkePublicKey>,
max_fee_rate: FeeRate,
}

impl SessionContext {
Expand Down Expand Up @@ -268,8 +269,10 @@ impl Receiver<UninitializedReceiver> {
ohttp_keys: OhttpKeys,
expire_after: Option<Duration>,
amount: Option<Amount>,
max_fee_rate: Option<FeeRate>,
) -> Result<NextStateTransition<SessionEvent, Receiver<Initialized>>, IntoUrlError> {
let directory = directory.into_url()?;

let session_context = SessionContext {
address,
directory,
Expand All @@ -279,6 +282,7 @@ impl Receiver<UninitializedReceiver> {
s: HpkeKeyPair::gen_keypair(),
e: None,
amount,
max_fee_rate: max_fee_rate.unwrap_or(FeeRate::BROADCAST_MIN),
};
Ok(NextStateTransition::success(
SessionEvent::Created(session_context.clone()),
Expand Down Expand Up @@ -892,6 +896,9 @@ impl Receiver<WantsFeeRange> {
min_fee_rate: Option<FeeRate>,
max_effective_fee_rate: Option<FeeRate>,
) -> MaybeFatalTransition<SessionEvent, Receiver<ProvisionalProposal>, ReplyableError> {
let max_effective_fee_rate =
max_effective_fee_rate.or(Some(self.state.session_context.max_fee_rate));

let inner = match self.state.v1.apply_fee_range(min_fee_rate, max_effective_fee_rate) {
Ok(inner) => inner,
Err(e) => {
Expand Down Expand Up @@ -1116,6 +1123,7 @@ pub mod test {
ohttp::KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).expect("valid key config"),
),
expiry: SystemTime::now() + Duration::from_secs(60),
max_fee_rate: FeeRate::BROADCAST_MIN,
s: HpkeKeyPair::gen_keypair(),
e: None,
amount: None,
Expand All @@ -1141,6 +1149,23 @@ pub mod test {
}
}

fn create_wants_fee_range_with_context(context: SessionContext) -> WantsFeeRange {
let unchecked = v1::test::unchecked_proposal_from_test_vector();
let wants_outputs = unchecked
.assume_interactive_receiver()
.check_inputs_not_owned(&mut |_| Ok(false))
.expect("No inputs should be owned")
.check_no_inputs_seen_before(&mut |_| Ok(false))
.expect("No inputs should be seen before")
.identify_receiver_outputs(&mut |_| Ok(true))
.expect("Receiver output should be identified");

let wants_inputs = wants_outputs.commit_outputs();
let v1_wants_fee_range = wants_inputs.commit_inputs();

WantsFeeRange { v1: v1_wants_fee_range, session_context: context }
}

#[test]
fn test_v2_mutable_receiver_state_closures() {
let persister = NoopSessionPersister::default();
Expand Down Expand Up @@ -1361,6 +1386,7 @@ pub mod test {
SHARED_CONTEXT.ohttp_keys.clone(),
None,
None,
None,
)
.expect("constructor on test vector should not fail")
.save(&noop_persister)
Expand All @@ -1375,6 +1401,83 @@ pub mod test {

#[test]
fn test_v2_pj_uri() {
let context = SHARED_CONTEXT.clone();
let uri = pj_uri(&context, OutputSubstitution::Disabled);
assert!(!uri.to_string().is_empty());
}

#[test]
fn test_session_creation_with_max_fee_rate() {
let custom_fee_rate = FeeRate::from_sat_per_vb_unchecked(5);
let address = Address::from_str("tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4")
.expect("valid address")
.assume_checked();

let session = Receiver::create_session(
address,
EXAMPLE_URL.clone(),
SHARED_CONTEXT.ohttp_keys.clone(),
None,
None,
Some(custom_fee_rate),
);

let noop_persister = NoopSessionPersister::default();
let session = session
.expect("Session creation should not fail")
.save(&noop_persister)
.expect("Noop persister shouldn't fail");

assert_eq!(session.context.max_fee_rate, custom_fee_rate);
}

#[test]
fn test_apply_fee_range_session_max_overrides_parameter() {
let session_max = FeeRate::from_sat_per_vb_unchecked(5);
let context = SessionContext { max_fee_rate: session_max, ..SHARED_CONTEXT.clone() };
let receiver = Receiver { state: create_wants_fee_range_with_context(context) };

let higher_rate = FeeRate::from_sat_per_vb_unchecked(10);

let result = receiver
.apply_fee_range(None, Some(higher_rate))
.save(&NoopSessionPersister::default())
.expect("Noop persister shouldn't fail");

let payjoin_psbt = &result.state.psbt_context.payjoin_psbt;
let payjoin_fee = payjoin_psbt.fee().expect("PSBT should have fee");
let actual_fee_rate =
payjoin_fee / payjoin_psbt.clone().extract_tx_unchecked_fee_rate().weight();

assert!(
actual_fee_rate <= session_max,
"Fee rate {actual_fee_rate} should be capped at session maximum {session_max} even when higher parameter rate {higher_rate} is provided"
);
}

#[test]
fn test_apply_fee_range_with_none_uses_session_max() {
let session_max = FeeRate::from_sat_per_vb_unchecked(7);
let context = SessionContext { max_fee_rate: session_max, ..SHARED_CONTEXT.clone() };
let receiver = Receiver { state: create_wants_fee_range_with_context(context) };

let result = receiver
.apply_fee_range(None, None)
.save(&NoopSessionPersister::default())
.expect("Noop persister shouldn't fail");
let payjoin_psbt = &result.state.psbt_context.payjoin_psbt;
let payjoin_fee = payjoin_psbt.fee().expect("PSBT should have fee");
let actual_fee_rate =
payjoin_fee / payjoin_psbt.clone().extract_tx_unchecked_fee_rate().weight();

assert!(
actual_fee_rate <= session_max,
"Fee rate {actual_fee_rate} should not exceed session maximum {session_max}"
);
}

#[test]
fn test_v2_pj_uri_with_output_substitution() {
let uri = Receiver { state: Initialized { context: SHARED_CONTEXT.clone() } }.pj_uri();
assert_ne!(uri.extras.pj_param.endpoint(), EXAMPLE_URL.clone());
assert_eq!(uri.extras.output_substitution, OutputSubstitution::Disabled);
Expand Down
13 changes: 7 additions & 6 deletions payjoin/src/core/send/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -593,19 +593,19 @@ mod test {
}

#[test]
fn test_v2_sender_builder() {
fn test_v2_sender_builder() -> Result<(), BoxError> {
let address = Address::from_str("2N47mmrWXsNBvQR6k78hWJoTji57zXwNcU7")
.expect("valid address")
.assume_checked();
let directory = EXAMPLE_URL.clone();
let ohttp_keys = OhttpKeys(
ohttp::KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).expect("valid key config"),
);
let pj_uri = Receiver::create_session(address.clone(), directory, ohttp_keys, None, None)
.expect("constructor on test vector should not fail")
.save(&NoopSessionPersister::default())
.expect("receiver should succeed")
.pj_uri();
let pj_uri =
Receiver::create_session(address.clone(), directory, ohttp_keys, None, None, None)?
.save(&NoopSessionPersister::default())
.expect("receiver should succeed")
.pj_uri();
let req_ctx = SenderBuilder::new(PARSED_ORIGINAL_PSBT.clone(), pj_uri.clone())
.build_recommended(FeeRate::BROADCAST_MIN)
.expect("build on test vector should succeed")
Expand Down Expand Up @@ -641,5 +641,6 @@ mod test {
.save(&NoopSessionPersister::default())
.expect("sender should succeed");
assert_eq!(req_ctx.state.psbt_ctx.output_substitution, OutputSubstitution::Disabled);
Ok(())
}
}
Loading
Loading