mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-23 07:35:03 +01:00
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:
@@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,14 +21,73 @@ 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<()> {
|
||||||
|
// Get available mints from the wallet
|
||||||
|
let mints: Vec<cdk::mint_url::MintUrl> = multi_mint_wallet
|
||||||
|
.get_balances(&CurrencyUnit::Sat)
|
||||||
|
.await?
|
||||||
|
.keys()
|
||||||
|
.cloned()
|
||||||
|
.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();
|
let keys = Keys::generate();
|
||||||
let relays = vec!["wss://relay.nos.social", "wss://relay.damus.io"];
|
|
||||||
|
// 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 nprofile = Nip19Profile::new(keys.public_key, relays.clone())?;
|
||||||
|
|
||||||
@@ -33,35 +97,171 @@ pub async fn create_request(
|
|||||||
tags: Some(vec![vec!["n".to_string(), "17".to_string()]]),
|
tags: Some(vec![vec!["n".to_string(), "17".to_string()]]),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mints: Vec<cdk::mint_url::MintUrl> = multi_mint_wallet
|
// We'll need the Nostr keys and relays later for listening
|
||||||
.get_balances(&CurrencyUnit::Sat)
|
let transport_info = Some((keys, relays, nprofile.public_key));
|
||||||
.await?
|
|
||||||
.keys()
|
(Some(vec![nostr_transport]), transport_info)
|
||||||
.cloned()
|
}
|
||||||
.collect();
|
"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.connect().await;
|
||||||
|
|
||||||
client.subscribe(vec![filter], None).await?;
|
client.subscribe(vec![filter], None).await?;
|
||||||
|
|
||||||
// Handle subscription notifications with `handle_notifications` method
|
// Handle subscription notifications with `handle_notifications` method
|
||||||
@@ -75,12 +275,10 @@ pub async fn create_request(
|
|||||||
} = notification
|
} = notification
|
||||||
{
|
{
|
||||||
let unwrapped = client.unwrap_gift_wrap(&event).await?;
|
let unwrapped = client.unwrap_gift_wrap(&event).await?;
|
||||||
|
|
||||||
let rumor = unwrapped.rumor;
|
let rumor = unwrapped.rumor;
|
||||||
|
|
||||||
let payload: PaymentRequestPayload = serde_json::from_str(&rumor.content)?;
|
let payload: PaymentRequestPayload = serde_json::from_str(&rumor.content)?;
|
||||||
|
let token =
|
||||||
let token = Token::new(payload.mint, payload.proofs, payload.memo, payload.unit);
|
Token::new(payload.mint, payload.proofs, payload.memo, payload.unit);
|
||||||
|
|
||||||
let amount = multi_mint_wallet
|
let amount = multi_mint_wallet
|
||||||
.receive(&token.to_string(), ReceiveOptions::default())
|
.receive(&token.to_string(), ReceiveOptions::default())
|
||||||
@@ -92,6 +290,7 @@ pub async fn create_request(
|
|||||||
Ok(exit) // Set to true to exit from the loop
|
Ok(exit) // Set to true to exit from the loop
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,6 +93,7 @@ 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();
|
||||||
|
|
||||||
|
if let Some(transport) = transport {
|
||||||
let payload = PaymentRequestPayload {
|
let payload = PaymentRequestPayload {
|
||||||
id: payment_request.payment_id.clone(),
|
id: payment_request.payment_id.clone(),
|
||||||
memo: None,
|
memo: None,
|
||||||
@@ -167,6 +170,16 @@ pub async fn pay_request(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// If no transport is available, print the token
|
||||||
|
let token = Token::new(
|
||||||
|
matching_wallet.mint_url.clone(),
|
||||||
|
proofs,
|
||||||
|
None,
|
||||||
|
matching_wallet.unit.clone(),
|
||||||
|
);
|
||||||
|
println!("Token: {token}");
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user