Skip to content

Commit 5fbcc94

Browse files
committed
fix(payment-processor): pass custom quote amount over gRPC
Forward the optional custom melt quote amount in PaymentQuoteRequest so remote processors can price and validate amount-aware custom payments.
1 parent 5733618 commit 5fbcc94

6 files changed

Lines changed: 112 additions & 5 deletions

File tree

crates/cdk-axum/src/custom_handlers.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,6 +1105,7 @@ mod tests {
11051105
method: "venmo".to_string(),
11061106
request: "test-payment-request".to_string(),
11071107
unit: CurrencyUnit::Sat,
1108+
amount: None,
11081109
extra: Value::Null,
11091110
})
11101111
.unwrap();
@@ -1144,6 +1145,7 @@ mod tests {
11441145
method: "PayPal".to_string(),
11451146
request: "test-payment-request".to_string(),
11461147
unit: CurrencyUnit::Sat,
1148+
amount: None,
11471149
extra: Value::Null,
11481150
})
11491151
.unwrap();

crates/cdk-fake-wallet/src/lib.rs

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -441,8 +441,19 @@ impl FakeWallet {
441441
async fn custom_outgoing_amount(
442442
&self,
443443
unit: &CurrencyUnit,
444+
amount: Option<&Amount<CurrencyUnit>>,
444445
extra_json: &Option<String>,
445446
) -> Result<Amount<CurrencyUnit>, Error> {
447+
if let Some(amount) = amount {
448+
return convert_currency_amount(
449+
amount.value(),
450+
amount.unit(),
451+
unit,
452+
&self.exchange_rate_cache,
453+
)
454+
.await;
455+
}
456+
446457
#[derive(Deserialize)]
447458
struct CustomAmountField {
448459
amount: Option<u64>,
@@ -587,7 +598,11 @@ impl MintPayment for FakeWallet {
587598
self.ensure_custom_method_supported(&custom_options.method)?;
588599

589600
let amount = self
590-
.custom_outgoing_amount(unit, &custom_options.extra_json)
601+
.custom_outgoing_amount(
602+
unit,
603+
custom_options.amount.as_ref(),
604+
&custom_options.extra_json,
605+
)
591606
.await?;
592607

593608
return Ok(PaymentQuoteResponse {
@@ -768,7 +783,11 @@ impl MintPayment for FakeWallet {
768783
self.ensure_custom_method_supported(&custom_options.method)?;
769784

770785
let total_spent = self
771-
.custom_outgoing_amount(unit, &custom_options.extra_json)
786+
.custom_outgoing_amount(
787+
unit,
788+
custom_options.amount.as_ref(),
789+
&custom_options.extra_json,
790+
)
772791
.await?;
773792

774793
Ok(MakePaymentResponse {
@@ -1070,8 +1089,8 @@ fn fake_secret_key(seed: &str) -> SecretKey {
10701089
#[cfg(test)]
10711090
mod tests {
10721091
use cdk_common::payment::{
1073-
CustomIncomingPaymentOptions, IncomingPaymentOptions, MintPayment,
1074-
OnchainOutgoingPaymentOptions, OutgoingPaymentOptions, PaymentIdentifier,
1092+
CustomIncomingPaymentOptions, CustomOutgoingPaymentOptions, IncomingPaymentOptions,
1093+
MintPayment, OnchainOutgoingPaymentOptions, OutgoingPaymentOptions, PaymentIdentifier,
10751094
};
10761095

10771096
use super::*;
@@ -1093,6 +1112,22 @@ mod tests {
10931112
test_wallet_with_delay(0)
10941113
}
10951114

1115+
fn custom_outgoing_options(
1116+
amount: Option<Amount<CurrencyUnit>>,
1117+
extra_json: Option<String>,
1118+
) -> OutgoingPaymentOptions {
1119+
OutgoingPaymentOptions::Custom(Box::new(CustomOutgoingPaymentOptions {
1120+
method: "venmo".to_string(),
1121+
request: "test-payment-request".to_string(),
1122+
amount,
1123+
max_fee_amount: None,
1124+
timeout_secs: None,
1125+
melt_options: None,
1126+
extra_json,
1127+
quote_id: cdk_common::QuoteId::new(),
1128+
}))
1129+
}
1130+
10961131
#[tokio::test]
10971132
async fn custom_payment_methods_are_configurable() {
10981133
let settings = test_wallet()
@@ -1177,4 +1212,58 @@ mod tests {
11771212
);
11781213
assert_eq!(response.status, MeltQuoteState::Paid);
11791214
}
1215+
1216+
#[tokio::test]
1217+
async fn custom_outgoing_quote_prefers_typed_amount() {
1218+
let wallet = test_wallet()
1219+
.with_custom_payment_methods(HashMap::from([("venmo".to_string(), "{}".to_string())]));
1220+
1221+
let response = wallet
1222+
.get_payment_quote(
1223+
&CurrencyUnit::Sat,
1224+
custom_outgoing_options(
1225+
Some(Amount::new(20, CurrencyUnit::Sat)),
1226+
Some(r#"{"amount":30}"#.to_string()),
1227+
),
1228+
)
1229+
.await
1230+
.expect("configured custom method should quote outgoing payment");
1231+
1232+
assert_eq!(response.amount, Amount::new(20, CurrencyUnit::Sat));
1233+
}
1234+
1235+
#[tokio::test]
1236+
async fn custom_outgoing_payment_prefers_typed_amount() {
1237+
let wallet = test_wallet()
1238+
.with_custom_payment_methods(HashMap::from([("venmo".to_string(), "{}".to_string())]));
1239+
1240+
let response = wallet
1241+
.make_payment(
1242+
&CurrencyUnit::Sat,
1243+
custom_outgoing_options(
1244+
Some(Amount::new(20, CurrencyUnit::Sat)),
1245+
Some(r#"{"amount":30}"#.to_string()),
1246+
),
1247+
)
1248+
.await
1249+
.expect("configured custom method should make outgoing payment");
1250+
1251+
assert_eq!(response.total_spent, Amount::new(21, CurrencyUnit::Sat));
1252+
}
1253+
1254+
#[tokio::test]
1255+
async fn custom_outgoing_amount_falls_back_to_extra_json() {
1256+
let wallet = test_wallet()
1257+
.with_custom_payment_methods(HashMap::from([("venmo".to_string(), "{}".to_string())]));
1258+
1259+
let response = wallet
1260+
.get_payment_quote(
1261+
&CurrencyUnit::Sat,
1262+
custom_outgoing_options(None, Some(r#"{"amount":30}"#.to_string())),
1263+
)
1264+
.await
1265+
.expect("configured custom method should quote outgoing payment");
1266+
1267+
assert_eq!(response.amount, Amount::new(30, CurrencyUnit::Sat));
1268+
}
11801269
}

crates/cdk-payment-processor/src/proto/client.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,13 @@ impl MintPayment for PaymentProcessorClient {
288288
_ => None,
289289
};
290290

291+
let amount = match &options {
292+
cdk_common::payment::OutgoingPaymentOptions::Custom(opts) => {
293+
opts.amount.clone().into_proto()
294+
}
295+
_ => None,
296+
};
297+
291298
let quote_id = match &options {
292299
cdk_common::payment::OutgoingPaymentOptions::Custom(opts) => opts.quote_id.to_string(),
293300
cdk_common::payment::OutgoingPaymentOptions::Bolt11(opts) => opts.quote_id.to_string(),
@@ -304,6 +311,7 @@ impl MintPayment for PaymentProcessorClient {
304311
extra_json,
305312
quote_id,
306313
onchain_options,
314+
amount,
307315
}))
308316
.await
309317
.map_err(|err| {

crates/cdk-payment-processor/src/proto/payment_processor.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@ message PaymentQuoteRequest {
180180
// Structured options for onchain quote requests. Onchain processors need the
181181
// mint-generated quote_id and amount when preparing fee_options.
182182
optional OnchainOutgoingPaymentOptions onchain_options = 7;
183+
// Optional amount the wallet would like to pay for custom payment methods.
184+
optional AmountMessage amount = 8;
183185
}
184186

185187
enum QuoteState {

crates/cdk-payment-processor/src/proto/server.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,12 +329,17 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
329329
))
330330
}
331331
OutgoingPaymentRequestType::Custom => {
332+
let amount = request
333+
.amount
334+
.try_from_proto()
335+
.map_err(|_| Status::invalid_argument("Invalid amount"))?;
336+
332337
// Custom payment method - pass request as-is with no validation
333338
cdk_common::payment::OutgoingPaymentOptions::Custom(Box::new(
334339
cdk_common::payment::CustomOutgoingPaymentOptions {
335340
method: String::new(), // Will be set from variant
336341
request: request.request.clone(),
337-
amount: None,
342+
amount,
338343
max_fee_amount: None,
339344
timeout_secs: None,
340345
melt_options: request.options.map(TryInto::try_into).transpose()?,

crates/cdk/src/wallet/mint_connector/http_client.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,6 +1346,7 @@ mod tests {
13461346
method: "paypal".to_string(),
13471347
request: "paypal://pay?id=123".to_string(),
13481348
unit: cdk_common::CurrencyUnit::Sat,
1349+
amount: None,
13491350
extra: serde_json::Value::Null,
13501351
}))
13511352
.await

0 commit comments

Comments
 (0)