mirror of
https://github.com/aljazceru/cdk.git
synced 2026-01-07 06:56:07 +01:00
chore: move pay_request logic into cdk lib (#1028)
* pay request into cdk lib
This commit is contained in:
@@ -20,7 +20,7 @@ redb = ["dep:cdk-redb"]
|
||||
anyhow.workspace = true
|
||||
bip39.workspace = true
|
||||
bitcoin.workspace = true
|
||||
cdk = { workspace = true, default-features = false, features = ["wallet", "auth", "bip353"]}
|
||||
cdk = { workspace = true, default-features = false, features = ["wallet", "auth", "nostr", "bip353"]}
|
||||
cdk-redb = { workspace = true, features = ["wallet"], optional = true }
|
||||
cdk-sqlite = { workspace = true, features = ["wallet"] }
|
||||
clap.workspace = true
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
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::wallet::{MultiMintWallet, ReceiveOptions};
|
||||
use anyhow::Result;
|
||||
use cdk::wallet::{payment_request as pr, MultiMintWallet};
|
||||
use clap::Args;
|
||||
use nostr_sdk::nips::nip19::Nip19Profile;
|
||||
use nostr_sdk::prelude::*;
|
||||
use nostr_sdk::{Client as NostrClient, Filter, Keys, ToBech32};
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct CreateRequestSubCommand {
|
||||
@@ -55,241 +45,30 @@ pub async fn create_request(
|
||||
multi_mint_wallet: &MultiMintWallet,
|
||||
sub_command_args: &CreateRequestSubCommand,
|
||||
) -> 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();
|
||||
|
||||
// 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));
|
||||
|
||||
(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,
|
||||
};
|
||||
|
||||
(vec![http_transport], None)
|
||||
} else {
|
||||
println!(
|
||||
"Warning: HTTP transport selected but no URL provided, skipping transport"
|
||||
);
|
||||
(vec![], None)
|
||||
}
|
||||
}
|
||||
"none" => (vec![], None),
|
||||
_ => {
|
||||
println!("Warning: Unknown transport type '{transport_type}', defaulting to none");
|
||||
(vec![], 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,
|
||||
num_sigs_refund: None,
|
||||
};
|
||||
|
||||
// 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,
|
||||
num_sigs_refund: None,
|
||||
};
|
||||
|
||||
// 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,
|
||||
num_sigs_refund: None,
|
||||
}),
|
||||
))
|
||||
}
|
||||
}
|
||||
} 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, nostr_info) = transports;
|
||||
|
||||
let req = PaymentRequest {
|
||||
payment_id: None,
|
||||
amount: sub_command_args.amount.map(|a| a.into()),
|
||||
unit: Some(CurrencyUnit::from_str(&sub_command_args.unit)?),
|
||||
single_use: Some(true),
|
||||
mints: Some(mints),
|
||||
// Gather parameters for library call
|
||||
let params = pr::CreateRequestParams {
|
||||
amount: sub_command_args.amount,
|
||||
unit: sub_command_args.unit.clone(),
|
||||
description: sub_command_args.description.clone(),
|
||||
transports,
|
||||
nut10,
|
||||
pubkeys: sub_command_args.pubkey.clone(),
|
||||
num_sigs: sub_command_args.num_sigs,
|
||||
hash: sub_command_args.hash.clone(),
|
||||
preimage: sub_command_args.preimage.clone(),
|
||||
transport: sub_command_args.transport.to_lowercase(),
|
||||
http_url: sub_command_args.http_url.clone(),
|
||||
nostr_relays: sub_command_args.nostr_relay.clone(),
|
||||
};
|
||||
|
||||
// Always print the request
|
||||
println!("{req}");
|
||||
let (req, nostr_wait) = multi_mint_wallet.create_request(params).await?;
|
||||
|
||||
// Only listen for Nostr payment if Nostr transport was selected
|
||||
if let Some((keys, relays, pubkey)) = nostr_info {
|
||||
// Print the request to stdout
|
||||
println!("{}", req);
|
||||
|
||||
// If we set up Nostr transport, optionally wait for payment and receive it
|
||||
if let Some(info) = nostr_wait {
|
||||
println!("Listening for payment via Nostr...");
|
||||
|
||||
let client = NostrClient::new(keys);
|
||||
let filter = Filter::new().pubkey(pubkey);
|
||||
|
||||
for relay in relays {
|
||||
client.add_read_relay(relay).await?;
|
||||
}
|
||||
|
||||
client.connect().await;
|
||||
client.subscribe(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?;
|
||||
let amount = multi_mint_wallet.wait_for_nostr_payment(info).await?;
|
||||
println!("Received {}", amount);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
use std::io::{self, Write};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use cdk::nuts::nut18::TransportType;
|
||||
use cdk::nuts::{PaymentRequest, PaymentRequestPayload, Token};
|
||||
use cdk::wallet::{MultiMintWallet, SendOptions};
|
||||
use cdk::nuts::PaymentRequest;
|
||||
use cdk::wallet::MultiMintWallet;
|
||||
use cdk::Amount;
|
||||
use clap::Args;
|
||||
use nostr_sdk::nips::nip19::Nip19Profile;
|
||||
use nostr_sdk::{Client as NostrClient, EventBuilder, FromBech32, Keys};
|
||||
use reqwest::Client;
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct PayRequestSubCommand {
|
||||
@@ -22,7 +19,8 @@ pub async fn pay_request(
|
||||
|
||||
let unit = &payment_request.unit;
|
||||
|
||||
let amount = match payment_request.amount {
|
||||
// Determine amount: use from request or prompt user
|
||||
let amount: Amount = match payment_request.amount {
|
||||
Some(amount) => amount,
|
||||
None => {
|
||||
println!("Enter the amount you would like to pay");
|
||||
@@ -65,132 +63,12 @@ pub async fn pay_request(
|
||||
}
|
||||
}
|
||||
|
||||
let matching_wallet = matching_wallets.first().unwrap();
|
||||
let matching_wallet = matching_wallets
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("No wallet found that can pay this request"))?;
|
||||
|
||||
if payment_request.transports.is_empty() {
|
||||
return Err(anyhow!("Cannot pay request without transport"));
|
||||
}
|
||||
let transports = payment_request.transports.clone();
|
||||
|
||||
// We prefer nostr transport if it is available to hide ip.
|
||||
let transport = transports
|
||||
.iter()
|
||||
.find(|t| t._type == TransportType::Nostr)
|
||||
.or_else(|| {
|
||||
transports
|
||||
.iter()
|
||||
.find(|t| t._type == TransportType::HttpPost)
|
||||
});
|
||||
|
||||
let prepared_send = matching_wallet
|
||||
.prepare_send(
|
||||
amount,
|
||||
SendOptions {
|
||||
include_fee: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let token = prepared_send.confirm(None).await?;
|
||||
|
||||
// We need the keysets information to properly convert from token proof to proof
|
||||
let keysets_info = match matching_wallet
|
||||
.localstore
|
||||
.get_mint_keysets(token.mint_url()?)
|
||||
.await?
|
||||
{
|
||||
Some(keysets_info) => keysets_info,
|
||||
None => matching_wallet.load_mint_keysets().await?, // Hit the keysets endpoint if we don't have the keysets for this Mint
|
||||
};
|
||||
let proofs = token.proofs(&keysets_info)?;
|
||||
|
||||
if let Some(transport) = transport {
|
||||
let payload = PaymentRequestPayload {
|
||||
id: payment_request.payment_id.clone(),
|
||||
memo: None,
|
||||
mint: matching_wallet.mint_url.clone(),
|
||||
unit: matching_wallet.unit.clone(),
|
||||
proofs,
|
||||
};
|
||||
|
||||
match transport._type {
|
||||
TransportType::Nostr => {
|
||||
let keys = Keys::generate();
|
||||
let client = NostrClient::new(keys);
|
||||
let nprofile = Nip19Profile::from_bech32(&transport.target)?;
|
||||
|
||||
println!("{:?}", nprofile.relays);
|
||||
|
||||
let rumor = EventBuilder::new(
|
||||
nostr_sdk::Kind::from_u16(14),
|
||||
serde_json::to_string(&payload)?,
|
||||
)
|
||||
.build(nprofile.public_key);
|
||||
let relays = nprofile.relays;
|
||||
|
||||
for relay in relays.iter() {
|
||||
client.add_write_relay(relay).await?;
|
||||
}
|
||||
|
||||
client.connect().await;
|
||||
|
||||
let gift_wrap = client
|
||||
.gift_wrap_to(relays, &nprofile.public_key, rumor, None)
|
||||
.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!(
|
||||
"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 {
|
||||
// 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(())
|
||||
matching_wallet
|
||||
.pay_request(payment_request.clone(), Some(amount))
|
||||
.await
|
||||
.map_err(|e| anyhow!(e.to_string()))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user