Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NUT18): Payment request #412

Merged
merged 2 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions crates/cdk-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,15 @@ tracing = { version = "0.1", default-features = false, features = ["attributes",
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
rand = "0.8.5"
home = "0.5.5"
nostr-sdk = { version = "0.33.0", default-features = false, features = [
nostr-sdk = { version = "0.35.0", default-features = false, features = [
"nip04",
"nip44"
"nip44",
"nip59"
]}
reqwest = { version = "0.12", default-features = false, features = [
"json",
"rustls-tls",
"rustls-tls-native-roots",
"socks",
]}
url = "2.3"
15 changes: 15 additions & 0 deletions crates/cdk-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ enum Commands {
UpdateMintUrl(sub_commands::update_mint_url::UpdateMintUrlSubCommand),
/// Get proofs from mint.
ListMintProofs,
/// Decode a payment request
DecodeRequest(sub_commands::decode_request::DecodePaymentRequestSubCommand),
/// Pay a payment request
PayRequest(sub_commands::pay_request::PayRequestSubCommand),
/// Create Payment request
CreateRequest(sub_commands::create_request::CreateRequestSubCommand),
}

#[tokio::main]
Expand Down Expand Up @@ -204,5 +210,14 @@ async fn main() -> Result<()> {
Commands::ListMintProofs => {
sub_commands::list_mint_proofs::proofs(&multi_mint_wallet).await
}
Commands::DecodeRequest(sub_command_args) => {
sub_commands::decode_request::decode_payment_request(sub_command_args)
}
Commands::PayRequest(sub_command_args) => {
sub_commands::pay_request::pay_request(&multi_mint_wallet, sub_command_args).await
}
Commands::CreateRequest(sub_command_args) => {
sub_commands::create_request::create_request(&multi_mint_wallet, sub_command_args).await
}
}
}
104 changes: 104 additions & 0 deletions crates/cdk-cli/src/sub_commands/create_request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use anyhow::Result;
use cdk::{
nuts::{
nut18::TransportType, CurrencyUnit, PaymentRequest, PaymentRequestPayload, Token, Transport,
},
wallet::MultiMintWallet,
};
use clap::Args;
use nostr_sdk::prelude::*;
use nostr_sdk::{nips::nip19::Nip19Profile, Client as NostrClient, Filter, Keys, ToBech32};

#[derive(Args)]
pub struct CreateRequestSubCommand {
#[arg(short, long)]
amount: Option<u64>,
/// Currency unit e.g. sat
#[arg(default_value = "sat")]
unit: String,
/// Quote description
description: Option<String>,
}

pub async fn create_request(
multi_mint_wallet: &MultiMintWallet,
sub_command_args: &CreateRequestSubCommand,
) -> Result<()> {
let keys = Keys::generate();
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
.get_balances(&CurrencyUnit::Sat)
.await?
.keys()
.cloned()
.collect();

let req = PaymentRequest {
payment_id: None,
amount: sub_command_args.amount.map(|a| a.into()),
unit: None,
single_use: Some(true),
mints: Some(mints),
description: sub_command_args.description.clone(),
transports: vec![nostr_transport],
};

println!("{}", req);

let client = NostrClient::new(keys);

let filter = Filter::new().pubkey(nprofile.public_key);

for relay in relays {
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,
Some(payload.unit),
);

let amount = multi_mint_wallet
.receive(&token.to_string(), &[], &[])
.await?;

println!("Received {}", amount);
exit = true;
}
Ok(exit) // Set to true to exit from the loop
})
.await?;

Ok(())
}
19 changes: 19 additions & 0 deletions crates/cdk-cli/src/sub_commands/decode_request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use std::str::FromStr;

use anyhow::Result;
use cdk::nuts::PaymentRequest;
use cdk::util::serialize_to_cbor_diag;
use clap::Args;

#[derive(Args)]
pub struct DecodePaymentRequestSubCommand {
/// Payment request
payment_request: String,
}

pub fn decode_payment_request(sub_command_args: &DecodePaymentRequestSubCommand) -> Result<()> {
let payment_request = PaymentRequest::from_str(&sub_command_args.payment_request)?;

println!("{:}", serialize_to_cbor_diag(&payment_request)?);
Ok(())
}
3 changes: 3 additions & 0 deletions crates/cdk-cli/src/sub_commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
pub mod balance;
pub mod burn;
pub mod check_spent;
pub mod create_request;
pub mod decode_request;
pub mod decode_token;
pub mod list_mint_proofs;
pub mod melt;
pub mod mint;
pub mod mint_info;
pub mod pay_request;
pub mod pending_mints;
pub mod receive;
pub mod restore;
Expand Down
177 changes: 177 additions & 0 deletions crates/cdk-cli/src/sub_commands/pay_request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
use std::io::{self, Write};

use anyhow::{anyhow, Result};
use cdk::{
amount::SplitTarget,
nuts::{nut18::TransportType, PaymentRequest, PaymentRequestPayload},
wallet::{MultiMintWallet, SendKind},
};
use clap::Args;
use nostr_sdk::{nips::nip19::Nip19Profile, Client as NostrClient, EventBuilder, FromBech32, Keys};
use reqwest::Client;

#[derive(Args)]
pub struct PayRequestSubCommand {
payment_request: PaymentRequest,
}

pub async fn pay_request(
multi_mint_wallet: &MultiMintWallet,
sub_command_args: &PayRequestSubCommand,
) -> Result<()> {
let payment_request = &sub_command_args.payment_request;

let unit = payment_request.unit;

let amount = match payment_request.amount {
Some(amount) => amount,
None => {
println!("Enter the amount you would like to pay");

let mut user_input = String::new();
let stdin = io::stdin();
io::stdout().flush().unwrap();
stdin.read_line(&mut user_input)?;

let amount: u64 = user_input.trim().parse()?;

amount.into()
}
};

let request_mints = &payment_request.mints;

let wallet_mints = multi_mint_wallet.get_wallets().await;

// Wallets where unit, balance and mint match request
let mut matching_wallets = vec![];

for wallet in wallet_mints.iter() {
let balance = wallet.total_balance().await?;

if let Some(request_mints) = request_mints {
if !request_mints.contains(&wallet.mint_url) {
continue;
}
}

if let Some(unit) = unit {
if wallet.unit != unit {
continue;
}
}

if balance >= amount {
matching_wallets.push(wallet);
}
}

let matching_wallet = matching_wallets.first().unwrap();

// We prefer nostr transport if it is available to hide ip.
let transport = payment_request
.transports
.iter()
.find(|t| t._type == TransportType::Nostr)
.or_else(|| {
payment_request
.transports
.iter()
.find(|t| t._type == TransportType::HttpPost)
})
.ok_or(anyhow!("No supported transport method found"))?;

let proofs = matching_wallet
.send(
amount,
None,
None,
&SplitTarget::default(),
&SendKind::default(),
true,
)
.await?
.proofs()
.get(&matching_wallet.mint_url)
.unwrap()
.clone();

let payload = PaymentRequestPayload {
id: payment_request.payment_id.clone(),
memo: None,
mint: matching_wallet.mint_url.clone(),
unit: matching_wallet.unit,
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)?,
[],
);

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");
}
}
}

Ok(())
}
17 changes: 11 additions & 6 deletions crates/cdk-cli/src/sub_commands/receive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,20 +184,25 @@ async fn nostr_receive(

let client = nostr_sdk::Client::default();

client.add_relays(relays).await?;

client.connect().await;

let events = client.get_events_of(vec![filter], None).await?;
let events = client
.get_events_of(
vec![filter],
nostr_sdk::EventSource::Relays {
timeout: None,
specific_relays: Some(relays),
},
)
.await?;

let mut tokens: HashSet<String> = HashSet::new();

let keys = Keys::from_str(&(nostr_signing_key).to_secret_hex())?;

for event in events {
if event.kind() == Kind::EncryptedDirectMessage {
if let Ok(msg) = nip04::decrypt(keys.secret_key()?, event.author_ref(), event.content())
{
if event.kind == Kind::EncryptedDirectMessage {
if let Ok(msg) = nip04::decrypt(keys.secret_key(), &event.pubkey, event.content) {
if let Some(token) = cdk::wallet::util::token_from_text(&msg) {
tokens.insert(token.to_string());
}
Expand Down
Loading
Loading