feat: optional transport and nut10 secret on payment request (#744)

* feat: optional transport on payment request

* feat: create token for payment rquest

* feat: create payment request

* feat: arg append
This commit is contained in:
thesimplekid
2025-05-15 09:06:10 +01:00
committed by GitHub
parent 9ac387ae3d
commit 385ec4d295
3 changed files with 492 additions and 126 deletions

View File

@@ -3,15 +3,18 @@
//! <https://github.com/cashubtc/nuts/blob/main/18.md> //! <https://github.com/cashubtc/nuts/blob/main/18.md>
use std::fmt; use std::fmt;
use std::ops::Not;
use std::str::FromStr; use std::str::FromStr;
use bitcoin::base64::engine::{general_purpose, GeneralPurpose}; use bitcoin::base64::engine::{general_purpose, GeneralPurpose};
use bitcoin::base64::{alphabet, Engine}; use bitcoin::base64::{alphabet, Engine};
use serde::ser::{SerializeTuple, Serializer};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use super::{CurrencyUnit, Proofs}; use super::{CurrencyUnit, Nut10Secret, Proofs, SpendingConditions};
use crate::mint_url::MintUrl; use crate::mint_url::MintUrl;
use crate::nuts::nut10::Kind;
use crate::Amount; use crate::Amount;
const PAYMENT_REQUEST_PREFIX: &str = "creqA"; const PAYMENT_REQUEST_PREFIX: &str = "creqA";
@@ -146,6 +149,91 @@ impl AsRef<String> for Transport {
} }
} }
/// Secret Data without nonce for payment requests
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SecretDataRequest {
/// Expresses the spending condition specific to each kind
pub data: String,
/// Additional data committed to and can be used for feature extensions
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<Vec<String>>>,
}
/// Nut10Secret without nonce for payment requests
#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize)]
pub struct Nut10SecretRequest {
/// Kind of the spending condition
pub kind: Kind,
/// Secret Data without nonce
pub secret_data: SecretDataRequest,
}
impl Nut10SecretRequest {
/// Create a new Nut10SecretRequest
pub fn new<S, V>(kind: Kind, data: S, tags: Option<V>) -> Self
where
S: Into<String>,
V: Into<Vec<Vec<String>>>,
{
let secret_data = SecretDataRequest {
data: data.into(),
tags: tags.map(|v| v.into()),
};
Self { kind, secret_data }
}
}
impl From<Nut10Secret> for Nut10SecretRequest {
fn from(secret: Nut10Secret) -> Self {
let secret_data = SecretDataRequest {
data: secret.secret_data.data,
tags: secret.secret_data.tags,
};
Self {
kind: secret.kind,
secret_data,
}
}
}
impl From<Nut10SecretRequest> for Nut10Secret {
fn from(value: Nut10SecretRequest) -> Self {
Self::new(value.kind, value.secret_data.data, value.secret_data.tags)
}
}
impl From<SpendingConditions> for Nut10SecretRequest {
fn from(conditions: SpendingConditions) -> Self {
match conditions {
SpendingConditions::P2PKConditions { data, conditions } => {
Self::new(Kind::P2PK, data.to_hex(), conditions)
}
SpendingConditions::HTLCConditions { data, conditions } => {
Self::new(Kind::HTLC, data.to_string(), conditions)
}
}
}
}
impl Serialize for Nut10SecretRequest {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Create a tuple representing the struct fields
let secret_tuple = (&self.kind, &self.secret_data);
// Serialize the tuple as a JSON array
let mut s = serializer.serialize_tuple(2)?;
s.serialize_element(&secret_tuple.0)?;
s.serialize_element(&secret_tuple.1)?;
s.end()
}
}
/// Payment Request /// Payment Request
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct PaymentRequest { pub struct PaymentRequest {
@@ -169,7 +257,10 @@ pub struct PaymentRequest {
pub description: Option<String>, pub description: Option<String>,
/// Transport /// Transport
#[serde(rename = "t")] #[serde(rename = "t")]
pub transports: Vec<Transport>, #[serde(skip_serializing_if = "Option::is_none")]
pub transports: Option<Vec<Transport>>,
/// Nut10
pub nut10: Option<Nut10SecretRequest>,
} }
impl PaymentRequest { impl PaymentRequest {
@@ -189,6 +280,7 @@ pub struct PaymentRequestBuilder {
mints: Option<Vec<MintUrl>>, mints: Option<Vec<MintUrl>>,
description: Option<String>, description: Option<String>,
transports: Vec<Transport>, transports: Vec<Transport>,
nut10: Option<Nut10SecretRequest>,
} }
impl PaymentRequestBuilder { impl PaymentRequestBuilder {
@@ -252,8 +344,16 @@ impl PaymentRequestBuilder {
self self
} }
/// Set Nut10 secret
pub fn nut10(mut self, nut10: Nut10SecretRequest) -> Self {
self.nut10 = Some(nut10);
self
}
/// Build the PaymentRequest /// Build the PaymentRequest
pub fn build(self) -> PaymentRequest { pub fn build(self) -> PaymentRequest {
let transports = self.transports.is_empty().not().then_some(self.transports);
PaymentRequest { PaymentRequest {
payment_id: self.payment_id, payment_id: self.payment_id,
amount: self.amount, amount: self.amount,
@@ -261,7 +361,8 @@ impl PaymentRequestBuilder {
single_use: self.single_use, single_use: self.single_use,
mints: self.mints, mints: self.mints,
description: self.description, description: self.description,
transports: self.transports, transports,
nut10: self.nut10,
} }
} }
} }
@@ -334,7 +435,8 @@ mod tests {
); );
assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat); assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat);
let transport = req.transports.first().unwrap(); let transport = req.transports.unwrap();
let transport = transport.first().unwrap();
let expected_transport = Transport {_type: TransportType::Nostr, target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), tags: Some(vec![vec!["n".to_string(), "17".to_string()]])}; let expected_transport = Transport {_type: TransportType::Nostr, target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), tags: Some(vec![vec!["n".to_string(), "17".to_string()]])};
@@ -354,7 +456,8 @@ mod tests {
.parse() .parse()
.expect("valid mint url")]), .expect("valid mint url")]),
description: None, description: None,
transports: vec![transport.clone()], transports: Some(vec![transport.clone()]),
nut10: None,
}; };
let request_str = request.to_string(); let request_str = request.to_string();
@@ -370,7 +473,8 @@ mod tests {
); );
assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat); assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat);
let t = req.transports.first().unwrap(); let t = req.transports.unwrap();
let t = t.first().unwrap();
assert_eq!(&transport, t); assert_eq!(&transport, t);
} }
@@ -400,7 +504,8 @@ mod tests {
assert_eq!(request.unit.clone().unwrap(), CurrencyUnit::Sat); assert_eq!(request.unit.clone().unwrap(), CurrencyUnit::Sat);
assert_eq!(request.mints.clone().unwrap(), vec![mint_url]); assert_eq!(request.mints.clone().unwrap(), vec![mint_url]);
let t = request.transports.first().unwrap(); let t = request.transports.clone().unwrap();
let t = t.first().unwrap();
assert_eq!(&transport, t); assert_eq!(&transport, t);
// Test serialization and deserialization // Test serialization and deserialization
@@ -434,4 +539,53 @@ mod tests {
let result = TransportBuilder::default().build(); let result = TransportBuilder::default().build();
assert!(result.is_err()); assert!(result.is_err());
} }
#[test]
fn test_nut10_secret_request() {
use crate::nuts::nut10::Kind;
// Create a Nut10SecretRequest
let secret_request = Nut10SecretRequest::new(
Kind::P2PK,
"026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198",
Some(vec![vec!["key".to_string(), "value".to_string()]]),
);
// Convert to a full Nut10Secret
let full_secret: Nut10Secret = secret_request.clone().into();
// Check conversion
assert_eq!(full_secret.kind, Kind::P2PK);
assert_eq!(
full_secret.secret_data.data,
"026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198"
);
assert_eq!(
full_secret.secret_data.tags,
Some(vec![vec!["key".to_string(), "value".to_string()]])
);
// Convert back to Nut10SecretRequest
let converted_back = Nut10SecretRequest::from(full_secret);
// Check round-trip conversion
assert_eq!(converted_back.kind, secret_request.kind);
assert_eq!(
converted_back.secret_data.data,
secret_request.secret_data.data
);
assert_eq!(
converted_back.secret_data.tags,
secret_request.secret_data.tags
);
// Test in PaymentRequest builder
let payment_request = PaymentRequest::builder()
.payment_id("test123")
.amount(Amount::from(100))
.nut10(secret_request.clone())
.build();
assert_eq!(payment_request.nut10, Some(secret_request));
}
} }

View File

@@ -1,5 +1,10 @@
use anyhow::Result; use std::str::FromStr;
use cdk::nuts::nut18::TransportType;
use anyhow::{bail, Result};
use bitcoin::hashes::sha256::Hash as Sha256Hash;
use cdk::nuts::nut01::PublicKey;
use cdk::nuts::nut11::{Conditions, SigFlag, SpendingConditions};
use cdk::nuts::nut18::{Nut10SecretRequest, TransportType};
use cdk::nuts::{CurrencyUnit, PaymentRequest, PaymentRequestPayload, Token, Transport}; use cdk::nuts::{CurrencyUnit, PaymentRequest, PaymentRequestPayload, Token, Transport};
use cdk::wallet::{MultiMintWallet, ReceiveOptions}; use cdk::wallet::{MultiMintWallet, ReceiveOptions};
use clap::Args; use clap::Args;
@@ -16,23 +21,41 @@ pub struct CreateRequestSubCommand {
unit: String, unit: String,
/// Quote description /// Quote description
description: Option<String>, description: Option<String>,
/// P2PK: Public key(s) for which the token can be spent with valid signature(s)
/// Can be specified multiple times for multiple pubkeys
#[arg(long, action = clap::ArgAction::Append)]
pubkey: Option<Vec<String>>,
/// Number of required signatures (for multiple pubkeys)
/// Defaults to 1 if not specified
#[arg(long, default_value = "1")]
num_sigs: u64,
/// HTLC: Hash for hash time locked contract
#[arg(long, conflicts_with = "preimage")]
hash: Option<String>,
/// HTLC: Preimage of the hash (to be used instead of hash)
#[arg(long, conflicts_with = "hash")]
preimage: Option<String>,
/// Transport type to use (nostr, http, or none)
/// - nostr: Use Nostr transport and listen for payment
/// - http: Use HTTP transport but only print the request
/// - none: Don't use any transport, just print the request
#[arg(long, default_value = "nostr")]
transport: String,
/// URL for HTTP transport (only used when transport=http)
#[arg(long)]
http_url: Option<String>,
/// Nostr relays to use (only used when transport=nostr)
/// Can be specified multiple times for multiple relays
/// If not provided, defaults to standard relays
#[arg(long, action = clap::ArgAction::Append)]
nostr_relay: Option<Vec<String>>,
} }
pub async fn create_request( pub async fn create_request(
multi_mint_wallet: &MultiMintWallet, multi_mint_wallet: &MultiMintWallet,
sub_command_args: &CreateRequestSubCommand, sub_command_args: &CreateRequestSubCommand,
) -> Result<()> { ) -> Result<()> {
let keys = Keys::generate(); // Get available mints from the wallet
let relays = vec!["wss://relay.nos.social", "wss://relay.damus.io"];
let nprofile = Nip19Profile::new(keys.public_key, relays.clone())?;
let nostr_transport = Transport {
_type: TransportType::Nostr,
target: nprofile.to_bech32()?,
tags: Some(vec![vec!["n".to_string(), "17".to_string()]]),
};
let mints: Vec<cdk::mint_url::MintUrl> = multi_mint_wallet let mints: Vec<cdk::mint_url::MintUrl> = multi_mint_wallet
.get_balances(&CurrencyUnit::Sat) .get_balances(&CurrencyUnit::Sat)
.await? .await?
@@ -40,58 +63,234 @@ pub async fn create_request(
.cloned() .cloned()
.collect(); .collect();
// Process transport based on command line args
let transport_type = sub_command_args.transport.to_lowercase();
let transports = match transport_type.as_str() {
"nostr" => {
let keys = Keys::generate();
// Use custom relays if provided, otherwise use defaults
let relays = if let Some(custom_relays) = &sub_command_args.nostr_relay {
if !custom_relays.is_empty() {
println!("Using custom Nostr relays: {:?}", custom_relays);
custom_relays.clone()
} else {
// Empty vector provided, fall back to defaults
vec![
"wss://relay.nos.social".to_string(),
"wss://relay.damus.io".to_string(),
]
}
} else {
// No relays provided, use defaults
vec![
"wss://relay.nos.social".to_string(),
"wss://relay.damus.io".to_string(),
]
};
let nprofile = Nip19Profile::new(keys.public_key, relays.clone())?;
let nostr_transport = Transport {
_type: TransportType::Nostr,
target: nprofile.to_bech32()?,
tags: Some(vec![vec!["n".to_string(), "17".to_string()]]),
};
// We'll need the Nostr keys and relays later for listening
let transport_info = Some((keys, relays, nprofile.public_key));
(Some(vec![nostr_transport]), transport_info)
}
"http" => {
if let Some(url) = &sub_command_args.http_url {
let http_transport = Transport {
_type: TransportType::HttpPost,
target: url.clone(),
tags: None,
};
(Some(vec![http_transport]), None)
} else {
println!(
"Warning: HTTP transport selected but no URL provided, skipping transport"
);
(None, None)
}
}
"none" => (None, None),
_ => {
println!(
"Warning: Unknown transport type '{}', defaulting to none",
transport_type
);
(None, None)
}
};
// Create spending conditions based on provided arguments
// Handle the following cases:
// 1. Only P2PK condition
// 2. Only HTLC condition with hash
// 3. Only HTLC condition with preimage
// 4. Both P2PK and HTLC conditions
let spending_conditions = if let Some(pubkey_strings) = &sub_command_args.pubkey {
// Parse all pubkeys
let mut parsed_pubkeys = Vec::new();
for pubkey_str in pubkey_strings {
match PublicKey::from_str(pubkey_str) {
Ok(pubkey) => parsed_pubkeys.push(pubkey),
Err(err) => {
println!("Error parsing pubkey {}: {}", pubkey_str, err);
// Continue with other pubkeys
}
}
}
if parsed_pubkeys.is_empty() {
println!("No valid pubkeys provided");
None
} else {
// We have pubkeys for P2PK condition
let num_sigs = sub_command_args.num_sigs.min(parsed_pubkeys.len() as u64);
// Check if we also have an HTLC condition
if let Some(hash_str) = &sub_command_args.hash {
// Create conditions with the pubkeys
let conditions = Conditions {
locktime: None,
pubkeys: Some(parsed_pubkeys),
refund_keys: None,
num_sigs: Some(num_sigs),
sig_flag: SigFlag::SigInputs,
};
// Try to parse the hash
match Sha256Hash::from_str(hash_str) {
Ok(hash) => {
// Create HTLC condition with P2PK in the conditions
Some(SpendingConditions::HTLCConditions {
data: hash,
conditions: Some(conditions),
})
}
Err(err) => {
println!("Error parsing hash: {}", err);
// Fallback to just P2PK with multiple pubkeys
bail!("Error parsing hash");
}
}
} else if let Some(preimage) = &sub_command_args.preimage {
// Create conditions with the pubkeys
let conditions = Conditions {
locktime: None,
pubkeys: Some(parsed_pubkeys),
refund_keys: None,
num_sigs: Some(num_sigs),
sig_flag: SigFlag::SigInputs,
};
// Create HTLC conditions with the hash and pubkeys in conditions
Some(SpendingConditions::new_htlc(
preimage.to_string(),
Some(conditions),
)?)
} else {
// Only P2PK condition with multiple pubkeys
Some(SpendingConditions::new_p2pk(
*parsed_pubkeys.first().unwrap(),
Some(Conditions {
locktime: None,
pubkeys: Some(parsed_pubkeys[1..].to_vec()),
refund_keys: None,
num_sigs: Some(num_sigs),
sig_flag: SigFlag::SigInputs,
}),
))
}
}
} else if let Some(hash_str) = &sub_command_args.hash {
// Only HTLC condition with provided hash
match Sha256Hash::from_str(hash_str) {
Ok(hash) => Some(SpendingConditions::HTLCConditions {
data: hash,
conditions: None,
}),
Err(err) => {
println!("Error parsing hash: {}", err);
None
}
}
} else if let Some(preimage) = &sub_command_args.preimage {
// Only HTLC condition with provided preimage
// For HTLC, create the hash from the preimage and use it directly
Some(SpendingConditions::new_htlc(preimage.to_string(), None)?)
} else {
None
};
// Convert SpendingConditions to Nut10SecretRequest
let nut10 = spending_conditions.map(Nut10SecretRequest::from);
// Extract the transports option from our match result
let (transports_option, nostr_info) = transports;
let req = PaymentRequest { let req = PaymentRequest {
payment_id: None, payment_id: None,
amount: sub_command_args.amount.map(|a| a.into()), amount: sub_command_args.amount.map(|a| a.into()),
unit: None, unit: Some(CurrencyUnit::from_str(&sub_command_args.unit)?),
single_use: Some(true), single_use: Some(true),
mints: Some(mints), mints: Some(mints),
description: sub_command_args.description.clone(), description: sub_command_args.description.clone(),
transports: vec![nostr_transport], transports: transports_option,
nut10,
}; };
// Always print the request
println!("{req}"); println!("{req}");
let client = NostrClient::new(keys); // Only listen for Nostr payment if Nostr transport was selected
if let Some((keys, relays, pubkey)) = nostr_info {
println!("Listening for payment via Nostr...");
let filter = Filter::new().pubkey(nprofile.public_key); let client = NostrClient::new(keys);
let filter = Filter::new().pubkey(pubkey);
for relay in relays { for relay in relays {
client.add_read_relay(relay).await?; client.add_read_relay(relay).await?;
}
client.connect().await;
client.subscribe(vec![filter], None).await?;
// Handle subscription notifications with `handle_notifications` method
client
.handle_notifications(|notification| async {
let mut exit = false;
if let RelayPoolNotification::Event {
subscription_id: _,
event,
..
} = notification
{
let unwrapped = client.unwrap_gift_wrap(&event).await?;
let rumor = unwrapped.rumor;
let payload: PaymentRequestPayload = serde_json::from_str(&rumor.content)?;
let token =
Token::new(payload.mint, payload.proofs, payload.memo, payload.unit);
let amount = multi_mint_wallet
.receive(&token.to_string(), ReceiveOptions::default())
.await?;
println!("Received {amount}");
exit = true;
}
Ok(exit) // Set to true to exit from the loop
})
.await?;
} }
client.connect().await;
client.subscribe(vec![filter], None).await?;
// Handle subscription notifications with `handle_notifications` method
client
.handle_notifications(|notification| async {
let mut exit = false;
if let RelayPoolNotification::Event {
subscription_id: _,
event,
..
} = notification
{
let unwrapped = client.unwrap_gift_wrap(&event).await?;
let rumor = unwrapped.rumor;
let payload: PaymentRequestPayload = serde_json::from_str(&rumor.content)?;
let token = Token::new(payload.mint, payload.proofs, payload.memo, payload.unit);
let amount = multi_mint_wallet
.receive(&token.to_string(), ReceiveOptions::default())
.await?;
println!("Received {amount}");
exit = true;
}
Ok(exit) // Set to true to exit from the loop
})
.await?;
Ok(()) Ok(())
} }

View File

@@ -2,7 +2,7 @@ use std::io::{self, Write};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use cdk::nuts::nut18::TransportType; use cdk::nuts::nut18::TransportType;
use cdk::nuts::{PaymentRequest, PaymentRequestPayload}; use cdk::nuts::{PaymentRequest, PaymentRequestPayload, Token};
use cdk::wallet::{MultiMintWallet, SendOptions}; use cdk::wallet::{MultiMintWallet, SendOptions};
use clap::Args; use clap::Args;
use nostr_sdk::nips::nip19::Nip19Profile; use nostr_sdk::nips::nip19::Nip19Profile;
@@ -67,18 +67,20 @@ pub async fn pay_request(
let matching_wallet = matching_wallets.first().unwrap(); let matching_wallet = matching_wallets.first().unwrap();
// We prefer nostr transport if it is available to hide ip. let transports = payment_request
let transport = payment_request
.transports .transports
.clone()
.ok_or(anyhow!("Cannot pay request without transport"))?;
// We prefer nostr transport if it is available to hide ip.
let transport = transports
.iter() .iter()
.find(|t| t._type == TransportType::Nostr) .find(|t| t._type == TransportType::Nostr)
.or_else(|| { .or_else(|| {
payment_request transports
.transports
.iter() .iter()
.find(|t| t._type == TransportType::HttpPost) .find(|t| t._type == TransportType::HttpPost)
}) });
.ok_or(anyhow!("No supported transport method found"))?;
let prepared_send = matching_wallet let prepared_send = matching_wallet
.prepare_send( .prepare_send(
@@ -91,81 +93,92 @@ pub async fn pay_request(
.await?; .await?;
let proofs = matching_wallet.send(prepared_send, None).await?.proofs(); let proofs = matching_wallet.send(prepared_send, None).await?.proofs();
let payload = PaymentRequestPayload { if let Some(transport) = transport {
id: payment_request.payment_id.clone(), let payload = PaymentRequestPayload {
memo: None, id: payment_request.payment_id.clone(),
mint: matching_wallet.mint_url.clone(), memo: None,
unit: matching_wallet.unit.clone(), mint: matching_wallet.mint_url.clone(),
proofs, unit: matching_wallet.unit.clone(),
}; proofs,
};
match transport._type { match transport._type {
TransportType::Nostr => { TransportType::Nostr => {
let keys = Keys::generate(); let keys = Keys::generate();
let client = NostrClient::new(keys); let client = NostrClient::new(keys);
let nprofile = Nip19Profile::from_bech32(&transport.target)?; let nprofile = Nip19Profile::from_bech32(&transport.target)?;
println!("{:?}", nprofile.relays); println!("{:?}", nprofile.relays);
let rumor = EventBuilder::new( let rumor = EventBuilder::new(
nostr_sdk::Kind::from_u16(14), nostr_sdk::Kind::from_u16(14),
serde_json::to_string(&payload)?, serde_json::to_string(&payload)?,
[], [],
); );
let relays = nprofile.relays; let relays = nprofile.relays;
for relay in relays.iter() { for relay in relays.iter() {
client.add_write_relay(relay).await?; client.add_write_relay(relay).await?;
} }
client.connect().await; client.connect().await;
let gift_wrap = client let gift_wrap = client
.gift_wrap_to(relays, &nprofile.public_key, rumor, None) .gift_wrap_to(relays, &nprofile.public_key, rumor, None)
.await?; .await?;
println!(
"Published event {} succufully to {}",
gift_wrap.val,
gift_wrap
.success
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.join(", ")
);
if !gift_wrap.failed.is_empty() {
println!( println!(
"Could not publish to {:?}", "Published event {} succufully to {}",
gift_wrap.val,
gift_wrap gift_wrap
.failed .success
.keys() .iter()
.map(|relay| relay.to_string()) .map(|s| s.to_string())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", ") .join(", ")
); );
if !gift_wrap.failed.is_empty() {
println!(
"Could not publish to {:?}",
gift_wrap
.failed
.keys()
.map(|relay| relay.to_string())
.collect::<Vec<_>>()
.join(", ")
);
}
}
TransportType::HttpPost => {
let client = Client::new();
let res = client
.post(transport.target.clone())
.json(&payload)
.send()
.await?;
let status = res.status();
if status.is_success() {
println!("Successfully posted payment");
} else {
println!("{res:?}");
println!("Error posting payment");
}
} }
} }
} else {
TransportType::HttpPost => { // If no transport is available, print the token
let client = Client::new(); let token = Token::new(
matching_wallet.mint_url.clone(),
let res = client proofs,
.post(transport.target.clone()) None,
.json(&payload) matching_wallet.unit.clone(),
.send() );
.await?; println!("Token: {token}");
let status = res.status();
if status.is_success() {
println!("Successfully posted payment");
} else {
println!("{res:?}");
println!("Error posting payment");
}
}
} }
Ok(()) Ok(())