diff --git a/Cargo.lock b/Cargo.lock index 2b2ac74..28e41bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2991,8 +2991,8 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "paykit-lib" -version = "0.1.0-rc39" -source = "git+https://github.com/pubky/paykit-rs.git?rev=52a852995bfc457b78d32f5a45f6741766a89bba#52a852995bfc457b78d32f5a45f6741766a89bba" +version = "0.1.0-rc43" +source = "git+https://github.com/pubky/paykit-rs.git?rev=6b241878a9bba5cecea919c0298c3f90624be6ff#6b241878a9bba5cecea919c0298c3f90624be6ff" dependencies = [ "anyhow", "base64", @@ -3010,8 +3010,8 @@ dependencies = [ [[package]] name = "paykit-sdk" -version = "0.1.0-rc39" -source = "git+https://github.com/pubky/paykit-rs.git?rev=52a852995bfc457b78d32f5a45f6741766a89bba#52a852995bfc457b78d32f5a45f6741766a89bba" +version = "0.1.0-rc43" +source = "git+https://github.com/pubky/paykit-rs.git?rev=6b241878a9bba5cecea919c0298c3f90624be6ff#6b241878a9bba5cecea919c0298c3f90624be6ff" dependencies = [ "anyhow", "async-trait", @@ -5828,7 +5828,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8776b94..b832d53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,8 +24,8 @@ hkdf = "0.12" hmac = "0.12" humantime-serde = "1" locks-core = { git = "https://github.com/pubky/locks.git", rev = "df5ea1b6d8dcdec3a9b5a915c3f57bca69d75c8a" } -paykit-lib = { git = "https://github.com/pubky/paykit-rs.git", rev = "52a852995bfc457b78d32f5a45f6741766a89bba" } -paykit-sdk = { git = "https://github.com/pubky/paykit-rs.git", rev = "52a852995bfc457b78d32f5a45f6741766a89bba" } +paykit-lib = { git = "https://github.com/pubky/paykit-rs.git", rev = "6b241878a9bba5cecea919c0298c3f90624be6ff" } +paykit-sdk = { git = "https://github.com/pubky/paykit-rs.git", rev = "6b241878a9bba5cecea919c0298c3f90624be6ff" } pubky = "=0.8.0" pubky-testnet = "=0.8.0" prometheus-client = "0.25" diff --git a/paykit-server-e2e/tests/common/mod.rs b/paykit-server-e2e/tests/common/mod.rs index e79e9ac..09126fd 100644 --- a/paykit-server-e2e/tests/common/mod.rs +++ b/paykit-server-e2e/tests/common/mod.rs @@ -37,7 +37,7 @@ pub fn payment_intent(reader: &ReaderPubky) -> DeliveryIntentV1 { &marker(), PaykitReceiverPath::new("paykit/server").unwrap(), &PaymentRequestTerms { - amount: PaymentAmount::new("0.00000100", "BTC").unwrap(), + amount: PaymentAmount::new("0.00000100", "btc").unwrap(), payment_reference: PaymentReference::new(uuid::Uuid::new_v4().to_string()).unwrap(), proposal_expires_at: None, recurrence: None, diff --git a/paykit-server-e2e/tests/composition.rs b/paykit-server-e2e/tests/composition.rs index 1e5cc3f..8b6408b 100644 --- a/paykit-server-e2e/tests/composition.rs +++ b/paykit-server-e2e/tests/composition.rs @@ -173,7 +173,7 @@ fn payment_intent(reader: &ReaderPubky, marker: &PaykitReceiverMarker) -> Delive marker, PaykitReceiverPath::new("paykit/server").unwrap(), &PaymentRequestTerms { - amount: PaymentAmount::new("0.00000100", "BTC").unwrap(), + amount: PaymentAmount::new("0.00000100", "btc").unwrap(), payment_reference: PaymentReference::new(Uuid::new_v4().to_string()).unwrap(), proposal_expires_at: None, recurrence: None, diff --git a/paykit-server-e2e/tests/invoices.rs b/paykit-server-e2e/tests/invoices.rs index a077053..a64472e 100644 --- a/paykit-server-e2e/tests/invoices.rs +++ b/paykit-server-e2e/tests/invoices.rs @@ -86,7 +86,7 @@ fn payment_intent() -> DeliveryIntentV1 { &marker(), PaykitReceiverPath::new("paykit/server").unwrap(), &PaymentRequestTerms { - amount: PaymentAmount::new("0.00000100", "BTC").unwrap(), + amount: PaymentAmount::new("0.00000100", "btc").unwrap(), payment_reference: PaymentReference::new(uuid::Uuid::new_v4().to_string()).unwrap(), proposal_expires_at: None, recurrence: None, diff --git a/paykit-server-e2e/tests/live_adapters.rs b/paykit-server-e2e/tests/live_adapters.rs index 7c63ab1..431d706 100644 --- a/paykit-server-e2e/tests/live_adapters.rs +++ b/paykit-server-e2e/tests/live_adapters.rs @@ -204,7 +204,7 @@ async fn live_pubky_marker_discovery_and_payment_request_delivery() { payer_key.clone(), payer_path.clone(), PaymentRequestTerms { - amount: PaymentAmount::new("0.00000100", "BTC").unwrap(), + amount: PaymentAmount::new("0.00000100", "btc").unwrap(), payment_reference: PaymentReference::new(reference.clone()).unwrap(), proposal_expires_at: None, recurrence: None, diff --git a/paykit-server-e2e/tests/server_workflow.rs b/paykit-server-e2e/tests/server_workflow.rs index 91003d4..507ce56 100644 --- a/paykit-server-e2e/tests/server_workflow.rs +++ b/paykit-server-e2e/tests/server_workflow.rs @@ -726,12 +726,13 @@ async fn assert_persisted_workflow_inputs( "paykit/server" ); assert_ne!(endpoint.marker_fingerprint(), [0; 32]); + let expected_payload = serde_json::json!({ "value": fixture.address }).to_string(); assert!(matches!( endpoint.operation(), DeliveryOperationV1::EndpointPublication { receiving_details } if receiving_details.len() == 1 - && receiving_details[0].identifier == "btc-bitcoin-p2wpkh" - && receiving_details[0].payload == fixture.address + && receiving_details[0].identifier == "btc-testnet-p2wpkh" + && receiving_details[0].payload == expected_payload )); let payment_plaintext = crypto @@ -761,13 +762,13 @@ async fn assert_persisted_workflow_inputs( payment.operation(), DeliveryOperationV1::PaymentRequestProposal { terms } if terms.amount == amount - && terms.asset == "BTC" + && terms.asset == "btc" && uuid::Uuid::parse_str(&terms.payment_reference) .is_ok_and(|reference| reference.get_version_num() == 4 && reference.get_variant() == uuid::Variant::RFC4122 && terms.payment_reference == reference.hyphenated().to_string()) && terms.proposal_expires_at.is_none() - && terms.accepted_endpoint_identifiers == ["btc-bitcoin-p2wpkh"] + && terms.accepted_endpoint_identifiers == ["btc-testnet-p2wpkh"] && terms.metadata.get("bundle_id") == Some(&serde_json::json!(bundle)) && terms.metadata.get("lock_resource") == Some(&serde_json::json!(fixture.lock_resource)) diff --git a/paykit-server/src/application/create_invoice.rs b/paykit-server/src/application/create_invoice.rs index 9a43a5d..882ba37 100644 --- a/paykit-server/src/application/create_invoice.rs +++ b/paykit-server/src/application/create_invoice.rs @@ -153,8 +153,25 @@ pub trait IntentBuilder: Send + Sync { ) -> Result, CreateInvoiceError>; } -#[derive(Default)] -pub struct PaykitIntentBuilder; +pub struct PaykitIntentBuilder { + bitcoin_network: crate::config::BitcoinNetwork, +} + +impl PaykitIntentBuilder { + pub fn new(bitcoin_network: crate::config::BitcoinNetwork) -> Self { + Self { bitcoin_network } + } + + fn p2wpkh_identifier(&self) -> &'static str { + match self.bitcoin_network { + crate::config::BitcoinNetwork::Mainnet => "btc-bitcoin-p2wpkh", + crate::config::BitcoinNetwork::Testnet => "btc-testnet-p2wpkh", + crate::config::BitcoinNetwork::Signet => "btc-signet-p2wpkh", + crate::config::BitcoinNetwork::Regtest => "btc-regtest-p2wpkh", + } + } +} + impl IntentBuilder for PaykitIntentBuilder { fn payment_request_terms( &self, @@ -176,7 +193,7 @@ impl IntentBuilder for PaykitIntentBuilder { Ok(PaymentRequestTerms { amount: PaymentAmount::new( format!("{}.{:08}", sats / 100_000_000, sats % 100_000_000), - "BTC", + "btc", ) .map_err(|_| CreateInvoiceError::InvalidRequest)?, payment_reference: PaymentReference::new(uuid::Uuid::new_v4().hyphenated().to_string()) @@ -184,7 +201,7 @@ impl IntentBuilder for PaykitIntentBuilder { proposal_expires_at: None, recurrence: None, accepted_payment_endpoint_identifiers: vec![ - PaymentEndpointIdentifier::new("btc-bitcoin-p2wpkh") + PaymentEndpointIdentifier::new(self.p2wpkh_identifier()) .map_err(|_| CreateInvoiceError::InvalidRequest)?, ], metadata, @@ -198,9 +215,11 @@ impl IntentBuilder for PaykitIntentBuilder { if address.is_empty() { return Err(CreateInvoiceError::InvalidRequest); } - let identifier = PaymentEndpointIdentifier::new("btc-bitcoin-p2wpkh") + let identifier = PaymentEndpointIdentifier::new(self.p2wpkh_identifier()) + .map_err(|_| CreateInvoiceError::InvalidRequest)?; + let payload = serde_json::to_string(&serde_json::json!({ "value": address })) .map_err(|_| CreateInvoiceError::InvalidRequest)?; - Ok(vec![(identifier, PaymentEndpointPayload::new(address))]) + Ok(vec![(identifier, PaymentEndpointPayload::new(payload))]) } } diff --git a/paykit-server/src/application/semantic_intent.rs b/paykit-server/src/application/semantic_intent.rs index bcf2fe8..b991a59 100644 --- a/paykit-server/src/application/semantic_intent.rs +++ b/paykit-server/src/application/semantic_intent.rs @@ -336,7 +336,7 @@ mod tests { operation: DeliveryOperationV1::PaymentRequestProposal { terms: PaymentTermsV1 { amount: "0.00000100".into(), - asset: "BTC".into(), + asset: "btc".into(), payment_reference: "550e8400-e29b-41d4-a716-446655440000".into(), proposal_expires_at: None, accepted_endpoint_identifiers: vec!["btc-bitcoin-p2wpkh".into()], diff --git a/paykit-server/src/bin/paykit-reader-demo/payment_instructions.rs b/paykit-server/src/bin/paykit-reader-demo/payment_instructions.rs index 82b6e07..b36fe40 100644 --- a/paykit-server/src/bin/paykit-reader-demo/payment_instructions.rs +++ b/paykit-server/src/bin/paykit-reader-demo/payment_instructions.rs @@ -5,12 +5,19 @@ use paykit_sdk::{ PaymentRequestLifecycleState, PaymentRequestLocalRole, PaymentRequestRecord, PrivatePaymentListView, PubkyPublicKey, }; +use serde::Deserialize; use super::{Failure, ReceiveOutput}; -pub(super) const BITCOIN_ENDPOINT: &str = "btc-bitcoin-p2wpkh"; +pub(super) const BITCOIN_ENDPOINT: &str = "btc-regtest-p2wpkh"; const MAX_BITCOIN_SATS: u64 = 2_100_000_000_000_000; +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct BitcoinEndpointPayload { + value: String, +} + pub(super) fn select_actionable_request( requests: &[PaymentRequestRecord], ) -> Result, Failure> { @@ -50,7 +57,7 @@ pub(super) fn payment_instructions( return Err(Failure::ProtocolFailed); } let terms = request.terms.as_ref().ok_or(Failure::ProtocolFailed)?; - if terms.amount.asset != "BTC" + if terms.amount.asset != "btc" || terms.recurrence.is_some() || terms.proposal_expires_at.is_some() || terms.accepted_payment_endpoint_identifiers != [BITCOIN_ENDPOINT.to_owned()] @@ -71,15 +78,18 @@ pub(super) fn payment_instructions( if private_list.payment_endpoints.len() != 1 { return Err(Failure::ProtocolFailed); } - let raw_address = private_list + let raw_payload = private_list .payment_endpoints .get(BITCOIN_ENDPOINT) .ok_or(Failure::ProtocolFailed)?; - let address = Address::from_str(raw_address) + let raw_address = serde_json::from_str::(raw_payload) + .map_err(|_| Failure::ProtocolFailed)? + .value; + let address = Address::from_str(&raw_address) .map_err(|_| Failure::ProtocolFailed)? .require_network(Network::Regtest) .map_err(|_| Failure::ProtocolFailed)?; - if address.address_type() != Some(AddressType::P2wpkh) || address.to_string() != *raw_address { + if address.address_type() != Some(AddressType::P2wpkh) || address.to_string() != raw_address { return Err(Failure::ProtocolFailed); } let address = address.to_string(); @@ -93,7 +103,7 @@ pub(super) fn payment_instructions( status: "received", payment_request_id: request.payment_request_id.clone(), address: address.clone(), - asset: "BTC", + asset: "btc", amount_sats: amount_sats.to_string(), payment_command: format!( "docker compose exec -T bitcoin sh -ec 'bitcoin-cli -conf=\"$BITCOIN_DATA/bitcoin.conf\" -regtest -rpcwallet=miner sendtoaddress \"{address}\" \"{bitcoin_amount}\"'" @@ -139,7 +149,7 @@ mod tests { proposal_event_id: Some("8a0d8b4c-913f-4e31-9f2c-2a6f5bb4d101".into()), terms: Some(PaymentRequestTermsRecord { amount: AmountRecord { - asset: "BTC".into(), + asset: "btc".into(), value: "0.00050000".into(), }, payment_reference: "reference-1".into(), @@ -166,7 +176,10 @@ mod tests { fn private_list(address: &str) -> PrivatePaymentListView { PrivatePaymentListView { latest_stream_item_id: Some(2), - payment_endpoints: HashMap::from([(BITCOIN_ENDPOINT.into(), address.into())]), + payment_endpoints: HashMap::from([( + BITCOIN_ENDPOINT.into(), + serde_json::json!({ "value": address }).to_string(), + )]), last_refresh_at: None, } } @@ -206,7 +219,7 @@ mod tests { "status": "received", "payment_request_id": request().payment_request_id, "address": regtest_p2wpkh(), - "asset": "BTC", + "asset": "btc", "amount_sats": "50000", "payment_command": format!( "docker compose exec -T bitcoin sh -ec 'bitcoin-cli -conf=\"$BITCOIN_DATA/bitcoin.conf\" -regtest -rpcwallet=miner sendtoaddress \"{}\" \"0.00050000\"'", diff --git a/paykit-server/src/server.rs b/paykit-server/src/server.rs index e016fca..bf143f5 100644 --- a/paykit-server/src/server.rs +++ b/paykit-server/src/server.rs @@ -26,7 +26,10 @@ use crate::{ setup_orchestration::PubkyCompanionRelay, workers::{ observer::{ElectrumAdapter, ElectrumPort, ObserverError, observe_once}, - outbox::{ProcessingHealth, process_claim_with_health, process_reconciliation_with_health}, + outbox::{ + ProcessingHealth, RetrySchedule, process_claim_with_health, + process_reconciliation_with_health, + }, }, }; use async_trait::async_trait; @@ -185,7 +188,9 @@ impl Server { Arc::new(creators.clone()), config.deployment_invariants().bitcoin_network.clone(), Arc::new(invoices.clone()), - Arc::new(PaykitIntentBuilder), + Arc::new(PaykitIntentBuilder::new( + config.deployment_invariants().bitcoin_network.clone(), + )), )); let status_service = Arc::new(PaymentStatusService::new(Arc::new(invoices.clone()))); let signed_auth = Arc::new(SignedLocksAuth::from_config(&config)); @@ -366,14 +371,34 @@ fn retry_delay(initial: Duration, maximum: Duration, attempt_count: i32) -> Dura initial.saturating_mul(1_u32 << exponent).min(maximum) } +const RAPID_LINK_ESTABLISHMENT_RETRY_ATTEMPTS: i32 = 20; +const RAPID_LINK_ESTABLISHMENT_RETRY_DELAY: Duration = Duration::from_secs(1); + +fn outbox_retry_schedule( + initial: Duration, + maximum: Duration, + attempt_count: i32, +) -> RetrySchedule { + let default = retry_delay(initial, maximum, attempt_count); + let link_establishment = if attempt_count <= RAPID_LINK_ESTABLISHMENT_RETRY_ATTEMPTS { + RAPID_LINK_ESTABLISHMENT_RETRY_DELAY + } else { + retry_delay( + initial, + maximum, + attempt_count - RAPID_LINK_ESTABLISHMENT_RETRY_ATTEMPTS, + ) + }; + RetrySchedule::new(default, link_establishment) +} + async fn outbox_enqueue_loop(workers: Arc, runtime: Arc) { let owner = Uuid::new_v4(); - let mut interval = tokio::time::interval(workers.outbox_poll_interval); - interval.set_missed_tick_behavior(MissedTickBehavior::Delay); + let mut next_poll_delay = Duration::ZERO; loop { tokio::select! { _ = runtime.cancelled() => break, - _ = interval.tick() => {} + _ = tokio::time::sleep(next_poll_delay) => {} } if !runtime.may_start_worker_claim() { break; @@ -393,6 +418,7 @@ async fn outbox_enqueue_loop(workers: Arc, runtime: Arc { runtime.set_outbox_enqueue_available(false); + next_poll_delay = workers.outbox_poll_interval; continue; } }; @@ -400,14 +426,15 @@ async fn outbox_enqueue_loop(workers: Arc, runtime: Arc { - process_claim_with_health(&workers.outbox, &adapter, &claim, delay).await + process_claim_with_health(&workers.outbox, &adapter, &claim, retry_schedule) + .await } Err(AdapterBuildError::Permanent) => workers .outbox @@ -416,18 +443,32 @@ async fn outbox_enqueue_loop(workers: Arc, runtime: Arc workers .outbox - .mark_retryable(&claim, delay, OutboxRetryClass::AdapterUnavailable) + .mark_retryable( + &claim, + retry_schedule.default_delay(), + OutboxRetryClass::AdapterUnavailable, + ) .await - .map(|transitioned| (transitioned, ProcessingHealth::Retryable)), + .map(|transitioned| { + ( + transitioned, + ProcessingHealth::Retryable(retry_schedule.default_delay()), + ) + }), } }); } let mut delivery_available = true; let mut outbox_available = true; + next_poll_delay = workers.outbox_poll_interval; while let Some(result) = batch.join_next().await { match result { Ok(Ok((_, ProcessingHealth::Available))) => {} - Ok(Ok((_, ProcessingHealth::Retryable | ProcessingHealth::PermanentFailure))) => { + Ok(Ok((_, ProcessingHealth::Retryable(delay)))) => { + delivery_available = false; + next_poll_delay = next_poll_delay.min(delay); + } + Ok(Ok((_, ProcessingHealth::PermanentFailure))) => { delivery_available = false; } Ok(Err(_)) => outbox_available = false, @@ -501,7 +542,7 @@ async fn outbox_reconciliation_loop(workers: Arc, runtime: Arc .outbox .retry_reconciliation(&claim, delay, OutboxRetryClass::AdapterUnavailable) .await - .map(|transitioned| (transitioned, ProcessingHealth::Retryable)), + .map(|transitioned| (transitioned, ProcessingHealth::Retryable(delay))), } }); } @@ -510,7 +551,10 @@ async fn outbox_reconciliation_loop(workers: Arc, runtime: Arc while let Some(result) = batch.join_next().await { match result { Ok(Ok((_, ProcessingHealth::Available))) => {} - Ok(Ok((_, ProcessingHealth::Retryable | ProcessingHealth::PermanentFailure))) => { + Ok(Ok(( + _, + ProcessingHealth::Retryable(_) | ProcessingHealth::PermanentFailure, + ))) => { delivery_available = false; } Ok(Err(_)) => outbox_available = false, @@ -727,6 +771,36 @@ mod tests { ); } + #[test] + fn link_establishment_retries_rapidly_before_restarting_exponential_backoff() { + let initial = Duration::from_secs(1); + let maximum = Duration::from_secs(300); + + for attempt in 1..=RAPID_LINK_ESTABLISHMENT_RETRY_ATTEMPTS { + let schedule = outbox_retry_schedule(initial, maximum, attempt); + assert_eq!( + schedule.delay_for(OutboxRetryClass::LinkEstablishment), + Duration::from_secs(1) + ); + } + + assert_eq!( + outbox_retry_schedule(initial, maximum, 21) + .delay_for(OutboxRetryClass::LinkEstablishment), + Duration::from_secs(1) + ); + assert_eq!( + outbox_retry_schedule(initial, maximum, 22) + .delay_for(OutboxRetryClass::LinkEstablishment), + Duration::from_secs(2) + ); + assert_eq!( + outbox_retry_schedule(initial, maximum, 30) + .delay_for(OutboxRetryClass::LinkEstablishment), + Duration::from_secs(300) + ); + } + #[tokio::test] async fn production_spawn_path_owns_all_three_workers() { let config = Config::from_toml_and_environment( diff --git a/paykit-server/src/workers/outbox.rs b/paykit-server/src/workers/outbox.rs index 6009dc0..80ba654 100644 --- a/paykit-server/src/workers/outbox.rs +++ b/paykit-server/src/workers/outbox.rs @@ -50,10 +50,37 @@ fn at_stage(error: HandoffError, stage: RetryableHandoffStage) -> HandoffFailure #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ProcessingHealth { Available, - Retryable, + Retryable(Duration), PermanentFailure, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RetrySchedule { + default: Duration, + link_establishment: Duration, +} + +impl RetrySchedule { + pub fn new(default: Duration, link_establishment: Duration) -> Self { + Self { + default, + link_establishment, + } + } + + pub fn default_delay(self) -> Duration { + self.default + } + + pub(crate) fn delay_for(self, stage: RetryableHandoffStage) -> Duration { + if stage == RetryableHandoffStage::LinkEstablishment { + self.link_establishment + } else { + self.default + } + } +} + /// Public-SDK-only adapter. Production implementations must persist SDK state /// through the creator SDK-state service; an in-memory runtime is test-only. #[async_trait] @@ -155,16 +182,21 @@ pub async fn process_claim( claim: &ClaimedOutbox, retry_delay: Duration, ) -> Result { - process_claim_with_health(store, adapter, claim, retry_delay) - .await - .map(|(transitioned, _)| transitioned) + process_claim_with_health( + store, + adapter, + claim, + RetrySchedule::new(retry_delay, retry_delay), + ) + .await + .map(|(transitioned, _)| transitioned) } pub async fn process_claim_with_health( store: &OutboxStore, adapter: &dyn Adapter, claim: &ClaimedOutbox, - retry_delay: Duration, + retry_schedule: RetrySchedule, ) -> Result<(bool, ProcessingHealth), PersistenceError> { let intent = match store.delivery_intent(claim) { Ok(intent) => intent, @@ -180,10 +212,13 @@ pub async fn process_claim_with_health( .mark_handed_off(claim, &result) .await .map(|transitioned| (transitioned, ProcessingHealth::Available)), - Err(HandoffFailure::Retryable(stage)) => store - .mark_retryable(claim, retry_delay, stage) - .await - .map(|transitioned| (transitioned, ProcessingHealth::Retryable)), + Err(HandoffFailure::Retryable(stage)) => { + let delay = retry_schedule.delay_for(stage); + store + .mark_retryable(claim, delay, stage) + .await + .map(|transitioned| (transitioned, ProcessingHealth::Retryable(delay))) + } Err(HandoffFailure::Permanent) => store .mark_permanently_failed(claim) .await @@ -241,11 +276,11 @@ pub async fn process_reconciliation_with_health( RetryableHandoffStage::ReconciliationPending, ) .await - .map(|transitioned| (transitioned, ProcessingHealth::Retryable)), + .map(|transitioned| (transitioned, ProcessingHealth::Retryable(retry_delay))), Err(HandoffError::Retryable(_)) => store .retry_reconciliation(claim, retry_delay, RetryableHandoffStage::Reconciliation) .await - .map(|transitioned| (transitioned, ProcessingHealth::Retryable)), + .map(|transitioned| (transitioned, ProcessingHealth::Retryable(retry_delay))), Err(HandoffError::Permanent) => store .mark_reconciliation_permanently_failed(claim) .await diff --git a/paykit-server/tests/create_invoice.rs b/paykit-server/tests/create_invoice.rs index c61e65e..b5a7e44 100644 --- a/paykit-server/tests/create_invoice.rs +++ b/paykit-server/tests/create_invoice.rs @@ -100,11 +100,11 @@ fn capable_marker() -> paykit_lib::PaykitReceiverMarker { #[test] fn library_payment_request_has_exact_terms_amount_and_metadata() { let request = request(); - let terms = PaykitIntentBuilder + let terms = PaykitIntentBuilder::new(BitcoinNetwork::Mainnet) .payment_request_terms(&request, &valid_lock()) .unwrap(); assert_eq!(terms.amount.value, "0.00050000"); - assert_eq!(terms.amount.asset, "BTC"); + assert_eq!(terms.amount.asset, "btc"); assert_eq!(terms.proposal_expires_at, None); assert_eq!(terms.recurrence, None); assert_eq!( @@ -117,6 +117,28 @@ fn library_payment_request_has_exact_terms_amount_and_metadata() { ); } +#[test] +fn payment_request_and_private_payment_list_use_the_configured_network() { + for (network, expected_identifier) in [ + (BitcoinNetwork::Mainnet, "btc-bitcoin-p2wpkh"), + (BitcoinNetwork::Testnet, "btc-testnet-p2wpkh"), + (BitcoinNetwork::Signet, "btc-signet-p2wpkh"), + (BitcoinNetwork::Regtest, "btc-regtest-p2wpkh"), + ] { + let builder = PaykitIntentBuilder::new(network); + let terms = builder + .payment_request_terms(&request(), &valid_lock()) + .unwrap(); + let details = builder.receiving_details("address").unwrap(); + + assert_eq!( + terms.accepted_payment_endpoint_identifiers[0].as_str(), + expected_identifier + ); + assert_eq!(details[0].0.as_str(), expected_identifier); + } +} + #[test] fn private_payment_list_uses_derived_bech32_p2wpkh_address() { use bitcoin::{ @@ -140,11 +162,14 @@ fn private_payment_list_uses_derived_bech32_p2wpkh_address() { let xpub = Xpub::from_priv(&secp, &account).to_string(); let address = derive_bip84_p2wpkh_address(&xpub, 0, &BitcoinNetwork::Mainnet, 0) .expect("valid account xpub derives an address"); - let details = PaykitIntentBuilder + let details = PaykitIntentBuilder::new(BitcoinNetwork::Mainnet) .receiving_details(&address) .expect("canonical library types accept endpoint"); assert_eq!(details[0].0.as_str(), "btc-bitcoin-p2wpkh"); - assert_eq!(details[0].1.as_str(), address); + assert_eq!( + serde_json::from_str::(details[0].1.as_str()).unwrap(), + serde_json::json!({ "value": address }) + ); } struct FakeStore { @@ -299,7 +324,7 @@ fn service( Arc::new(FakeCredentials), BitcoinNetwork::Mainnet, store, - Arc::new(PaykitIntentBuilder), + Arc::new(PaykitIntentBuilder::new(BitcoinNetwork::Mainnet)), ) } @@ -439,7 +464,7 @@ async fn fifteen_second_deadline_is_safe_and_does_not_commit() { Arc::new(FakeCredentials), BitcoinNetwork::Mainnet, store.clone(), - Arc::new(PaykitIntentBuilder), + Arc::new(PaykitIntentBuilder::new(BitcoinNetwork::Mainnet)), Arc::new(FixedClock::new([start, start + Duration::from_secs(15)])), ); @@ -477,7 +502,7 @@ async fn marker_discovery_cannot_start_after_the_whole_request_deadline() { Arc::new(FakeCredentials), BitcoinNetwork::Mainnet, store.clone(), - Arc::new(PaykitIntentBuilder), + Arc::new(PaykitIntentBuilder::new(BitcoinNetwork::Mainnet)), Arc::new(FixedClock::new([ start, start, @@ -521,7 +546,7 @@ async fn signed_router_maps_deadline_exhaustion_to_dependency_timeout() { Arc::new(FakeCredentials), BitcoinNetwork::Mainnet, store, - Arc::new(PaykitIntentBuilder), + Arc::new(PaykitIntentBuilder::new(BitcoinNetwork::Mainnet)), Arc::new(FixedClock::new([start, start + Duration::from_secs(15)])), ); let router = invoices_router(Arc::new(service)).layer(Extension(signed_auth(&key))); @@ -793,7 +818,7 @@ async fn new_invoice_discovers_marker_before_atomic_persistence_and_pins_it_in_b Arc::new(FakeCredentials), BitcoinNetwork::Mainnet, store.clone(), - Arc::new(PaykitIntentBuilder), + Arc::new(PaykitIntentBuilder::new(BitcoinNetwork::Mainnet)), ); service.create(request()).await.unwrap(); diff --git a/paykit-server/tests/outbox.rs b/paykit-server/tests/outbox.rs index a01f5f5..809b2f8 100644 --- a/paykit-server/tests/outbox.rs +++ b/paykit-server/tests/outbox.rs @@ -86,7 +86,7 @@ fn payment_intent(marker: &PaykitReceiverMarker) -> DeliveryIntentV1 { marker, PaykitReceiverPath::new("paykit/server").unwrap(), &PaymentRequestTerms { - amount: PaymentAmount::new("0.00050000", "BTC").unwrap(), + amount: PaymentAmount::new("0.00050000", "btc").unwrap(), payment_reference: PaymentReference::new(uuid::Uuid::new_v4().to_string()).unwrap(), proposal_expires_at: None, recurrence: None, diff --git a/scripts/prepare-local-docker-sources.sh b/scripts/prepare-local-docker-sources.sh index 2dc77d3..ff3e6fd 100644 --- a/scripts/prepare-local-docker-sources.sh +++ b/scripts/prepare-local-docker-sources.sh @@ -6,13 +6,13 @@ lockfile=${2:-Cargo.lock} sed -i \ -e 's#locks-core = { git = "https://github.com/pubky/locks.git", rev = "df5ea1b6d8dcdec3a9b5a915c3f57bca69d75c8a" }#locks-core = { path = "/build/locks/locks-core" }#' \ - -e 's#paykit-lib = { git = "https://github.com/pubky/paykit-rs.git", rev = "52a852995bfc457b78d32f5a45f6741766a89bba" }#paykit-lib = { path = "/build/paykit-rs/paykit-lib" }#' \ - -e 's#paykit-sdk = { git = "https://github.com/pubky/paykit-rs.git", rev = "52a852995bfc457b78d32f5a45f6741766a89bba" }#paykit-sdk = { path = "/build/paykit-rs/paykit-sdk" }#' \ + -e 's#paykit-lib = { git = "https://github.com/pubky/paykit-rs.git", rev = "6b241878a9bba5cecea919c0298c3f90624be6ff" }#paykit-lib = { path = "/build/paykit-rs/paykit-lib" }#' \ + -e 's#paykit-sdk = { git = "https://github.com/pubky/paykit-rs.git", rev = "6b241878a9bba5cecea919c0298c3f90624be6ff" }#paykit-sdk = { path = "/build/paykit-rs/paykit-sdk" }#' \ "$manifest" sed -i \ -e '/source = "git+https:\/\/github.com\/pubky\/locks.git?rev=df5ea1b6d8dcdec3a9b5a915c3f57bca69d75c8a#df5ea1b6d8dcdec3a9b5a915c3f57bca69d75c8a"/d' \ - -e '/source = "git+https:\/\/github.com\/pubky\/paykit-rs.git?rev=52a852995bfc457b78d32f5a45f6741766a89bba#52a852995bfc457b78d32f5a45f6741766a89bba"/d' \ + -e '/source = "git+https:\/\/github.com\/pubky\/paykit-rs.git?rev=6b241878a9bba5cecea919c0298c3f90624be6ff#6b241878a9bba5cecea919c0298c3f90624be6ff"/d' \ "$lockfile" grep -Fx 'locks-core = { path = "/build/locks/locks-core" }' "$manifest"