From 42c3806589bdc82fa59138f97fda7550c0710f94 Mon Sep 17 00:00:00 2001 From: f321x Date: Wed, 17 Jul 2024 00:08:00 +0200 Subject: [PATCH 01/43] splitting up client --- client/Cargo.toml | 4 +- client/src/cli/mod.rs | 97 +++++++++++++++++++++++++++++++ client/src/cli/trade_contract.rs | 36 ++++++++++++ client/src/ecash/mod.rs | 3 +- client/src/escrow_client/mod.rs | 24 +++++--- client/src/escrow_client/nostr.rs | 19 ++++++ client/src/main.rs | 76 +++++++----------------- 7 files changed, 192 insertions(+), 67 deletions(-) create mode 100644 client/src/cli/mod.rs create mode 100644 client/src/cli/trade_contract.rs create mode 100644 client/src/escrow_client/nostr.rs diff --git a/client/Cargo.toml b/client/Cargo.toml index f704f37..f185856 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -16,4 +16,6 @@ serde = "1.0.203" serde_json = "1.0.117" sha2 = "0.10.8" -cashu_escrow_common = { path = "../common" } \ No newline at end of file +cashu_escrow_common = { path = "../common" } +log = "0.4.22" +env_logger = "0.11.3" diff --git a/client/src/cli/mod.rs b/client/src/cli/mod.rs new file mode 100644 index 0000000..a88fb87 --- /dev/null +++ b/client/src/cli/mod.rs @@ -0,0 +1,97 @@ +pub mod trade_contract; + +use super::*; +use cdk::nuts::nut01::PublicKey as EcashPubkey; +use nostr_sdk::Keys as NostrKeys; +use nostr_sdk::PublicKey as NostrPubkey; +use std::str::FromStr; + +pub enum TradeMode { + Buyer, + Seller, +} + +struct RawCliInput { + buyer_npub: String, + seller_npub: String, + seller_ecash_pubkey: String, + buyer_ecash_pubkey: String, + coordinator_npub: String, + nostr_nsec: String, + mode: TradeMode, +} + +pub struct ClientCliInput { + pub mode: TradeMode, + pub trader_nostr_keys: NostrKeys, + pub ecash_pubkey_buyer: EcashPubkey, + pub ecash_pubkey_seller: EcashPubkey, + pub coordinator_nostr_pubkey: NostrPubkey, + pub trade_partner_nostr_pubkey: NostrPubkey, +} + +impl RawCliInput { + async fn parse() -> anyhow::Result { + // information would be communicated OOB in production + let buyer_npub: String = env::var("BUYER_NPUB")?; + let seller_npub: String = env::var("SELLER_NPUB")?; + let coordinator_npub: String = env::var("ESCROW_NPUB")?; + + let mut seller_ecash_pubkey: String = String::new(); + let mut buyer_ecash_pubkey: String = String::new(); + let nostr_nsec: String; + + let mode = match get_user_input("Select mode: (1) buyer, (2) seller: ") + .await? + .as_str() + { + "1" => { + nostr_nsec = env::var("BUYER_NSEC")?; + seller_ecash_pubkey = get_user_input("Enter seller's ecash pubkey: ").await?; + TradeMode::Buyer + } + "2" => { + nostr_nsec = env::var("SELLER_NSEC")?; + buyer_ecash_pubkey = get_user_input("Enter buyer's ecash pubkey: ").await?; + TradeMode::Seller + } + _ => { + panic!("Wrong trading mode selected. Select either (1) buyer or (2) seller"); + } + }; + Ok(Self { + buyer_npub, + seller_npub, + seller_ecash_pubkey, + buyer_ecash_pubkey, + coordinator_npub, + nostr_nsec, + mode, + }) + } +} + +impl ClientCliInput { + pub async fn parse() -> anyhow::Result { + let raw_input = RawCliInput::parse().await?; + + let ecash_pubkey_buyer = EcashPubkey::from_str(&raw_input.buyer_ecash_pubkey)?; + let ecash_pubkey_seller = EcashPubkey::from_str(&raw_input.seller_ecash_pubkey)?; + + let trader_nostr_keys = NostrKeys::from_str(&raw_input.nostr_nsec)?; + let coordinator_nostr_pubkey = NostrPubkey::from_str(&raw_input.coordinator_npub)?; + let trade_partner_nostr_pubkey = match raw_input.mode { + TradeMode::Buyer => NostrPubkey::from_str(&raw_input.seller_npub)?, + TradeMode::Seller => NostrPubkey::from_str(&raw_input.buyer_npub)?, + }; + + Ok(Self { + mode: raw_input.mode, + trader_nostr_keys, + ecash_pubkey_buyer, + ecash_pubkey_seller, + coordinator_nostr_pubkey, + trade_partner_nostr_pubkey, + }) + } +} diff --git a/client/src/cli/trade_contract.rs b/client/src/cli/trade_contract.rs new file mode 100644 index 0000000..b33d8e9 --- /dev/null +++ b/client/src/cli/trade_contract.rs @@ -0,0 +1,36 @@ +use super::*; + +pub trait FromClientCliInput { + fn from_client_cli_input(cli_input: &ClientCliInput) -> anyhow::Result; +} + +impl FromClientCliInput for TradeContract { + fn from_client_cli_input(cli_input: &ClientCliInput) -> anyhow::Result { + debug!("Constructing hard coded client trade contract..."); + let npub_seller: String; + let npub_buyer: String; + + match cli_input.mode { + TradeMode::Buyer => { + npub_seller = cli_input.trade_partner_nostr_pubkey.to_string(); + npub_buyer = cli_input.trader_nostr_keys.public_key().to_bech32()?; + } + TradeMode::Seller => { + npub_buyer = cli_input.trade_partner_nostr_pubkey.to_string(); + npub_seller = cli_input.trader_nostr_keys.public_key().to_bech32()?; + } + } + Ok(TradeContract { + trade_beginning_ts: 1718975980, + trade_description: + "Purchase of one Watermelon for 5000 satoshi. 3 days delivery to ...".to_string(), + trade_mint_url: "https://mint.minibits.cash/Bitcoin".to_string(), + trade_amount_sat: 5000, + npub_seller, + npub_buyer, + time_limit: 3 * 24 * 60 * 60, + seller_ecash_public_key: cli_input.ecash_pubkey_seller.to_string(), + buyer_ecash_public_key: cli_input.ecash_pubkey_buyer.to_string(), + }) + } +} diff --git a/client/src/ecash/mod.rs b/client/src/ecash/mod.rs index 6304e3d..ed34fc0 100644 --- a/client/src/ecash/mod.rs +++ b/client/src/ecash/mod.rs @@ -7,7 +7,6 @@ use cdk::{ nuts::{Conditions, CurrencyUnit, PublicKey, SecretKey, SigFlag, SpendingConditions, Token}, wallet::Wallet, }; -use escrow_client::EscrowUser; use std::str::FromStr; use std::sync::Arc; @@ -23,7 +22,7 @@ impl EcashWallet { let secret = SecretKey::generate(); let trade_pubkey: String = secret.public_key().to_string(); let seed = rand::thread_rng().gen::<[u8; 32]>(); - println!("Trade ecash pubkey: {}", trade_pubkey); + info!("Trade ecash pubkey: {}", trade_pubkey); let wallet = Wallet::new("", CurrencyUnit::Sat, Arc::new(localstore), &seed); diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index 1bac0de..bab2c69 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -1,18 +1,24 @@ +pub mod nostr; + use super::*; use cdk::nuts::PublicKey; +use nostr_sdk::PublicKey as NostrPubkey; pub enum Trader { - Buyer(EscrowUser), - Seller(EscrowUser), + Buyer(ClientEscrowMetadata), + Seller(ClientEscrowMetadata), +} + +pub struct ClientEscrowMetadata { + pub escrow_coordinator_nostr_public_key: NostrPubkey, } -pub struct EscrowUser { - pub trade_beginning_ts: Timestamp, - pub escrow_coordinator_npub: String, - pub escrow_coordinator_cashu_pk: PublicKey, - pub contract: TradeContract, - pub wallet: EcashWallet, - pub nostr_client: NostrClient, +impl ClientEscrowMetadata { + pub fn from_client_cli_input(cli_input: &ClientCliInput) -> anyhow::Result { + Ok(Self { + escrow_coordinator_nostr_public_key: cli_input.coordinator_nostr_pubkey, + }) + } } impl Trader { diff --git a/client/src/escrow_client/nostr.rs b/client/src/escrow_client/nostr.rs new file mode 100644 index 0000000..3bba2c8 --- /dev/null +++ b/client/src/escrow_client/nostr.rs @@ -0,0 +1,19 @@ +use super::*; + +pub struct ClientNostrClient { + pub nostr_client: NostrClient, +} + +impl ClientNostrClient { + pub async fn from_client_cli_input(cli_input: &ClientCliInput) -> anyhow::Result { + let nostr_client = NostrClient::new( + &cli_input + .trader_nostr_keys + .secret_key() + .unwrap() + .to_bech32()?, + ) + .await?; + Ok(Self { nostr_client }) + } +} diff --git a/client/src/main.rs b/client/src/main.rs index 9c8c492..3746b52 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,75 +1,41 @@ +mod cli; mod ecash; mod escrow_client; use std::env; +use crate::cli::trade_contract::FromClientCliInput; use anyhow::anyhow; use cashu_escrow_common as common; +use cli::ClientCliInput; use common::cli::get_user_input; use common::nostr::NostrClient; use common::TradeContract; use dotenv::dotenv; use ecash::EcashWallet; -use escrow_client::{EscrowUser, Trader}; +use escrow_client::{nostr::ClientNostrClient, ClientEscrowMetadata}; +use log::{debug, error, info}; use nostr_sdk::prelude::*; +pub struct ClientEscrow { + pub nostr_client: Option, // option as we intend to make nostr functionality optional + pub ecash_wallet: EcashWallet, + pub escrow_metadata: ClientEscrowMetadata, + pub escrow_contract: TradeContract, +} + #[tokio::main] async fn main() -> anyhow::Result<()> { dotenv().ok(); - // parsing was hacked together last minute :) - // information would be communicated oob - let mut buyer_npub: String = env::var("BUYER_NPUB")?; - let mut seller_npub: String = env::var("SELLER_NPUB")?; - let coordinator_npub: String = env::var("ESCROW_NPUB")?; - let ecash_wallet = EcashWallet::new().await?; - let mut seller_ecash_pubkey: String = String::new(); - let mut buyer_ecash_pubkey: String = String::new(); - let nostr_client: NostrClient; - - let mode = match get_user_input("Select mode: (1) buyer, (2) seller: ") - .await? - .as_str() - { - "1" => { - nostr_client = NostrClient::new(&env::var("BUYER_NSEC")?).await?; - buyer_npub = nostr_client.get_npub()?; - //println!("Buyer npub: {}", &buyer_npub); - seller_ecash_pubkey = get_user_input("Enter seller's ecash pubkey: ").await?; - buyer_ecash_pubkey = ecash_wallet.trade_pubkey.clone(); - String::from("buyer") - } - "2" => { - nostr_client = NostrClient::new(&env::var("SELLER_NSEC")?).await?; - seller_npub = nostr_client.get_npub()?; - //println!("Seller npub: {}", &seller_npub); - seller_ecash_pubkey = ecash_wallet.trade_pubkey.clone(); - buyer_ecash_pubkey = get_user_input("Enter buyer's ecash pubkey: ").await?; - String::from("seller") - } - _ => { - panic!("Wrong trading mode selected. Select either (1) buyer or (2) seller"); - } - }; - - let contract = TradeContract { - trade_beginning_ts: 1718975980, - trade_description: "Purchase of one Watermelon for 5000 satoshi. 3 days delivery to ..." - .to_string(), - trade_mint_url: "https://mint.minibits.cash/Bitcoin".to_string(), - trade_amount_sat: 5000, - npub_seller: seller_npub, - npub_buyer: buyer_npub, - time_limit: 3 * 24 * 60 * 60, - seller_ecash_public_key: seller_ecash_pubkey, - buyer_ecash_public_key: buyer_ecash_pubkey, - }; + env_logger::builder() + .filter_module("client", log::LevelFilter::Debug) + .filter_level(log::LevelFilter::Info) + .init(); - let escrow_user = - EscrowUser::new(contract, ecash_wallet, nostr_client, coordinator_npub).await?; + let cli_input = ClientCliInput::parse().await?; + let escrow_contract = TradeContract::from_client_cli_input(&cli_input)?; + let escrow_metadata = ClientEscrowMetadata::from_client_cli_input(&cli_input)?; + let nostr_client = ClientNostrClient::from_client_cli_input(&cli_input).await?; - match mode.as_str() { - "buyer" => Trader::Buyer(escrow_user).init_trade().await, - "seller" => Trader::Seller(escrow_user).init_trade().await, - _ => return Err(anyhow!("Invalid mode")), - } + Ok(()) } From b6fc2748c6cc95445c7d443e169c6fa5bf32db2e Mon Sep 17 00:00:00 2001 From: f321x Date: Sun, 21 Jul 2024 19:08:39 +0200 Subject: [PATCH 02/43] separate client logic, cleanup --- client/src/cli/mod.rs | 9 + client/src/cli/trade_contract.rs | 5 +- client/src/ecash/mod.rs | 54 +++--- client/src/escrow_client/buyer_utils.rs | 1 + client/src/escrow_client/general_utils.rs | 1 + client/src/escrow_client/mod.rs | 192 ++++++++-------------- client/src/escrow_client/nostr.rs | 19 --- client/src/escrow_client/seller_utils.rs | 1 + client/src/main.rs | 30 ++-- client/src/nostr/client_nostr_utils.rs | 8 + client/src/nostr/mod.rs | 132 +++++++++++++++ 11 files changed, 274 insertions(+), 178 deletions(-) create mode 100644 client/src/escrow_client/buyer_utils.rs create mode 100644 client/src/escrow_client/general_utils.rs delete mode 100644 client/src/escrow_client/nostr.rs create mode 100644 client/src/escrow_client/seller_utils.rs create mode 100644 client/src/nostr/client_nostr_utils.rs create mode 100644 client/src/nostr/mod.rs diff --git a/client/src/cli/mod.rs b/client/src/cli/mod.rs index a88fb87..90456ad 100644 --- a/client/src/cli/mod.rs +++ b/client/src/cli/mod.rs @@ -6,11 +6,13 @@ use nostr_sdk::Keys as NostrKeys; use nostr_sdk::PublicKey as NostrPubkey; use std::str::FromStr; +#[derive(Debug, Clone)] pub enum TradeMode { Buyer, Seller, } +#[derive(Debug)] struct RawCliInput { buyer_npub: String, seller_npub: String, @@ -19,8 +21,10 @@ struct RawCliInput { coordinator_npub: String, nostr_nsec: String, mode: TradeMode, + mint_url: String, } +#[derive(Debug)] pub struct ClientCliInput { pub mode: TradeMode, pub trader_nostr_keys: NostrKeys, @@ -28,6 +32,7 @@ pub struct ClientCliInput { pub ecash_pubkey_seller: EcashPubkey, pub coordinator_nostr_pubkey: NostrPubkey, pub trade_partner_nostr_pubkey: NostrPubkey, + pub mint_url: String, } impl RawCliInput { @@ -36,6 +41,7 @@ impl RawCliInput { let buyer_npub: String = env::var("BUYER_NPUB")?; let seller_npub: String = env::var("SELLER_NPUB")?; let coordinator_npub: String = env::var("ESCROW_NPUB")?; + let mint_url = env::var("MINT_URL")?; let mut seller_ecash_pubkey: String = String::new(); let mut buyer_ecash_pubkey: String = String::new(); @@ -67,6 +73,7 @@ impl RawCliInput { coordinator_npub, nostr_nsec, mode, + mint_url, }) } } @@ -74,6 +81,7 @@ impl RawCliInput { impl ClientCliInput { pub async fn parse() -> anyhow::Result { let raw_input = RawCliInput::parse().await?; + debug!("Raw parsed CLI input: {:?}", raw_input); let ecash_pubkey_buyer = EcashPubkey::from_str(&raw_input.buyer_ecash_pubkey)?; let ecash_pubkey_seller = EcashPubkey::from_str(&raw_input.seller_ecash_pubkey)?; @@ -92,6 +100,7 @@ impl ClientCliInput { ecash_pubkey_seller, coordinator_nostr_pubkey, trade_partner_nostr_pubkey, + mint_url: raw_input.mint_url, }) } } diff --git a/client/src/cli/trade_contract.rs b/client/src/cli/trade_contract.rs index b33d8e9..f7bafb4 100644 --- a/client/src/cli/trade_contract.rs +++ b/client/src/cli/trade_contract.rs @@ -20,11 +20,12 @@ impl FromClientCliInput for TradeContract { npub_seller = cli_input.trader_nostr_keys.public_key().to_bech32()?; } } + + // hardcoded trade contract Ok(TradeContract { - trade_beginning_ts: 1718975980, trade_description: "Purchase of one Watermelon for 5000 satoshi. 3 days delivery to ...".to_string(), - trade_mint_url: "https://mint.minibits.cash/Bitcoin".to_string(), + trade_mint_url: cli_input.mint_url.clone(), trade_amount_sat: 5000, npub_seller, npub_buyer, diff --git a/client/src/ecash/mod.rs b/client/src/ecash/mod.rs index fe78963..d6ce814 100644 --- a/client/src/ecash/mod.rs +++ b/client/src/ecash/mod.rs @@ -1,22 +1,23 @@ use super::*; -use cdk::secp256k1::rand::Rng; +use cdk::nuts::PublicKey as EscrowPubkey; use cdk::{ amount::SplitTarget, cdk_database::WalletMemoryDatabase, - nuts::{Conditions, CurrencyUnit, PublicKey, SecretKey, SigFlag, SpendingConditions, Token}, + nuts::{Conditions, CurrencyUnit, SecretKey, SigFlag, SpendingConditions, Token}, + secp256k1::rand::Rng, wallet::Wallet, }; use std::str::FromStr; use std::sync::Arc; -pub struct EcashWallet { +pub struct ClientEcashWallet { secret: SecretKey, pub wallet: Wallet, pub trade_pubkey: String, } -impl EcashWallet { +impl ClientEcashWallet { pub async fn new(mint_url: &str) -> anyhow::Result { let localstore = WalletMemoryDatabase::default(); let secret = SecretKey::generate(); @@ -33,19 +34,27 @@ impl EcashWallet { }) } - async fn assemble_escrow_conditions( + fn assemble_escrow_conditions( &self, - user: &EscrowUser, + contract: &TradeContract, + escrow_metadata: &ClientEscrowMetadata, ) -> anyhow::Result { - let buyer_pubkey = PublicKey::from_str(user.contract.buyer_ecash_public_key.as_str())?; - let seller_pubkey = PublicKey::from_str(user.contract.seller_ecash_public_key.as_str())?; - let escrow_pubkey_ts = user.escrow_pk_ts.clone(); + let seller_pubkey = EscrowPubkey::from_str(&contract.seller_ecash_public_key)?; + let buyer_pubkey = EscrowPubkey::from_str(&contract.buyer_ecash_public_key)?; + let escrow_pubkey = escrow_metadata + .escrow_coordinator_ecash_public_key + .ok_or(anyhow!("Escrow coordinator ecash public key not set"))?; + let start_timestamp = escrow_metadata + .escrow_start_timestamp + .ok_or(anyhow!("Escrow start timestamp not set"))?; + + let locktime = start_timestamp.as_u64() + contract.time_limit; let spending_conditions = SpendingConditions::new_p2pk( seller_pubkey, Some(Conditions::new( - Some(user.escrow_pk_ts.1.as_u64() + user.contract.time_limit), - Some(vec![buyer_pubkey, escrow_pubkey_ts.0]), + Some(locktime), + Some(vec![buyer_pubkey, escrow_pubkey]), Some(vec![buyer_pubkey]), Some(2), Some(SigFlag::SigAll), @@ -54,13 +63,17 @@ impl EcashWallet { Ok(spending_conditions) } - pub async fn create_escrow_token(&self, user: &EscrowUser) -> anyhow::Result { - let spending_conditions = self.assemble_escrow_conditions(user).await?; + pub async fn create_escrow_token( + &self, + contract: &TradeContract, + escrow_metadata: &ClientEscrowMetadata, + ) -> anyhow::Result { + let spending_conditions = self.assemble_escrow_conditions(contract, escrow_metadata)?; let token = self .wallet .send( - user.contract.trade_amount_sat.into(), - Some(user.contract.trade_description.clone()), + contract.trade_amount_sat.into(), + Some(contract.trade_description.clone()), Some(spending_conditions), &SplitTarget::None, ) @@ -68,13 +81,14 @@ impl EcashWallet { Ok(token) } - pub async fn validate_escrow_token( + pub fn validate_escrow_token( &self, - token: &String, - user: &EscrowUser, + escrow_token: &str, + contract: &TradeContract, + escrow_metadata: &ClientEscrowMetadata, ) -> anyhow::Result { - let spending_conditions = self.assemble_escrow_conditions(user).await?; - let token = Token::from_str(&token)?; + let spending_conditions = self.assemble_escrow_conditions(contract, escrow_metadata)?; + let token = Token::from_str(escrow_token)?; self.wallet.verify_token_p2pk(&token, spending_conditions)?; Ok(token) } diff --git a/client/src/escrow_client/buyer_utils.rs b/client/src/escrow_client/buyer_utils.rs new file mode 100644 index 0000000..4563e55 --- /dev/null +++ b/client/src/escrow_client/buyer_utils.rs @@ -0,0 +1 @@ +use super::*; diff --git a/client/src/escrow_client/general_utils.rs b/client/src/escrow_client/general_utils.rs new file mode 100644 index 0000000..4563e55 --- /dev/null +++ b/client/src/escrow_client/general_utils.rs @@ -0,0 +1 @@ +use super::*; diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index 1a5513f..aea746c 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -1,162 +1,108 @@ -pub mod nostr; +mod buyer_utils; +pub mod general_utils; +mod seller_utils; use super::*; -use cashu_escrow_common::nostr::PubkeyMessage; -use cdk::nuts::PublicKey; -use nostr_sdk::PublicKey as NostrPubkey; - -pub enum Trader { - Buyer(ClientEscrowMetadata), - Seller(ClientEscrowMetadata), -} pub struct ClientEscrowMetadata { pub escrow_coordinator_nostr_public_key: NostrPubkey, + pub escrow_coordinator_ecash_public_key: Option, + pub escrow_start_timestamp: Option, } impl ClientEscrowMetadata { pub fn from_client_cli_input(cli_input: &ClientCliInput) -> anyhow::Result { Ok(Self { escrow_coordinator_nostr_public_key: cli_input.coordinator_nostr_pubkey, + escrow_coordinator_ecash_public_key: None, + escrow_start_timestamp: None, }) } } -impl Trader { - pub async fn init_trade(&self) -> anyhow::Result<()> { - match self { - Trader::Buyer(config) => { - self.buyer_pipeline(config).await?; +impl EscrowClient { + pub async fn from_cli_input(cli_input: &ClientCliInput) -> anyhow::Result { + let escrow_contract = TradeContract::from_client_cli_input(cli_input)?; + let escrow_metadata = ClientEscrowMetadata::from_client_cli_input(cli_input)?; + let nostr_instance = ClientNostrInstance::from_client_cli_input(cli_input).await?; + let ecash_wallet = ClientEcashWallet::new(&cli_input.mint_url).await?; + + Ok(Self { + nostr_instance, + ecash_wallet, + escrow_metadata, + escrow_contract, + mode: cli_input.mode.clone(), + }) + } + + pub async fn init_trade(&mut self) -> anyhow::Result<()> { + Self::common_trade_flow(self).await?; + debug!("Common trade flow completed"); + + match self.mode { + TradeMode::Buyer => { + self.buyer_pipeline().await?; Ok(()) } - Trader::Seller(config) => { - self.seller_pipeline(config).await?; + TradeMode::Seller => { + self.seller_pipeline().await?; Ok(()) } } } - async fn buyer_pipeline(&self, config: &EscrowUser) -> anyhow::Result<()> { - let token = config.wallet.create_escrow_token(config).await?; - dbg!("Sending token to the seller: {}", token.as_str()); + // the common trade flow is similar for both buyer and seller + async fn common_trade_flow(&mut self) -> anyhow::Result<()> { + let coordinator_pk = &self.escrow_metadata.escrow_coordinator_nostr_public_key; - config - .nostr_client - .submit_trade_token_to_seller(&config.contract.npub_seller, &token) + // submits the trade contract to the coordinator to initiate the escrow service + self.nostr_instance + .submit_escrow_contract(&self.escrow_contract, coordinator_pk) .await?; - // either send signature or begin dispute - Ok(()) - } - - async fn seller_pipeline(&self, config: &EscrowUser) -> anyhow::Result<()> { - let escrow_token = config.await_and_validate_trade_token().await?; - - // send product and proof of delivery (oracle) to seller + let escrow_coordinator_pk_ts = self + .nostr_instance + .get_escrow_coordinator_pk(coordinator_pk) + .await?; - // await signature or begin dispute + self.escrow_metadata.escrow_coordinator_ecash_public_key = Some(escrow_coordinator_pk_ts.0); + self.escrow_metadata.escrow_start_timestamp = Some(escrow_coordinator_pk_ts.1); Ok(()) } -} -impl EscrowUser { - pub async fn new( - contract: TradeContract, - wallet: EcashWallet, - nostr_client: NostrClient, - escrow_coordinator_npub: String, - ) -> anyhow::Result { - let escrow_pk_ts = - Self::common_flow(&contract, &escrow_coordinator_npub, &nostr_client).await?; + async fn buyer_pipeline(&self) -> anyhow::Result<()> { + let escrow_contract = &self.escrow_contract; + let client_metadata = &self.escrow_metadata; - Ok(Self { - escrow_coordinator_npub, - escrow_pk_ts, - contract, - wallet, - nostr_client, - }) - } + let escrow_token = self + .ecash_wallet + .create_escrow_token(escrow_contract, client_metadata) + .await?; - async fn common_flow( - contract: &TradeContract, - escrow_coordinator_npub: &String, - nostr_client: &NostrClient, - ) -> anyhow::Result<(PublicKey, Timestamp)> { - nostr_client - .send_escrow_contract(contract, escrow_coordinator_npub) + debug!("Sending token to the seller: {}", escrow_token.as_str()); + + self.nostr_instance + .submit_trade_token_to_seller(&escrow_contract.npub_seller, &escrow_token) .await?; - let escrow_coordinator_pk = - Self::receive_escrow_coordinator_pk(nostr_client, escrow_coordinator_npub).await?; - Ok(escrow_coordinator_pk) + // either send signature or begin dispute + Ok(()) } - async fn parse_escrow_pk(pk_message_json: &String) -> anyhow::Result<(PublicKey, Timestamp)> { - let pkm: PubkeyMessage = serde_json::from_str(pk_message_json)?; - let public_key = PublicKey::from_hex(pkm.escrow_coordinator_pubkey)?; - Ok((public_key, pkm.escrow_start_ts)) - } + async fn seller_pipeline(&self) -> anyhow::Result<()> { + let escrow_contract = &self.escrow_contract; + let client_metadata = &self.escrow_metadata; + let wallet = &self.ecash_wallet; - async fn receive_escrow_coordinator_pk( - nostr_client: &NostrClient, - coordinator_npub: &String, - ) -> anyhow::Result<(PublicKey, Timestamp)> { - let filter_note = Filter::new() - .kind(Kind::EncryptedDirectMessage) - .since(Timestamp::now()) - .author(nostr_sdk::PublicKey::from_bech32(coordinator_npub)?); - nostr_client.client.subscribe(vec![filter_note], None).await; - - let mut notifications = nostr_client.client.notifications(); - - while let Ok(notification) = notifications.recv().await { - if let RelayPoolNotification::Event { event, .. } = notification { - if let Some(decrypted) = nostr_client - .decrypt_msg(&event.content, &event.author()) - .await - { - dbg!("Received event: {:?}", &decrypted); - if let Ok(pk_ts) = Self::parse_escrow_pk(&decrypted).await { - nostr_client.client.unsubscribe_all().await; - return Ok(pk_ts); - } - } - } - } - Err(anyhow!("No valid escrow coordinator public key received")) - } + let _escrow_token = self + .nostr_instance + .await_and_validate_escrow_token(wallet, escrow_contract, client_metadata) + .await?; - async fn await_and_validate_trade_token(&self) -> anyhow::Result { - let filter_note = Filter::new() - .kind(Kind::EncryptedDirectMessage) - .since(self.escrow_pk_ts.1) - .author(nostr_sdk::PublicKey::from_bech32( - &self.contract.npub_buyer, - )?); - self.nostr_client - .client - .subscribe(vec![filter_note], None) - .await; - - let mut notifications = self.nostr_client.client.notifications(); - - while let Ok(notification) = notifications.recv().await { - if let RelayPoolNotification::Event { event, .. } = notification { - if let Some(decrypted) = self - .nostr_client - .decrypt_msg(&event.content, &event.author()) - .await - { - dbg!("Received token event: {:?}", &decrypted); - if let Ok(escrow_token) = - self.wallet.validate_escrow_token(&decrypted, &self).await - { - return Ok(escrow_token); - } - } - } - } - Err(anyhow!("No valid escrow token received")) + // send product and proof of delivery (oracle) to seller + + // await signature or begin dispute + Ok(()) } } diff --git a/client/src/escrow_client/nostr.rs b/client/src/escrow_client/nostr.rs deleted file mode 100644 index 3bba2c8..0000000 --- a/client/src/escrow_client/nostr.rs +++ /dev/null @@ -1,19 +0,0 @@ -use super::*; - -pub struct ClientNostrClient { - pub nostr_client: NostrClient, -} - -impl ClientNostrClient { - pub async fn from_client_cli_input(cli_input: &ClientCliInput) -> anyhow::Result { - let nostr_client = NostrClient::new( - &cli_input - .trader_nostr_keys - .secret_key() - .unwrap() - .to_bech32()?, - ) - .await?; - Ok(Self { nostr_client }) - } -} diff --git a/client/src/escrow_client/seller_utils.rs b/client/src/escrow_client/seller_utils.rs new file mode 100644 index 0000000..4563e55 --- /dev/null +++ b/client/src/escrow_client/seller_utils.rs @@ -0,0 +1 @@ +use super::*; diff --git a/client/src/main.rs b/client/src/main.rs index 3746b52..f823cbc 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,27 +1,29 @@ mod cli; mod ecash; mod escrow_client; +mod nostr; use std::env; -use crate::cli::trade_contract::FromClientCliInput; use anyhow::anyhow; use cashu_escrow_common as common; -use cli::ClientCliInput; -use common::cli::get_user_input; -use common::nostr::NostrClient; -use common::TradeContract; +use cdk::nuts::PublicKey as EcashPubkey; +use cli::{trade_contract::FromClientCliInput, ClientCliInput, TradeMode}; +use common::{cli::get_user_input, nostr::NostrClient, TradeContract}; use dotenv::dotenv; -use ecash::EcashWallet; -use escrow_client::{nostr::ClientNostrClient, ClientEscrowMetadata}; +use ecash::ClientEcashWallet; +use escrow_client::{general_utils::*, *}; use log::{debug, error, info}; +use nostr::ClientNostrInstance; use nostr_sdk::prelude::*; +use nostr_sdk::PublicKey as NostrPubkey; -pub struct ClientEscrow { - pub nostr_client: Option, // option as we intend to make nostr functionality optional - pub ecash_wallet: EcashWallet, - pub escrow_metadata: ClientEscrowMetadata, +pub struct EscrowClient { + pub nostr_instance: ClientNostrInstance, // can either be a Nostr Client or Nostr note signer (without networking) + pub ecash_wallet: ClientEcashWallet, + pub escrow_metadata: ClientEscrowMetadata, // data relevant for the application but not for the outcome of the trade contract pub escrow_contract: TradeContract, + pub mode: TradeMode, } #[tokio::main] @@ -33,9 +35,9 @@ async fn main() -> anyhow::Result<()> { .init(); let cli_input = ClientCliInput::parse().await?; - let escrow_contract = TradeContract::from_client_cli_input(&cli_input)?; - let escrow_metadata = ClientEscrowMetadata::from_client_cli_input(&cli_input)?; - let nostr_client = ClientNostrClient::from_client_cli_input(&cli_input).await?; + let mut escrow_client = EscrowClient::from_cli_input(&cli_input).await?; + + escrow_client.init_trade().await?; Ok(()) } diff --git a/client/src/nostr/client_nostr_utils.rs b/client/src/nostr/client_nostr_utils.rs new file mode 100644 index 0000000..5ad7978 --- /dev/null +++ b/client/src/nostr/client_nostr_utils.rs @@ -0,0 +1,8 @@ +use super::*; +use cashu_escrow_common::nostr::PubkeyMessage; + +pub async fn parse_escrow_pk(pk_message_json: &str) -> anyhow::Result<(EcashPubkey, Timestamp)> { + let pkm: PubkeyMessage = serde_json::from_str(pk_message_json)?; + let coordinator_ecash_pk = EcashPubkey::from_hex(pkm.escrow_coordinator_pubkey)?; + Ok((coordinator_ecash_pk, pkm.escrow_start_ts)) +} diff --git a/client/src/nostr/mod.rs b/client/src/nostr/mod.rs new file mode 100644 index 0000000..a83abcc --- /dev/null +++ b/client/src/nostr/mod.rs @@ -0,0 +1,132 @@ +mod client_nostr_utils; + +use self::client_nostr_utils::*; +use super::*; + +// here we can somehow make NostrInstance generic to be either a full Nostr Client or only a Nostr Signer depending on +// compilation flags or env variables? + +// should we keep the sending functions in the common crate? +// As they are only used by the client. + +pub struct ClientNostrInstance { + pub nostr_client: NostrClient, +} + +impl ClientNostrInstance { + pub async fn from_client_cli_input(cli_input: &ClientCliInput) -> anyhow::Result { + let nostr_client = NostrClient::new( + &cli_input + .trader_nostr_keys + .secret_key() + .unwrap() + .to_bech32()?, + ) + .await?; + Ok(Self { nostr_client }) + } + + // here submit_escrow_contract calls send_escrow_contract to submit the contract via nostr + // maybe this could be some kind of wasm callable function to just return a + // signed event depending on the setup + pub async fn submit_escrow_contract( + &self, + contract: &TradeContract, + coordinator_pk: &NostrPubkey, + ) -> anyhow::Result<()> { + let coordinator_pk_bech32 = coordinator_pk.to_bech32()?; + self.nostr_client + .send_escrow_contract(contract, &coordinator_pk_bech32) + .await?; + Ok(()) + } + + // await the answer to the submitted contract, once the coordinator returns the ecash public key + // the escrow service is confirmed by the coordinator + pub async fn get_escrow_coordinator_pk( + &self, + coordinator_pk: &NostrPubkey, + ) -> anyhow::Result<(EcashPubkey, Timestamp)> { + let filter_note = Filter::new() + .kind(Kind::EncryptedDirectMessage) + .since(Timestamp::now()) + .author(*coordinator_pk); + + let subscription_id = self + .nostr_client + .client + .subscribe(vec![filter_note], None) + .await; + + let mut notifications = self.nostr_client.client.notifications(); + + while let Ok(notification) = notifications.recv().await { + if let RelayPoolNotification::Event { event, .. } = notification { + if let Some(decrypted) = self + .nostr_client + .decrypt_msg(&event.content, &event.author()) + { + debug!("Received event: {:?}", &decrypted); + if let Ok(ecash_pubkey_and_timestamp) = parse_escrow_pk(&decrypted).await { + self.nostr_client.client.unsubscribe(subscription_id).await; + return Ok(ecash_pubkey_and_timestamp); + } + } + } + } + Err(anyhow!("No valid escrow coordinator public key received")) + } + + pub async fn submit_trade_token_to_seller( + &self, + seller_npub: &str, + token: &str, + ) -> anyhow::Result<()> { + self.nostr_client + .send_trade_token_to_seller(seller_npub, token) + .await?; + Ok(()) + } + + pub async fn await_and_validate_escrow_token( + &self, + wallet: &ClientEcashWallet, + contract: &TradeContract, + metadata: &ClientEscrowMetadata, + ) -> anyhow::Result { + let filter_note = Filter::new() + .kind(Kind::EncryptedDirectMessage) + .since( + metadata + .escrow_start_timestamp + .expect("Escrow timestamp not set"), + ) + .author(nostr_sdk::PublicKey::from_bech32(&contract.npub_buyer)?); + + let subscription_id = self + .nostr_client + .client + .subscribe(vec![filter_note], None) + .await; + + let mut notifications = self.nostr_client.client.notifications(); + + while let Ok(notification) = notifications.recv().await { + if let RelayPoolNotification::Event { event, .. } = notification { + if let Some(decrypted) = self + .nostr_client + .decrypt_msg(&event.content, &event.author()) + { + debug!("Received token event: {:?}", &decrypted); + if let Ok(escrow_token) = + wallet.validate_escrow_token(&decrypted, contract, metadata) + { + self.nostr_client.client.unsubscribe(subscription_id).await; + return Ok(escrow_token); + } + } + } + } + Err(anyhow!("No valid escrow token received")) + } +} From 3b9d16c75e665da1f2c3196e807aa86a5d335ab7 Mon Sep 17 00:00:00 2001 From: f321x Date: Sun, 21 Jul 2024 19:12:26 +0200 Subject: [PATCH 03/43] remaining changes --- common/src/nostr/mod.rs | 19 +++++++++++-------- coordinator/src/escrow_coordinator/mod.rs | 1 - shell.nix | 1 + 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/common/src/nostr/mod.rs b/common/src/nostr/mod.rs index 6075888..670aff7 100644 --- a/common/src/nostr/mod.rs +++ b/common/src/nostr/mod.rs @@ -39,7 +39,7 @@ impl NostrClient { Ok(self.keypair.public_key().to_bech32()?) } - pub async fn decrypt_msg(&self, msg: &String, sender_pk: &PublicKey) -> Option { + pub fn decrypt_msg(&self, msg: &str, sender_pk: &PublicKey) -> Option { let decrypted = ndk::nostr::nips::nip04::decrypt(self.keypair.secret_key().unwrap(), sender_pk, msg); if let Ok(decrypted) = decrypted { @@ -48,14 +48,15 @@ impl NostrClient { None } + // coordinator specific function? pub async fn send_escrow_pubkeys( &self, - receivers: (&String, &String), + receivers: (&str, &str), id: &[u8; 32], - trade_pk: &String, + trade_pk: &str, ) -> anyhow::Result<()> { let message = serde_json::to_string(&PubkeyMessage { - escrow_coordinator_pubkey: trade_pk.clone(), + escrow_coordinator_pubkey: trade_pk.to_string(), trade_id_hex: hex::encode(id), escrow_start_ts: Timestamp::now(), })?; @@ -68,10 +69,11 @@ impl NostrClient { Ok(()) } + // client specific function? pub async fn send_escrow_contract( &self, contract: &TradeContract, - coordinator_pk_bech32: &String, + coordinator_pk_bech32: &str, ) -> anyhow::Result<()> { let message = serde_json::to_string(contract)?; dbg!("sending contract to coordinator..."); @@ -85,10 +87,11 @@ impl NostrClient { Ok(()) } - pub async fn submit_trade_token_to_seller( + // client specific function? + pub async fn send_trade_token_to_seller( &self, - seller_npub: &String, - token: &String, + seller_npub: &str, + token: &str, ) -> anyhow::Result<()> { self.client .send_direct_msg(PublicKey::from_bech32(seller_npub)?, token, None) diff --git a/coordinator/src/escrow_coordinator/mod.rs b/coordinator/src/escrow_coordinator/mod.rs index a9cb44d..53cabaa 100644 --- a/coordinator/src/escrow_coordinator/mod.rs +++ b/coordinator/src/escrow_coordinator/mod.rs @@ -48,7 +48,6 @@ impl EscrowCoordinator { if let Some(decrypted) = self .nostr_client .decrypt_msg(&event.content, &event.author()) - .await { dbg!("Received event: {:?}", &decrypted); if let Ok((contract_hash, contract)) = self.parse(decrypted.as_str()).await { diff --git a/shell.nix b/shell.nix index 2676669..c9bd59f 100644 --- a/shell.nix +++ b/shell.nix @@ -11,6 +11,7 @@ in zlib openssl cargo + clippy rustc rustfmt gcc From 71efb919af0c3b5c5808f40e8a36456e6e4da32e Mon Sep 17 00:00:00 2001 From: f321x Date: Sun, 21 Jul 2024 19:18:34 +0200 Subject: [PATCH 04/43] add comments --- client/src/escrow_client/buyer_utils.rs | 2 ++ client/src/escrow_client/general_utils.rs | 2 ++ client/src/escrow_client/seller_utils.rs | 2 ++ client/src/main.rs | 4 ++-- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/client/src/escrow_client/buyer_utils.rs b/client/src/escrow_client/buyer_utils.rs index 4563e55..a410b6f 100644 --- a/client/src/escrow_client/buyer_utils.rs +++ b/client/src/escrow_client/buyer_utils.rs @@ -1 +1,3 @@ use super::*; + +// here we can place buyer specific functions diff --git a/client/src/escrow_client/general_utils.rs b/client/src/escrow_client/general_utils.rs index 4563e55..b075c22 100644 --- a/client/src/escrow_client/general_utils.rs +++ b/client/src/escrow_client/general_utils.rs @@ -1 +1,3 @@ use super::*; + +// here we can place general coordinator functions diff --git a/client/src/escrow_client/seller_utils.rs b/client/src/escrow_client/seller_utils.rs index 4563e55..0e3a5bd 100644 --- a/client/src/escrow_client/seller_utils.rs +++ b/client/src/escrow_client/seller_utils.rs @@ -1 +1,3 @@ use super::*; + +// here we can place seller specific functions diff --git a/client/src/main.rs b/client/src/main.rs index f823cbc..85cfb0c 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -30,8 +30,8 @@ pub struct EscrowClient { async fn main() -> anyhow::Result<()> { dotenv().ok(); env_logger::builder() - .filter_module("client", log::LevelFilter::Debug) - .filter_level(log::LevelFilter::Info) + .filter_module("client", log::LevelFilter::Debug) // logging level of the client + .filter_level(log::LevelFilter::Info) // logging level of all other crates .init(); let cli_input = ClientCliInput::parse().await?; From 29aabb17937d326dbb2143fee1b6bcad9343414b Mon Sep 17 00:00:00 2001 From: f321x Date: Sun, 21 Jul 2024 21:32:54 +0200 Subject: [PATCH 05/43] put trade mode in metadata --- client/src/cli/mod.rs | 2 +- client/src/escrow_client/mod.rs | 5 +++-- client/src/main.rs | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/cli/mod.rs b/client/src/cli/mod.rs index 90456ad..396470a 100644 --- a/client/src/cli/mod.rs +++ b/client/src/cli/mod.rs @@ -6,7 +6,7 @@ use nostr_sdk::Keys as NostrKeys; use nostr_sdk::PublicKey as NostrPubkey; use std::str::FromStr; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub enum TradeMode { Buyer, Seller, diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index aea746c..6e208f9 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -8,6 +8,7 @@ pub struct ClientEscrowMetadata { pub escrow_coordinator_nostr_public_key: NostrPubkey, pub escrow_coordinator_ecash_public_key: Option, pub escrow_start_timestamp: Option, + pub mode: TradeMode, } impl ClientEscrowMetadata { @@ -16,6 +17,7 @@ impl ClientEscrowMetadata { escrow_coordinator_nostr_public_key: cli_input.coordinator_nostr_pubkey, escrow_coordinator_ecash_public_key: None, escrow_start_timestamp: None, + mode: cli_input.mode, }) } } @@ -32,7 +34,6 @@ impl EscrowClient { ecash_wallet, escrow_metadata, escrow_contract, - mode: cli_input.mode.clone(), }) } @@ -40,7 +41,7 @@ impl EscrowClient { Self::common_trade_flow(self).await?; debug!("Common trade flow completed"); - match self.mode { + match self.escrow_metadata.mode { TradeMode::Buyer => { self.buyer_pipeline().await?; Ok(()) diff --git a/client/src/main.rs b/client/src/main.rs index 85cfb0c..e20d376 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -23,7 +23,6 @@ pub struct EscrowClient { pub ecash_wallet: ClientEcashWallet, pub escrow_metadata: ClientEscrowMetadata, // data relevant for the application but not for the outcome of the trade contract pub escrow_contract: TradeContract, - pub mode: TradeMode, } #[tokio::main] From e181c8943834f6f6b960b47e7e1ac605a4d81be8 Mon Sep 17 00:00:00 2001 From: f321x Date: Sun, 21 Jul 2024 21:43:11 +0200 Subject: [PATCH 06/43] minor improvements --- client/src/escrow_client/mod.rs | 7 +++---- client/src/nostr/client_nostr_utils.rs | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index 6e208f9..d594343 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -44,13 +44,12 @@ impl EscrowClient { match self.escrow_metadata.mode { TradeMode::Buyer => { self.buyer_pipeline().await?; - Ok(()) } TradeMode::Seller => { self.seller_pipeline().await?; - Ok(()) } - } + }; + Ok(()) } // the common trade flow is similar for both buyer and seller @@ -62,7 +61,7 @@ impl EscrowClient { .submit_escrow_contract(&self.escrow_contract, coordinator_pk) .await?; - let escrow_coordinator_pk_ts = self + let escrow_coordinator_pk_ts: (EcashPubkey, Timestamp) = self .nostr_instance .get_escrow_coordinator_pk(coordinator_pk) .await?; diff --git a/client/src/nostr/client_nostr_utils.rs b/client/src/nostr/client_nostr_utils.rs index 5ad7978..654a50f 100644 --- a/client/src/nostr/client_nostr_utils.rs +++ b/client/src/nostr/client_nostr_utils.rs @@ -4,5 +4,6 @@ use cashu_escrow_common::nostr::PubkeyMessage; pub async fn parse_escrow_pk(pk_message_json: &str) -> anyhow::Result<(EcashPubkey, Timestamp)> { let pkm: PubkeyMessage = serde_json::from_str(pk_message_json)?; let coordinator_ecash_pk = EcashPubkey::from_hex(pkm.escrow_coordinator_pubkey)?; + Ok((coordinator_ecash_pk, pkm.escrow_start_ts)) } From 9d6ad91543bb5059a49784619d8fa3d4f5744b58 Mon Sep 17 00:00:00 2001 From: rodant Date: Thu, 25 Jul 2024 16:24:04 +0200 Subject: [PATCH 07/43] Create ecash wallet at the parse input phase, the trade parner needs the public key. --- client/src/cli/mod.rs | 11 +++++++++-- client/src/ecash/mod.rs | 1 + client/src/escrow_client/mod.rs | 10 +++++----- client/src/main.rs | 6 +++--- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/client/src/cli/mod.rs b/client/src/cli/mod.rs index 396470a..46cd0c8 100644 --- a/client/src/cli/mod.rs +++ b/client/src/cli/mod.rs @@ -16,6 +16,7 @@ pub enum TradeMode { struct RawCliInput { buyer_npub: String, seller_npub: String, + pub ecash_wallet: ClientEcashWallet, seller_ecash_pubkey: String, buyer_ecash_pubkey: String, coordinator_npub: String, @@ -28,6 +29,7 @@ struct RawCliInput { pub struct ClientCliInput { pub mode: TradeMode, pub trader_nostr_keys: NostrKeys, + pub ecash_wallet: ClientEcashWallet, pub ecash_pubkey_buyer: EcashPubkey, pub ecash_pubkey_seller: EcashPubkey, pub coordinator_nostr_pubkey: NostrPubkey, @@ -43,8 +45,9 @@ impl RawCliInput { let coordinator_npub: String = env::var("ESCROW_NPUB")?; let mint_url = env::var("MINT_URL")?; - let mut seller_ecash_pubkey: String = String::new(); - let mut buyer_ecash_pubkey: String = String::new(); + let ecash_wallet = ClientEcashWallet::new(&mint_url).await?; + let seller_ecash_pubkey: String; + let buyer_ecash_pubkey: String; let nostr_nsec: String; let mode = match get_user_input("Select mode: (1) buyer, (2) seller: ") @@ -53,11 +56,13 @@ impl RawCliInput { { "1" => { nostr_nsec = env::var("BUYER_NSEC")?; + buyer_ecash_pubkey = ecash_wallet.trade_pubkey.clone(); seller_ecash_pubkey = get_user_input("Enter seller's ecash pubkey: ").await?; TradeMode::Buyer } "2" => { nostr_nsec = env::var("SELLER_NSEC")?; + seller_ecash_pubkey = ecash_wallet.trade_pubkey.clone(); buyer_ecash_pubkey = get_user_input("Enter buyer's ecash pubkey: ").await?; TradeMode::Seller } @@ -68,6 +73,7 @@ impl RawCliInput { Ok(Self { buyer_npub, seller_npub, + ecash_wallet, seller_ecash_pubkey, buyer_ecash_pubkey, coordinator_npub, @@ -96,6 +102,7 @@ impl ClientCliInput { Ok(Self { mode: raw_input.mode, trader_nostr_keys, + ecash_wallet: raw_input.ecash_wallet, ecash_pubkey_buyer, ecash_pubkey_seller, coordinator_nostr_pubkey, diff --git a/client/src/ecash/mod.rs b/client/src/ecash/mod.rs index d6ce814..c7f4f58 100644 --- a/client/src/ecash/mod.rs +++ b/client/src/ecash/mod.rs @@ -11,6 +11,7 @@ use cdk::{ use std::str::FromStr; use std::sync::Arc; +#[derive(Debug)] pub struct ClientEcashWallet { secret: SecretKey, pub wallet: Wallet, diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index d594343..aaa4caf 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -23,11 +23,11 @@ impl ClientEscrowMetadata { } impl EscrowClient { - pub async fn from_cli_input(cli_input: &ClientCliInput) -> anyhow::Result { - let escrow_contract = TradeContract::from_client_cli_input(cli_input)?; - let escrow_metadata = ClientEscrowMetadata::from_client_cli_input(cli_input)?; - let nostr_instance = ClientNostrInstance::from_client_cli_input(cli_input).await?; - let ecash_wallet = ClientEcashWallet::new(&cli_input.mint_url).await?; + pub async fn from_cli_input(cli_input: ClientCliInput) -> anyhow::Result { + let escrow_contract = TradeContract::from_client_cli_input(&cli_input)?; + let escrow_metadata = ClientEscrowMetadata::from_client_cli_input(&cli_input)?; + let nostr_instance = ClientNostrInstance::from_client_cli_input(&cli_input).await?; + let ecash_wallet = cli_input.ecash_wallet; Ok(Self { nostr_instance, diff --git a/client/src/main.rs b/client/src/main.rs index e20d376..6ef4690 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -12,8 +12,8 @@ use cli::{trade_contract::FromClientCliInput, ClientCliInput, TradeMode}; use common::{cli::get_user_input, nostr::NostrClient, TradeContract}; use dotenv::dotenv; use ecash::ClientEcashWallet; -use escrow_client::{general_utils::*, *}; -use log::{debug, error, info}; +use escrow_client::*; +use log::{debug, info}; use nostr::ClientNostrInstance; use nostr_sdk::prelude::*; use nostr_sdk::PublicKey as NostrPubkey; @@ -34,7 +34,7 @@ async fn main() -> anyhow::Result<()> { .init(); let cli_input = ClientCliInput::parse().await?; - let mut escrow_client = EscrowClient::from_cli_input(&cli_input).await?; + let mut escrow_client = EscrowClient::from_cli_input(cli_input).await?; escrow_client.init_trade().await?; From c9bb1cfcb8af1da1e217f8503200ab128cb904ff Mon Sep 17 00:00:00 2001 From: rodant Date: Fri, 26 Jul 2024 17:53:22 +0200 Subject: [PATCH 08/43] Fix npubs by usong to_bech32. --- client/src/cli/mod.rs | 4 ++-- client/src/cli/trade_contract.rs | 4 ++-- common/src/nostr/mod.rs | 14 ++++++++------ 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/client/src/cli/mod.rs b/client/src/cli/mod.rs index 46cd0c8..d485481 100644 --- a/client/src/cli/mod.rs +++ b/client/src/cli/mod.rs @@ -95,8 +95,8 @@ impl ClientCliInput { let trader_nostr_keys = NostrKeys::from_str(&raw_input.nostr_nsec)?; let coordinator_nostr_pubkey = NostrPubkey::from_str(&raw_input.coordinator_npub)?; let trade_partner_nostr_pubkey = match raw_input.mode { - TradeMode::Buyer => NostrPubkey::from_str(&raw_input.seller_npub)?, - TradeMode::Seller => NostrPubkey::from_str(&raw_input.buyer_npub)?, + TradeMode::Buyer => NostrPubkey::from_bech32(&raw_input.seller_npub)?, + TradeMode::Seller => NostrPubkey::from_bech32(&raw_input.buyer_npub)?, }; Ok(Self { diff --git a/client/src/cli/trade_contract.rs b/client/src/cli/trade_contract.rs index f7bafb4..1e7aac3 100644 --- a/client/src/cli/trade_contract.rs +++ b/client/src/cli/trade_contract.rs @@ -12,11 +12,11 @@ impl FromClientCliInput for TradeContract { match cli_input.mode { TradeMode::Buyer => { - npub_seller = cli_input.trade_partner_nostr_pubkey.to_string(); + npub_seller = cli_input.trade_partner_nostr_pubkey.to_bech32()?; npub_buyer = cli_input.trader_nostr_keys.public_key().to_bech32()?; } TradeMode::Seller => { - npub_buyer = cli_input.trade_partner_nostr_pubkey.to_string(); + npub_buyer = cli_input.trade_partner_nostr_pubkey.to_bech32()?; npub_seller = cli_input.trader_nostr_keys.public_key().to_bech32()?; } } diff --git a/common/src/nostr/mod.rs b/common/src/nostr/mod.rs index 670aff7..1d17e1d 100644 --- a/common/src/nostr/mod.rs +++ b/common/src/nostr/mod.rs @@ -40,12 +40,14 @@ impl NostrClient { } pub fn decrypt_msg(&self, msg: &str, sender_pk: &PublicKey) -> Option { - let decrypted = - ndk::nostr::nips::nip04::decrypt(self.keypair.secret_key().unwrap(), sender_pk, msg); - if let Ok(decrypted) = decrypted { - return Some(decrypted); - } - None + let decrypted: std::result::Result = self + .keypair + .secret_key() + .map_err(|e| e.into()) + .and_then(|sk| { + ndk::nostr::nips::nip04::decrypt(sk, sender_pk, msg).map_err(|e| e.into()) + }); + decrypted.ok() } // coordinator specific function? From b7288cff9be124e058d0dc6b1e8036904d04e1d5 Mon Sep 17 00:00:00 2001 From: rodant Date: Wed, 31 Jul 2024 08:29:56 +0200 Subject: [PATCH 09/43] rewrite in a more idiomatic form. --- common/src/nostr/mod.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/common/src/nostr/mod.rs b/common/src/nostr/mod.rs index 1d17e1d..1b73ef3 100644 --- a/common/src/nostr/mod.rs +++ b/common/src/nostr/mod.rs @@ -1,5 +1,5 @@ use crate::TradeContract; -use ndk::prelude::*; +use ndk::{nostr::nips::nip04, prelude::*}; use nostr_sdk as ndk; use serde::{Deserialize, Serialize}; @@ -40,14 +40,11 @@ impl NostrClient { } pub fn decrypt_msg(&self, msg: &str, sender_pk: &PublicKey) -> Option { - let decrypted: std::result::Result = self + let secret_key = self .keypair .secret_key() - .map_err(|e| e.into()) - .and_then(|sk| { - ndk::nostr::nips::nip04::decrypt(sk, sender_pk, msg).map_err(|e| e.into()) - }); - decrypted.ok() + .expect("The key pair must be set if we have a valid instance."); + nip04::decrypt(secret_key, sender_pk, msg).ok() } // coordinator specific function? From 64923f7b6c971d999cd30cf813912298b8e2722c Mon Sep 17 00:00:00 2001 From: rodant Date: Fri, 2 Aug 2024 16:11:14 +0200 Subject: [PATCH 10/43] Move EscrowClient struct to its module. --- client/src/ecash/mod.rs | 6 +++--- client/src/escrow_client/mod.rs | 15 +++++++++++---- client/src/main.rs | 7 ------- client/src/nostr/mod.rs | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/client/src/ecash/mod.rs b/client/src/ecash/mod.rs index c7f4f58..cb5092f 100644 --- a/client/src/ecash/mod.rs +++ b/client/src/ecash/mod.rs @@ -38,7 +38,7 @@ impl ClientEcashWallet { fn assemble_escrow_conditions( &self, contract: &TradeContract, - escrow_metadata: &ClientEscrowMetadata, + escrow_metadata: &EscrowClientMetadata, ) -> anyhow::Result { let seller_pubkey = EscrowPubkey::from_str(&contract.seller_ecash_public_key)?; let buyer_pubkey = EscrowPubkey::from_str(&contract.buyer_ecash_public_key)?; @@ -67,7 +67,7 @@ impl ClientEcashWallet { pub async fn create_escrow_token( &self, contract: &TradeContract, - escrow_metadata: &ClientEscrowMetadata, + escrow_metadata: &EscrowClientMetadata, ) -> anyhow::Result { let spending_conditions = self.assemble_escrow_conditions(contract, escrow_metadata)?; let token = self @@ -86,7 +86,7 @@ impl ClientEcashWallet { &self, escrow_token: &str, contract: &TradeContract, - escrow_metadata: &ClientEscrowMetadata, + escrow_metadata: &EscrowClientMetadata, ) -> anyhow::Result { let spending_conditions = self.assemble_escrow_conditions(contract, escrow_metadata)?; let token = Token::from_str(escrow_token)?; diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index aaa4caf..d1e12fe 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -4,14 +4,14 @@ mod seller_utils; use super::*; -pub struct ClientEscrowMetadata { +pub struct EscrowClientMetadata { pub escrow_coordinator_nostr_public_key: NostrPubkey, pub escrow_coordinator_ecash_public_key: Option, pub escrow_start_timestamp: Option, pub mode: TradeMode, } -impl ClientEscrowMetadata { +impl EscrowClientMetadata { pub fn from_client_cli_input(cli_input: &ClientCliInput) -> anyhow::Result { Ok(Self { escrow_coordinator_nostr_public_key: cli_input.coordinator_nostr_pubkey, @@ -22,10 +22,17 @@ impl ClientEscrowMetadata { } } +pub struct EscrowClient { + pub nostr_instance: ClientNostrInstance, // can either be a Nostr Client or Nostr note signer (without networking) + pub ecash_wallet: ClientEcashWallet, + pub escrow_metadata: EscrowClientMetadata, // data relevant for the application but not for the outcome of the trade contract + pub escrow_contract: TradeContract, +} + impl EscrowClient { pub async fn from_cli_input(cli_input: ClientCliInput) -> anyhow::Result { let escrow_contract = TradeContract::from_client_cli_input(&cli_input)?; - let escrow_metadata = ClientEscrowMetadata::from_client_cli_input(&cli_input)?; + let escrow_metadata = EscrowClientMetadata::from_client_cli_input(&cli_input)?; let nostr_instance = ClientNostrInstance::from_client_cli_input(&cli_input).await?; let ecash_wallet = cli_input.ecash_wallet; @@ -38,7 +45,7 @@ impl EscrowClient { } pub async fn init_trade(&mut self) -> anyhow::Result<()> { - Self::common_trade_flow(self).await?; + self.common_trade_flow().await?; debug!("Common trade flow completed"); match self.escrow_metadata.mode { diff --git a/client/src/main.rs b/client/src/main.rs index 6ef4690..05324bd 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -18,13 +18,6 @@ use nostr::ClientNostrInstance; use nostr_sdk::prelude::*; use nostr_sdk::PublicKey as NostrPubkey; -pub struct EscrowClient { - pub nostr_instance: ClientNostrInstance, // can either be a Nostr Client or Nostr note signer (without networking) - pub ecash_wallet: ClientEcashWallet, - pub escrow_metadata: ClientEscrowMetadata, // data relevant for the application but not for the outcome of the trade contract - pub escrow_contract: TradeContract, -} - #[tokio::main] async fn main() -> anyhow::Result<()> { dotenv().ok(); diff --git a/client/src/nostr/mod.rs b/client/src/nostr/mod.rs index a83abcc..f03881b 100644 --- a/client/src/nostr/mod.rs +++ b/client/src/nostr/mod.rs @@ -92,7 +92,7 @@ impl ClientNostrInstance { &self, wallet: &ClientEcashWallet, contract: &TradeContract, - metadata: &ClientEscrowMetadata, + metadata: &EscrowClientMetadata, ) -> anyhow::Result { let filter_note = Filter::new() .kind(Kind::EncryptedDirectMessage) From 236346c53c3f0f936644ac56ce7a2b2562663dc9 Mon Sep 17 00:00:00 2001 From: rodant Date: Fri, 2 Aug 2024 16:43:09 +0200 Subject: [PATCH 11/43] Inline init_ttrade in main. --- client/src/escrow_client/mod.rs | 28 ++++++++++------------------ client/src/main.rs | 6 ++++-- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index d1e12fe..4a5fcf7 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -44,23 +44,8 @@ impl EscrowClient { }) } - pub async fn init_trade(&mut self) -> anyhow::Result<()> { - self.common_trade_flow().await?; - debug!("Common trade flow completed"); - - match self.escrow_metadata.mode { - TradeMode::Buyer => { - self.buyer_pipeline().await?; - } - TradeMode::Seller => { - self.seller_pipeline().await?; - } - }; - Ok(()) - } - // the common trade flow is similar for both buyer and seller - async fn common_trade_flow(&mut self) -> anyhow::Result<()> { + pub async fn common_trade_flow(&mut self) -> anyhow::Result<()> { let coordinator_pk = &self.escrow_metadata.escrow_coordinator_nostr_public_key; // submits the trade contract to the coordinator to initiate the escrow service @@ -78,7 +63,14 @@ impl EscrowClient { Ok(()) } - async fn buyer_pipeline(&self) -> anyhow::Result<()> { + pub async fn rest_trade_flow(&self) -> std::result::Result<(), anyhow::Error> { + match self.escrow_metadata.mode { + TradeMode::Buyer => self.buyer_trade_flow().await, + TradeMode::Seller => self.seller_trade_flow().await, + } + } + + async fn buyer_trade_flow(&self) -> anyhow::Result<()> { let escrow_contract = &self.escrow_contract; let client_metadata = &self.escrow_metadata; @@ -97,7 +89,7 @@ impl EscrowClient { Ok(()) } - async fn seller_pipeline(&self) -> anyhow::Result<()> { + async fn seller_trade_flow(&self) -> anyhow::Result<()> { let escrow_contract = &self.escrow_contract; let client_metadata = &self.escrow_metadata; let wallet = &self.ecash_wallet; diff --git a/client/src/main.rs b/client/src/main.rs index 05324bd..585fe93 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -6,6 +6,7 @@ mod nostr; use std::env; use anyhow::anyhow; +use async_utility::futures_util::future::ok; use cashu_escrow_common as common; use cdk::nuts::PublicKey as EcashPubkey; use cli::{trade_contract::FromClientCliInput, ClientCliInput, TradeMode}; @@ -29,7 +30,8 @@ async fn main() -> anyhow::Result<()> { let cli_input = ClientCliInput::parse().await?; let mut escrow_client = EscrowClient::from_cli_input(cli_input).await?; - escrow_client.init_trade().await?; + escrow_client.common_trade_flow().await?; + debug!("Common trade flow completed"); - Ok(()) + escrow_client.rest_trade_flow().await } From 6b10695b488c505a7692b14d0a9c285e8a5e0f19 Mon Sep 17 00:00:00 2001 From: rodant Date: Mon, 5 Aug 2024 19:11:40 +0200 Subject: [PATCH 12/43] Extract wallet creation from RawCliInput --- client/src/cli/mod.rs | 34 ++++++++------------------------ client/src/cli/trade_contract.rs | 19 +++++++++++++----- client/src/escrow_client/mod.rs | 12 ++++++++--- client/src/main.rs | 8 +++++--- common/src/lib.rs | 1 - 5 files changed, 36 insertions(+), 38 deletions(-) diff --git a/client/src/cli/mod.rs b/client/src/cli/mod.rs index d485481..47aab1d 100644 --- a/client/src/cli/mod.rs +++ b/client/src/cli/mod.rs @@ -16,25 +16,19 @@ pub enum TradeMode { struct RawCliInput { buyer_npub: String, seller_npub: String, - pub ecash_wallet: ClientEcashWallet, - seller_ecash_pubkey: String, - buyer_ecash_pubkey: String, + partner_ecash_pubkey: String, coordinator_npub: String, nostr_nsec: String, mode: TradeMode, - mint_url: String, } #[derive(Debug)] pub struct ClientCliInput { pub mode: TradeMode, pub trader_nostr_keys: NostrKeys, - pub ecash_wallet: ClientEcashWallet, - pub ecash_pubkey_buyer: EcashPubkey, - pub ecash_pubkey_seller: EcashPubkey, + pub ecash_pubkey_partner: EcashPubkey, pub coordinator_nostr_pubkey: NostrPubkey, pub trade_partner_nostr_pubkey: NostrPubkey, - pub mint_url: String, } impl RawCliInput { @@ -43,11 +37,8 @@ impl RawCliInput { let buyer_npub: String = env::var("BUYER_NPUB")?; let seller_npub: String = env::var("SELLER_NPUB")?; let coordinator_npub: String = env::var("ESCROW_NPUB")?; - let mint_url = env::var("MINT_URL")?; - let ecash_wallet = ClientEcashWallet::new(&mint_url).await?; - let seller_ecash_pubkey: String; - let buyer_ecash_pubkey: String; + let partner_ecash_pubkey: String; let nostr_nsec: String; let mode = match get_user_input("Select mode: (1) buyer, (2) seller: ") @@ -56,14 +47,12 @@ impl RawCliInput { { "1" => { nostr_nsec = env::var("BUYER_NSEC")?; - buyer_ecash_pubkey = ecash_wallet.trade_pubkey.clone(); - seller_ecash_pubkey = get_user_input("Enter seller's ecash pubkey: ").await?; + partner_ecash_pubkey = get_user_input("Enter seller's ecash pubkey: ").await?; TradeMode::Buyer } "2" => { nostr_nsec = env::var("SELLER_NSEC")?; - seller_ecash_pubkey = ecash_wallet.trade_pubkey.clone(); - buyer_ecash_pubkey = get_user_input("Enter buyer's ecash pubkey: ").await?; + partner_ecash_pubkey = get_user_input("Enter buyer's ecash pubkey: ").await?; TradeMode::Seller } _ => { @@ -73,13 +62,10 @@ impl RawCliInput { Ok(Self { buyer_npub, seller_npub, - ecash_wallet, - seller_ecash_pubkey, - buyer_ecash_pubkey, + partner_ecash_pubkey, coordinator_npub, nostr_nsec, mode, - mint_url, }) } } @@ -89,8 +75,7 @@ impl ClientCliInput { let raw_input = RawCliInput::parse().await?; debug!("Raw parsed CLI input: {:?}", raw_input); - let ecash_pubkey_buyer = EcashPubkey::from_str(&raw_input.buyer_ecash_pubkey)?; - let ecash_pubkey_seller = EcashPubkey::from_str(&raw_input.seller_ecash_pubkey)?; + let ecash_pubkey_partner = EcashPubkey::from_str(&raw_input.partner_ecash_pubkey)?; let trader_nostr_keys = NostrKeys::from_str(&raw_input.nostr_nsec)?; let coordinator_nostr_pubkey = NostrPubkey::from_str(&raw_input.coordinator_npub)?; @@ -102,12 +87,9 @@ impl ClientCliInput { Ok(Self { mode: raw_input.mode, trader_nostr_keys, - ecash_wallet: raw_input.ecash_wallet, - ecash_pubkey_buyer, - ecash_pubkey_seller, + ecash_pubkey_partner, coordinator_nostr_pubkey, trade_partner_nostr_pubkey, - mint_url: raw_input.mint_url, }) } } diff --git a/client/src/cli/trade_contract.rs b/client/src/cli/trade_contract.rs index 1e7aac3..6e2b4d9 100644 --- a/client/src/cli/trade_contract.rs +++ b/client/src/cli/trade_contract.rs @@ -1,11 +1,17 @@ use super::*; pub trait FromClientCliInput { - fn from_client_cli_input(cli_input: &ClientCliInput) -> anyhow::Result; + fn from_client_cli_input( + cli_input: &ClientCliInput, + trade_pubkey: String, + ) -> anyhow::Result; } impl FromClientCliInput for TradeContract { - fn from_client_cli_input(cli_input: &ClientCliInput) -> anyhow::Result { + fn from_client_cli_input( + cli_input: &ClientCliInput, + trade_pubkey: String, + ) -> anyhow::Result { debug!("Constructing hard coded client trade contract..."); let npub_seller: String; let npub_buyer: String; @@ -21,17 +27,20 @@ impl FromClientCliInput for TradeContract { } } + let (ecash_pubkey_seller, ecash_pubkey_buyer) = match cli_input.mode { + TradeMode::Seller => (trade_pubkey, cli_input.ecash_pubkey_partner.to_string()), + TradeMode::Buyer => (cli_input.ecash_pubkey_partner.to_string(), trade_pubkey), + }; // hardcoded trade contract Ok(TradeContract { trade_description: "Purchase of one Watermelon for 5000 satoshi. 3 days delivery to ...".to_string(), - trade_mint_url: cli_input.mint_url.clone(), trade_amount_sat: 5000, npub_seller, npub_buyer, time_limit: 3 * 24 * 60 * 60, - seller_ecash_public_key: cli_input.ecash_pubkey_seller.to_string(), - buyer_ecash_public_key: cli_input.ecash_pubkey_buyer.to_string(), + seller_ecash_public_key: ecash_pubkey_seller, + buyer_ecash_public_key: ecash_pubkey_buyer, }) } } diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index 4a5fcf7..f34879c 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -2,6 +2,8 @@ mod buyer_utils; pub mod general_utils; mod seller_utils; +use cli::trade_contract::FromClientCliInput; + use super::*; pub struct EscrowClientMetadata { @@ -30,11 +32,15 @@ pub struct EscrowClient { } impl EscrowClient { - pub async fn from_cli_input(cli_input: ClientCliInput) -> anyhow::Result { - let escrow_contract = TradeContract::from_client_cli_input(&cli_input)?; + pub async fn from_cli_input( + cli_input: ClientCliInput, + ecash_wallet: ClientEcashWallet, + ) -> anyhow::Result { + let escrow_contract = + TradeContract::from_client_cli_input(&cli_input, ecash_wallet.trade_pubkey.clone())?; let escrow_metadata = EscrowClientMetadata::from_client_cli_input(&cli_input)?; let nostr_instance = ClientNostrInstance::from_client_cli_input(&cli_input).await?; - let ecash_wallet = cli_input.ecash_wallet; + let ecash_wallet = ecash_wallet; Ok(Self { nostr_instance, diff --git a/client/src/main.rs b/client/src/main.rs index 585fe93..54a651b 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -6,10 +6,9 @@ mod nostr; use std::env; use anyhow::anyhow; -use async_utility::futures_util::future::ok; use cashu_escrow_common as common; use cdk::nuts::PublicKey as EcashPubkey; -use cli::{trade_contract::FromClientCliInput, ClientCliInput, TradeMode}; +use cli::{ClientCliInput, TradeMode}; use common::{cli::get_user_input, nostr::NostrClient, TradeContract}; use dotenv::dotenv; use ecash::ClientEcashWallet; @@ -27,8 +26,11 @@ async fn main() -> anyhow::Result<()> { .filter_level(log::LevelFilter::Info) // logging level of all other crates .init(); + let mint_url = env::var("MINT_URL")?; + let ecash_wallet = ClientEcashWallet::new(&mint_url).await?; + let cli_input = ClientCliInput::parse().await?; - let mut escrow_client = EscrowClient::from_cli_input(cli_input).await?; + let mut escrow_client = EscrowClient::from_cli_input(cli_input, ecash_wallet).await?; escrow_client.common_trade_flow().await?; debug!("Common trade flow completed"); diff --git a/common/src/lib.rs b/common/src/lib.rs index 4cc5f65..f694060 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -3,7 +3,6 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct TradeContract { pub trade_description: String, - pub trade_mint_url: String, pub trade_amount_sat: u64, pub npub_seller: String, pub npub_buyer: String, From 7e28154600fb3fb4067d0e66b574e69dabe66644 Mon Sep 17 00:00:00 2001 From: rodant Date: Tue, 6 Aug 2024 13:25:23 +0200 Subject: [PATCH 13/43] Start introducing the escrow client as state machine for better testability. --- client/src/escrow_client/mod.rs | 54 ++++++++++++++++++++++++++------- client/src/main.rs | 15 ++++++--- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index f34879c..b072ddc 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -2,6 +2,7 @@ mod buyer_utils; pub mod general_utils; mod seller_utils; +use cdk::nuts::Token; use cli::trade_contract::FromClientCliInput; use super::*; @@ -31,7 +32,9 @@ pub struct EscrowClient { pub escrow_contract: TradeContract, } +// todo: model EscrowClient as an state machine (stm). This will improve testability too. impl EscrowClient { + // creates the inital state: the coordinator data isn't present. pub async fn from_cli_input( cli_input: ClientCliInput, ecash_wallet: ClientEcashWallet, @@ -50,8 +53,12 @@ impl EscrowClient { }) } - // the common trade flow is similar for both buyer and seller - pub async fn common_trade_flow(&mut self) -> anyhow::Result<()> { + /// The trade initialization is the same for both buyer and seller. + /// + /// After this the coordinator data is set, state trade registered. + /// + /// After this state the trade contract is effectfull as well, possible coordinator fees must be payed. + pub async fn register_trade(&mut self) -> anyhow::Result<()> { let coordinator_pk = &self.escrow_metadata.escrow_coordinator_nostr_public_key; // submits the trade contract to the coordinator to initiate the escrow service @@ -69,14 +76,28 @@ impl EscrowClient { Ok(()) } - pub async fn rest_trade_flow(&self) -> std::result::Result<(), anyhow::Error> { + /// Depending on the trade mode sends or receives the trade token. + /// + /// After this the state is token sent or received. + pub async fn exchange_trade_token(&self) -> std::result::Result<(), anyhow::Error> { match self.escrow_metadata.mode { - TradeMode::Buyer => self.buyer_trade_flow().await, - TradeMode::Seller => self.seller_trade_flow().await, + TradeMode::Buyer => { + // todo: store the sent token in this instance + self.send_trade_token().await?; + Ok(()) + } + TradeMode::Seller => { + // todo: store the received token in this instance + self.receive_and_validate_trade_token().await?; + Ok(()) + } } } - async fn buyer_trade_flow(&self) -> anyhow::Result<()> { + /// State change for the buyer. The state after that is token sent. + /// + /// Returns the sent trade token by this [`EscrowClient`]. + async fn send_trade_token(&self) -> anyhow::Result { let escrow_contract = &self.escrow_contract; let client_metadata = &self.escrow_metadata; @@ -91,23 +112,34 @@ impl EscrowClient { .submit_trade_token_to_seller(&escrow_contract.npub_seller, &escrow_token) .await?; - // either send signature or begin dispute - Ok(()) + Ok(escrow_token) } - async fn seller_trade_flow(&self) -> anyhow::Result<()> { + /// State change for a seller. The state after this is token received. + /// + /// Returns the received trade token by this [`EscrowClient`]. + async fn receive_and_validate_trade_token(&self) -> anyhow::Result { let escrow_contract = &self.escrow_contract; let client_metadata = &self.escrow_metadata; let wallet = &self.ecash_wallet; - let _escrow_token = self + let escrow_token = self .nostr_instance + // todo: split method in receive and validate steps, single responsability principle. .await_and_validate_escrow_token(wallet, escrow_contract, client_metadata) .await?; - // send product and proof of delivery (oracle) to seller + Ok(escrow_token) + } + /// Depending on the trade mode deliver product/service or sign the token after receiving the service. + /// + /// The state after this operation is duties fulfilled. + pub async fn do_your_trade_duties(&self) -> anyhow::Result<()> { + // todo: as seller send product and proof of delivery (oracle) to seller. // await signature or begin dispute + + // todo: as buyer either send signature or begin dispute Ok(()) } } diff --git a/client/src/main.rs b/client/src/main.rs index 54a651b..8fc48b6 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -27,13 +27,18 @@ async fn main() -> anyhow::Result<()> { .init(); let mint_url = env::var("MINT_URL")?; - let ecash_wallet = ClientEcashWallet::new(&mint_url).await?; + let escrow_wallet = ClientEcashWallet::new(&mint_url).await?; + + //todo: Ensure to have enough funds in the wallet. The buyer must probably transfer some ecash to the escrow wallet. let cli_input = ClientCliInput::parse().await?; - let mut escrow_client = EscrowClient::from_cli_input(cli_input, ecash_wallet).await?; + //todo: create TradeContrac and ExcrowClientMetadata (models) from CLI input and pass them to the EscrowClient. The escrow client shouldn't depend on the CLI module. + let mut escrow_client = EscrowClient::from_cli_input(cli_input, escrow_wallet).await?; + + escrow_client.register_trade().await?; + debug!("Common trade registration completed"); - escrow_client.common_trade_flow().await?; - debug!("Common trade flow completed"); + escrow_client.exchange_trade_token().await?; - escrow_client.rest_trade_flow().await + escrow_client.do_your_trade_duties().await } From 0cf65741b0db52d69b32b063b1f1521e6f393023 Mon Sep 17 00:00:00 2001 From: rodant Date: Tue, 6 Aug 2024 16:32:07 +0200 Subject: [PATCH 14/43] Remove dependency from cli in escrow client. --- client/src/escrow_client/mod.rs | 23 +++++++++-------------- client/src/main.rs | 12 +++++++++++- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index b072ddc..63c3edb 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -3,7 +3,6 @@ pub mod general_utils; mod seller_utils; use cdk::nuts::Token; -use cli::trade_contract::FromClientCliInput; use super::*; @@ -35,22 +34,18 @@ pub struct EscrowClient { // todo: model EscrowClient as an state machine (stm). This will improve testability too. impl EscrowClient { // creates the inital state: the coordinator data isn't present. - pub async fn from_cli_input( - cli_input: ClientCliInput, + pub fn new( + contract: TradeContract, + metadata: EscrowClientMetadata, + nostr_instance: ClientNostrInstance, ecash_wallet: ClientEcashWallet, - ) -> anyhow::Result { - let escrow_contract = - TradeContract::from_client_cli_input(&cli_input, ecash_wallet.trade_pubkey.clone())?; - let escrow_metadata = EscrowClientMetadata::from_client_cli_input(&cli_input)?; - let nostr_instance = ClientNostrInstance::from_client_cli_input(&cli_input).await?; - let ecash_wallet = ecash_wallet; - - Ok(Self { + ) -> Self { + Self { + escrow_contract: contract, + escrow_metadata: metadata, nostr_instance, ecash_wallet, - escrow_metadata, - escrow_contract, - }) + } } /// The trade initialization is the same for both buyer and seller. diff --git a/client/src/main.rs b/client/src/main.rs index 8fc48b6..9e088b1 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -8,6 +8,7 @@ use std::env; use anyhow::anyhow; use cashu_escrow_common as common; use cdk::nuts::PublicKey as EcashPubkey; +use cli::trade_contract::FromClientCliInput; use cli::{ClientCliInput, TradeMode}; use common::{cli::get_user_input, nostr::NostrClient, TradeContract}; use dotenv::dotenv; @@ -33,7 +34,16 @@ async fn main() -> anyhow::Result<()> { let cli_input = ClientCliInput::parse().await?; //todo: create TradeContrac and ExcrowClientMetadata (models) from CLI input and pass them to the EscrowClient. The escrow client shouldn't depend on the CLI module. - let mut escrow_client = EscrowClient::from_cli_input(cli_input, escrow_wallet).await?; + let escrow_contract = + TradeContract::from_client_cli_input(&cli_input, escrow_wallet.trade_pubkey.clone())?; + let escrow_metadata = EscrowClientMetadata::from_client_cli_input(&cli_input)?; + let nostr_instance = ClientNostrInstance::from_client_cli_input(&cli_input).await?; + let mut escrow_client = EscrowClient::new( + escrow_contract, + escrow_metadata, + nostr_instance, + escrow_wallet, + ); escrow_client.register_trade().await?; debug!("Common trade registration completed"); From 7cef1c76e54d99ce87c97dd2438a153e4765425b Mon Sep 17 00:00:00 2001 From: rodant Date: Tue, 6 Aug 2024 16:41:36 +0200 Subject: [PATCH 15/43] Some clean up. --- client/src/escrow_client/mod.rs | 8 ++++---- client/src/main.rs | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index 63c3edb..e20e9de 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -35,14 +35,14 @@ pub struct EscrowClient { impl EscrowClient { // creates the inital state: the coordinator data isn't present. pub fn new( - contract: TradeContract, - metadata: EscrowClientMetadata, + escrow_contract: TradeContract, + escrow_metadata: EscrowClientMetadata, nostr_instance: ClientNostrInstance, ecash_wallet: ClientEcashWallet, ) -> Self { Self { - escrow_contract: contract, - escrow_metadata: metadata, + escrow_contract, + escrow_metadata, nostr_instance, ecash_wallet, } diff --git a/client/src/main.rs b/client/src/main.rs index 9e088b1..19d59bb 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -33,7 +33,6 @@ async fn main() -> anyhow::Result<()> { //todo: Ensure to have enough funds in the wallet. The buyer must probably transfer some ecash to the escrow wallet. let cli_input = ClientCliInput::parse().await?; - //todo: create TradeContrac and ExcrowClientMetadata (models) from CLI input and pass them to the EscrowClient. The escrow client shouldn't depend on the CLI module. let escrow_contract = TradeContract::from_client_cli_input(&cli_input, escrow_wallet.trade_pubkey.clone())?; let escrow_metadata = EscrowClientMetadata::from_client_cli_input(&cli_input)?; From d32dc484bcc474d2adbb7599346bd4ca35f376be Mon Sep 17 00:00:00 2001 From: rodant Date: Tue, 6 Aug 2024 17:02:51 +0200 Subject: [PATCH 16/43] Move trade mode to the escrow client. --- client/src/cli/mod.rs | 6 ------ client/src/escrow_client/mod.rs | 29 ++++++++++++++++++----------- client/src/main.rs | 7 ++++--- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/client/src/cli/mod.rs b/client/src/cli/mod.rs index 47aab1d..17bd2f6 100644 --- a/client/src/cli/mod.rs +++ b/client/src/cli/mod.rs @@ -6,12 +6,6 @@ use nostr_sdk::Keys as NostrKeys; use nostr_sdk::PublicKey as NostrPubkey; use std::str::FromStr; -#[derive(Debug, Clone, Copy)] -pub enum TradeMode { - Buyer, - Seller, -} - #[derive(Debug)] struct RawCliInput { buyer_npub: String, diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index e20e9de..30947ad 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -6,11 +6,16 @@ use cdk::nuts::Token; use super::*; +#[derive(Debug, Clone, Copy)] +pub enum TradeMode { + Buyer, + Seller, +} + pub struct EscrowClientMetadata { pub escrow_coordinator_nostr_public_key: NostrPubkey, pub escrow_coordinator_ecash_public_key: Option, pub escrow_start_timestamp: Option, - pub mode: TradeMode, } impl EscrowClientMetadata { @@ -19,32 +24,34 @@ impl EscrowClientMetadata { escrow_coordinator_nostr_public_key: cli_input.coordinator_nostr_pubkey, escrow_coordinator_ecash_public_key: None, escrow_start_timestamp: None, - mode: cli_input.mode, }) } } pub struct EscrowClient { - pub nostr_instance: ClientNostrInstance, // can either be a Nostr Client or Nostr note signer (without networking) - pub ecash_wallet: ClientEcashWallet, - pub escrow_metadata: EscrowClientMetadata, // data relevant for the application but not for the outcome of the trade contract - pub escrow_contract: TradeContract, + nostr_instance: ClientNostrInstance, // can either be a Nostr Client or Nostr note signer (without networking) + ecash_wallet: ClientEcashWallet, + escrow_metadata: EscrowClientMetadata, // data relevant for the application but not for the outcome of the trade contract + escrow_contract: TradeContract, + trade_mode: TradeMode, } // todo: model EscrowClient as an state machine (stm). This will improve testability too. impl EscrowClient { // creates the inital state: the coordinator data isn't present. pub fn new( - escrow_contract: TradeContract, - escrow_metadata: EscrowClientMetadata, nostr_instance: ClientNostrInstance, ecash_wallet: ClientEcashWallet, + escrow_metadata: EscrowClientMetadata, + escrow_contract: TradeContract, + trade_mode: TradeMode, ) -> Self { Self { - escrow_contract, - escrow_metadata, nostr_instance, ecash_wallet, + escrow_metadata, + escrow_contract, + trade_mode, } } @@ -75,7 +82,7 @@ impl EscrowClient { /// /// After this the state is token sent or received. pub async fn exchange_trade_token(&self) -> std::result::Result<(), anyhow::Error> { - match self.escrow_metadata.mode { + match self.trade_mode { TradeMode::Buyer => { // todo: store the sent token in this instance self.send_trade_token().await?; diff --git a/client/src/main.rs b/client/src/main.rs index 19d59bb..752a8f5 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -9,7 +9,7 @@ use anyhow::anyhow; use cashu_escrow_common as common; use cdk::nuts::PublicKey as EcashPubkey; use cli::trade_contract::FromClientCliInput; -use cli::{ClientCliInput, TradeMode}; +use cli::ClientCliInput; use common::{cli::get_user_input, nostr::NostrClient, TradeContract}; use dotenv::dotenv; use ecash::ClientEcashWallet; @@ -38,10 +38,11 @@ async fn main() -> anyhow::Result<()> { let escrow_metadata = EscrowClientMetadata::from_client_cli_input(&cli_input)?; let nostr_instance = ClientNostrInstance::from_client_cli_input(&cli_input).await?; let mut escrow_client = EscrowClient::new( - escrow_contract, - escrow_metadata, nostr_instance, escrow_wallet, + escrow_metadata, + escrow_contract, + cli_input.mode, ); escrow_client.register_trade().await?; From 3ffe54f3560b9175f8ba48aa605ca2e1aa8e2349 Mon Sep 17 00:00:00 2001 From: rodant Date: Fri, 9 Aug 2024 19:06:11 +0200 Subject: [PATCH 17/43] Split escrow metadata in the contract and escrow registration parts. --- client/src/cli/trade_contract.rs | 1 + client/src/ecash/mod.rs | 20 ++++++-------- client/src/escrow_client/mod.rs | 46 +++++++++++++++++--------------- client/src/main.rs | 2 -- client/src/nostr/mod.rs | 8 ++---- common/src/lib.rs | 2 ++ 6 files changed, 38 insertions(+), 41 deletions(-) diff --git a/client/src/cli/trade_contract.rs b/client/src/cli/trade_contract.rs index 6e2b4d9..cf32371 100644 --- a/client/src/cli/trade_contract.rs +++ b/client/src/cli/trade_contract.rs @@ -38,6 +38,7 @@ impl FromClientCliInput for TradeContract { trade_amount_sat: 5000, npub_seller, npub_buyer, + npub_coordinator: cli_input.coordinator_nostr_pubkey, time_limit: 3 * 24 * 60 * 60, seller_ecash_public_key: ecash_pubkey_seller, buyer_ecash_public_key: ecash_pubkey_buyer, diff --git a/client/src/ecash/mod.rs b/client/src/ecash/mod.rs index cb5092f..e6b1f60 100644 --- a/client/src/ecash/mod.rs +++ b/client/src/ecash/mod.rs @@ -38,16 +38,12 @@ impl ClientEcashWallet { fn assemble_escrow_conditions( &self, contract: &TradeContract, - escrow_metadata: &EscrowClientMetadata, + escrow_registration: &EscrowRegistration, ) -> anyhow::Result { let seller_pubkey = EscrowPubkey::from_str(&contract.seller_ecash_public_key)?; let buyer_pubkey = EscrowPubkey::from_str(&contract.buyer_ecash_public_key)?; - let escrow_pubkey = escrow_metadata - .escrow_coordinator_ecash_public_key - .ok_or(anyhow!("Escrow coordinator ecash public key not set"))?; - let start_timestamp = escrow_metadata - .escrow_start_timestamp - .ok_or(anyhow!("Escrow start timestamp not set"))?; + let coordinator_escrow_pubkey = escrow_registration.coordinator_escrow_pubkey; + let start_timestamp = escrow_registration.escrow_start_time; let locktime = start_timestamp.as_u64() + contract.time_limit; @@ -55,7 +51,7 @@ impl ClientEcashWallet { seller_pubkey, Some(Conditions::new( Some(locktime), - Some(vec![buyer_pubkey, escrow_pubkey]), + Some(vec![buyer_pubkey, coordinator_escrow_pubkey]), Some(vec![buyer_pubkey]), Some(2), Some(SigFlag::SigAll), @@ -67,9 +63,9 @@ impl ClientEcashWallet { pub async fn create_escrow_token( &self, contract: &TradeContract, - escrow_metadata: &EscrowClientMetadata, + escrow_registration: &EscrowRegistration, ) -> anyhow::Result { - let spending_conditions = self.assemble_escrow_conditions(contract, escrow_metadata)?; + let spending_conditions = self.assemble_escrow_conditions(contract, escrow_registration)?; let token = self .wallet .send( @@ -86,9 +82,9 @@ impl ClientEcashWallet { &self, escrow_token: &str, contract: &TradeContract, - escrow_metadata: &EscrowClientMetadata, + escrow_registration: &EscrowRegistration, ) -> anyhow::Result { - let spending_conditions = self.assemble_escrow_conditions(contract, escrow_metadata)?; + let spending_conditions = self.assemble_escrow_conditions(contract, escrow_registration)?; let token = Token::from_str(escrow_token)?; self.wallet.verify_token_p2pk(&token, spending_conditions)?; Ok(token) diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index 30947ad..b1ed005 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -12,26 +12,24 @@ pub enum TradeMode { Seller, } -pub struct EscrowClientMetadata { - pub escrow_coordinator_nostr_public_key: NostrPubkey, - pub escrow_coordinator_ecash_public_key: Option, - pub escrow_start_timestamp: Option, +pub struct EscrowRegistration { + pub coordinator_escrow_pubkey: EcashPubkey, + pub escrow_start_time: Timestamp, } -impl EscrowClientMetadata { - pub fn from_client_cli_input(cli_input: &ClientCliInput) -> anyhow::Result { - Ok(Self { - escrow_coordinator_nostr_public_key: cli_input.coordinator_nostr_pubkey, - escrow_coordinator_ecash_public_key: None, - escrow_start_timestamp: None, - }) +impl EscrowRegistration { + pub fn new(coordinator_escrow_pubkey: EcashPubkey, escrow_start_time: Timestamp) -> Self { + Self { + coordinator_escrow_pubkey, + escrow_start_time, + } } } pub struct EscrowClient { nostr_instance: ClientNostrInstance, // can either be a Nostr Client or Nostr note signer (without networking) ecash_wallet: ClientEcashWallet, - escrow_metadata: EscrowClientMetadata, // data relevant for the application but not for the outcome of the trade contract + escrow_registration: Option, escrow_contract: TradeContract, trade_mode: TradeMode, } @@ -42,14 +40,13 @@ impl EscrowClient { pub fn new( nostr_instance: ClientNostrInstance, ecash_wallet: ClientEcashWallet, - escrow_metadata: EscrowClientMetadata, escrow_contract: TradeContract, trade_mode: TradeMode, ) -> Self { Self { nostr_instance, ecash_wallet, - escrow_metadata, + escrow_registration: None, escrow_contract, trade_mode, } @@ -61,7 +58,7 @@ impl EscrowClient { /// /// After this state the trade contract is effectfull as well, possible coordinator fees must be payed. pub async fn register_trade(&mut self) -> anyhow::Result<()> { - let coordinator_pk = &self.escrow_metadata.escrow_coordinator_nostr_public_key; + let coordinator_pk = &self.escrow_contract.npub_coordinator; // submits the trade contract to the coordinator to initiate the escrow service self.nostr_instance @@ -73,8 +70,9 @@ impl EscrowClient { .get_escrow_coordinator_pk(coordinator_pk) .await?; - self.escrow_metadata.escrow_coordinator_ecash_public_key = Some(escrow_coordinator_pk_ts.0); - self.escrow_metadata.escrow_start_timestamp = Some(escrow_coordinator_pk_ts.1); + let escrow_registration = + EscrowRegistration::new(escrow_coordinator_pk_ts.0, escrow_coordinator_pk_ts.1); + self.escrow_registration = Some(escrow_registration); Ok(()) } @@ -101,11 +99,14 @@ impl EscrowClient { /// Returns the sent trade token by this [`EscrowClient`]. async fn send_trade_token(&self) -> anyhow::Result { let escrow_contract = &self.escrow_contract; - let client_metadata = &self.escrow_metadata; + let escrow_registration = self + .escrow_registration + .as_ref() + .ok_or(anyhow!("Escrow registration not set, wrong state"))?; let escrow_token = self .ecash_wallet - .create_escrow_token(escrow_contract, client_metadata) + .create_escrow_token(escrow_contract, escrow_registration) .await?; debug!("Sending token to the seller: {}", escrow_token.as_str()); @@ -122,13 +123,16 @@ impl EscrowClient { /// Returns the received trade token by this [`EscrowClient`]. async fn receive_and_validate_trade_token(&self) -> anyhow::Result { let escrow_contract = &self.escrow_contract; - let client_metadata = &self.escrow_metadata; + let client_registration = self + .escrow_registration + .as_ref() + .ok_or(anyhow!("Escrow registration not set, wrong state"))?; let wallet = &self.ecash_wallet; let escrow_token = self .nostr_instance // todo: split method in receive and validate steps, single responsability principle. - .await_and_validate_escrow_token(wallet, escrow_contract, client_metadata) + .await_and_validate_escrow_token(wallet, escrow_contract, client_registration) .await?; Ok(escrow_token) diff --git a/client/src/main.rs b/client/src/main.rs index 752a8f5..3c6ff5a 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -35,12 +35,10 @@ async fn main() -> anyhow::Result<()> { let cli_input = ClientCliInput::parse().await?; let escrow_contract = TradeContract::from_client_cli_input(&cli_input, escrow_wallet.trade_pubkey.clone())?; - let escrow_metadata = EscrowClientMetadata::from_client_cli_input(&cli_input)?; let nostr_instance = ClientNostrInstance::from_client_cli_input(&cli_input).await?; let mut escrow_client = EscrowClient::new( nostr_instance, escrow_wallet, - escrow_metadata, escrow_contract, cli_input.mode, ); diff --git a/client/src/nostr/mod.rs b/client/src/nostr/mod.rs index f03881b..c52bc50 100644 --- a/client/src/nostr/mod.rs +++ b/client/src/nostr/mod.rs @@ -92,15 +92,11 @@ impl ClientNostrInstance { &self, wallet: &ClientEcashWallet, contract: &TradeContract, - metadata: &EscrowClientMetadata, + metadata: &EscrowRegistration, ) -> anyhow::Result { let filter_note = Filter::new() .kind(Kind::EncryptedDirectMessage) - .since( - metadata - .escrow_start_timestamp - .expect("Escrow timestamp not set"), - ) + .since(metadata.escrow_start_time) .author(nostr_sdk::PublicKey::from_bech32(&contract.npub_buyer)?); let subscription_id = self diff --git a/common/src/lib.rs b/common/src/lib.rs index f694060..f35a39b 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,3 +1,4 @@ +use nostr_sdk::PublicKey; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone)] @@ -6,6 +7,7 @@ pub struct TradeContract { pub trade_amount_sat: u64, pub npub_seller: String, pub npub_buyer: String, + pub npub_coordinator: PublicKey, pub time_limit: u64, pub seller_ecash_public_key: String, pub buyer_ecash_public_key: String, From 7e1b21a7bc05e3e50b28c169ac249f129d845427 Mon Sep 17 00:00:00 2001 From: rodant Date: Sat, 10 Aug 2024 17:49:12 +0200 Subject: [PATCH 18/43] Use PublicKey type in trade contract. --- client/src/cli/trade_contract.rs | 18 +++++++++--------- client/src/escrow_client/mod.rs | 4 ++-- client/src/nostr/mod.rs | 6 +++--- common/src/lib.rs | 6 +++--- common/src/nostr/mod.rs | 12 ++++++------ coordinator/src/escrow_coordinator/mod.rs | 2 +- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/client/src/cli/trade_contract.rs b/client/src/cli/trade_contract.rs index cf32371..e22e601 100644 --- a/client/src/cli/trade_contract.rs +++ b/client/src/cli/trade_contract.rs @@ -13,17 +13,17 @@ impl FromClientCliInput for TradeContract { trade_pubkey: String, ) -> anyhow::Result { debug!("Constructing hard coded client trade contract..."); - let npub_seller: String; - let npub_buyer: String; + let npubkey_seller: PublicKey; + let npubkey_buyer: PublicKey; match cli_input.mode { TradeMode::Buyer => { - npub_seller = cli_input.trade_partner_nostr_pubkey.to_bech32()?; - npub_buyer = cli_input.trader_nostr_keys.public_key().to_bech32()?; + npubkey_seller = cli_input.trade_partner_nostr_pubkey; + npubkey_buyer = cli_input.trader_nostr_keys.public_key(); } TradeMode::Seller => { - npub_buyer = cli_input.trade_partner_nostr_pubkey.to_bech32()?; - npub_seller = cli_input.trader_nostr_keys.public_key().to_bech32()?; + npubkey_buyer = cli_input.trade_partner_nostr_pubkey; + npubkey_seller = cli_input.trader_nostr_keys.public_key(); } } @@ -36,9 +36,9 @@ impl FromClientCliInput for TradeContract { trade_description: "Purchase of one Watermelon for 5000 satoshi. 3 days delivery to ...".to_string(), trade_amount_sat: 5000, - npub_seller, - npub_buyer, - npub_coordinator: cli_input.coordinator_nostr_pubkey, + npubkey_seller, + npubkey_buyer, + npubkey_coordinator: cli_input.coordinator_nostr_pubkey, time_limit: 3 * 24 * 60 * 60, seller_ecash_public_key: ecash_pubkey_seller, buyer_ecash_public_key: ecash_pubkey_buyer, diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index b1ed005..63f6cad 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -58,7 +58,7 @@ impl EscrowClient { /// /// After this state the trade contract is effectfull as well, possible coordinator fees must be payed. pub async fn register_trade(&mut self) -> anyhow::Result<()> { - let coordinator_pk = &self.escrow_contract.npub_coordinator; + let coordinator_pk = &self.escrow_contract.npubkey_coordinator; // submits the trade contract to the coordinator to initiate the escrow service self.nostr_instance @@ -112,7 +112,7 @@ impl EscrowClient { debug!("Sending token to the seller: {}", escrow_token.as_str()); self.nostr_instance - .submit_trade_token_to_seller(&escrow_contract.npub_seller, &escrow_token) + .submit_trade_token_to_seller(escrow_contract.npubkey_seller, &escrow_token) .await?; Ok(escrow_token) diff --git a/client/src/nostr/mod.rs b/client/src/nostr/mod.rs index c52bc50..a5bb3fb 100644 --- a/client/src/nostr/mod.rs +++ b/client/src/nostr/mod.rs @@ -79,11 +79,11 @@ impl ClientNostrInstance { pub async fn submit_trade_token_to_seller( &self, - seller_npub: &str, + seller_npubkey: PublicKey, token: &str, ) -> anyhow::Result<()> { self.nostr_client - .send_trade_token_to_seller(seller_npub, token) + .send_trade_token_to_seller(seller_npubkey, token) .await?; Ok(()) } @@ -97,7 +97,7 @@ impl ClientNostrInstance { let filter_note = Filter::new() .kind(Kind::EncryptedDirectMessage) .since(metadata.escrow_start_time) - .author(nostr_sdk::PublicKey::from_bech32(&contract.npub_buyer)?); + .author(contract.npubkey_buyer); let subscription_id = self .nostr_client diff --git a/common/src/lib.rs b/common/src/lib.rs index f35a39b..847840a 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -5,9 +5,9 @@ use serde::{Deserialize, Serialize}; pub struct TradeContract { pub trade_description: String, pub trade_amount_sat: u64, - pub npub_seller: String, - pub npub_buyer: String, - pub npub_coordinator: PublicKey, + pub npubkey_seller: PublicKey, + pub npubkey_buyer: PublicKey, + pub npubkey_coordinator: PublicKey, pub time_limit: u64, pub seller_ecash_public_key: String, pub buyer_ecash_public_key: String, diff --git a/common/src/nostr/mod.rs b/common/src/nostr/mod.rs index 1b73ef3..ae81fd9 100644 --- a/common/src/nostr/mod.rs +++ b/common/src/nostr/mod.rs @@ -28,7 +28,7 @@ impl NostrClient { .add_relay("wss://ftp.halifax.rwth-aachen.de/nostr") .await?; client.add_relay("wss://nostr.mom").await?; - client.add_relay("wss://relay.nostrplebs.com").await?; + //client.add_relay("wss://relay.nostrplebs.com").await?; (having errors) // Connect to relays client.connect().await; @@ -50,7 +50,7 @@ impl NostrClient { // coordinator specific function? pub async fn send_escrow_pubkeys( &self, - receivers: (&str, &str), + receivers: (PublicKey, PublicKey), id: &[u8; 32], trade_pk: &str, ) -> anyhow::Result<()> { @@ -60,10 +60,10 @@ impl NostrClient { escrow_start_ts: Timestamp::now(), })?; self.client - .send_direct_msg(PublicKey::from_bech32(receivers.0)?, &message, None) + .send_direct_msg(receivers.0, &message, None) .await?; self.client - .send_direct_msg(PublicKey::from_bech32(receivers.1)?, &message, None) + .send_direct_msg(receivers.1, &message, None) .await?; Ok(()) } @@ -89,11 +89,11 @@ impl NostrClient { // client specific function? pub async fn send_trade_token_to_seller( &self, - seller_npub: &str, + seller_npubkey: PublicKey, token: &str, ) -> anyhow::Result<()> { self.client - .send_direct_msg(PublicKey::from_bech32(seller_npub)?, token, None) + .send_direct_msg(seller_npubkey, token, None) .await?; Ok(()) } diff --git a/coordinator/src/escrow_coordinator/mod.rs b/coordinator/src/escrow_coordinator/mod.rs index 53cabaa..0dffe50 100644 --- a/coordinator/src/escrow_coordinator/mod.rs +++ b/coordinator/src/escrow_coordinator/mod.rs @@ -95,7 +95,7 @@ impl EscrowCoordinator { ); self.nostr_client .send_escrow_pubkeys( - (&trade.npub_buyer, &trade.npub_seller), + (trade.npubkey_buyer, trade.npubkey_seller), contract_hash, &contract_secret.public_key().to_hex(), ) From 71af4c2ed913af5c7376c2ee4245ed3be07ccfc1 Mon Sep 17 00:00:00 2001 From: rodant Date: Tue, 13 Aug 2024 11:57:19 +0200 Subject: [PATCH 19/43] Replace tuple though the registration message struct. --- client/src/escrow_client/mod.rs | 10 ++++++---- client/src/nostr/client_nostr_utils.rs | 11 +++++------ client/src/nostr/mod.rs | 10 ++++++---- common/src/nostr/mod.rs | 8 ++++---- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index 63f6cad..470b2c0 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -65,13 +65,15 @@ impl EscrowClient { .submit_escrow_contract(&self.escrow_contract, coordinator_pk) .await?; - let escrow_coordinator_pk_ts: (EcashPubkey, Timestamp) = self + let registration_message = self .nostr_instance - .get_escrow_coordinator_pk(coordinator_pk) + .receive_registration_message(coordinator_pk) .await?; - let escrow_registration = - EscrowRegistration::new(escrow_coordinator_pk_ts.0, escrow_coordinator_pk_ts.1); + let escrow_registration = EscrowRegistration::new( + EcashPubkey::from_hex(registration_message.coordinator_escrow_pubkey)?, + registration_message.escrow_start_ts, + ); self.escrow_registration = Some(escrow_registration); Ok(()) } diff --git a/client/src/nostr/client_nostr_utils.rs b/client/src/nostr/client_nostr_utils.rs index 654a50f..b674552 100644 --- a/client/src/nostr/client_nostr_utils.rs +++ b/client/src/nostr/client_nostr_utils.rs @@ -1,9 +1,8 @@ use super::*; -use cashu_escrow_common::nostr::PubkeyMessage; +use cashu_escrow_common::nostr::RegistrationMessage; -pub async fn parse_escrow_pk(pk_message_json: &str) -> anyhow::Result<(EcashPubkey, Timestamp)> { - let pkm: PubkeyMessage = serde_json::from_str(pk_message_json)?; - let coordinator_ecash_pk = EcashPubkey::from_hex(pkm.escrow_coordinator_pubkey)?; - - Ok((coordinator_ecash_pk, pkm.escrow_start_ts)) +pub async fn parse_registration_message( + pk_message_json: &str, +) -> anyhow::Result { + Ok(serde_json::from_str(pk_message_json)?) } diff --git a/client/src/nostr/mod.rs b/client/src/nostr/mod.rs index a5bb3fb..6b3e2ad 100644 --- a/client/src/nostr/mod.rs +++ b/client/src/nostr/mod.rs @@ -1,5 +1,7 @@ mod client_nostr_utils; +use cashu_escrow_common::nostr::RegistrationMessage; + use self::client_nostr_utils::*; use super::*; @@ -43,10 +45,10 @@ impl ClientNostrInstance { // await the answer to the submitted contract, once the coordinator returns the ecash public key // the escrow service is confirmed by the coordinator - pub async fn get_escrow_coordinator_pk( + pub async fn receive_registration_message( &self, coordinator_pk: &NostrPubkey, - ) -> anyhow::Result<(EcashPubkey, Timestamp)> { + ) -> anyhow::Result { let filter_note = Filter::new() .kind(Kind::EncryptedDirectMessage) .since(Timestamp::now()) @@ -67,9 +69,9 @@ impl ClientNostrInstance { .decrypt_msg(&event.content, &event.author()) { debug!("Received event: {:?}", &decrypted); - if let Ok(ecash_pubkey_and_timestamp) = parse_escrow_pk(&decrypted).await { + if let Ok(registration_message) = parse_registration_message(&decrypted).await { self.nostr_client.client.unsubscribe(subscription_id).await; - return Ok(ecash_pubkey_and_timestamp); + return Ok(registration_message); } } } diff --git a/common/src/nostr/mod.rs b/common/src/nostr/mod.rs index ae81fd9..b8be800 100644 --- a/common/src/nostr/mod.rs +++ b/common/src/nostr/mod.rs @@ -9,8 +9,8 @@ pub struct NostrClient { } #[derive(Serialize, Deserialize)] -pub struct PubkeyMessage { - pub escrow_coordinator_pubkey: String, +pub struct RegistrationMessage { + pub coordinator_escrow_pubkey: String, pub trade_id_hex: String, pub escrow_start_ts: Timestamp, } @@ -54,8 +54,8 @@ impl NostrClient { id: &[u8; 32], trade_pk: &str, ) -> anyhow::Result<()> { - let message = serde_json::to_string(&PubkeyMessage { - escrow_coordinator_pubkey: trade_pk.to_string(), + let message = serde_json::to_string(&RegistrationMessage { + coordinator_escrow_pubkey: trade_pk.to_string(), trade_id_hex: hex::encode(id), escrow_start_ts: Timestamp::now(), })?; From af5121cfb0fed57a4c7e76685162a186a076ba1f Mon Sep 17 00:00:00 2001 From: rodant Date: Tue, 13 Aug 2024 16:29:00 +0200 Subject: [PATCH 20/43] Remove utils modules. --- client/src/escrow_client/buyer_utils.rs | 3 --- client/src/escrow_client/general_utils.rs | 3 --- client/src/escrow_client/mod.rs | 4 ---- client/src/escrow_client/seller_utils.rs | 3 --- client/src/nostr/client_nostr_utils.rs | 8 -------- client/src/nostr/mod.rs | 5 +---- 6 files changed, 1 insertion(+), 25 deletions(-) delete mode 100644 client/src/escrow_client/buyer_utils.rs delete mode 100644 client/src/escrow_client/general_utils.rs delete mode 100644 client/src/escrow_client/seller_utils.rs delete mode 100644 client/src/nostr/client_nostr_utils.rs diff --git a/client/src/escrow_client/buyer_utils.rs b/client/src/escrow_client/buyer_utils.rs deleted file mode 100644 index a410b6f..0000000 --- a/client/src/escrow_client/buyer_utils.rs +++ /dev/null @@ -1,3 +0,0 @@ -use super::*; - -// here we can place buyer specific functions diff --git a/client/src/escrow_client/general_utils.rs b/client/src/escrow_client/general_utils.rs deleted file mode 100644 index b075c22..0000000 --- a/client/src/escrow_client/general_utils.rs +++ /dev/null @@ -1,3 +0,0 @@ -use super::*; - -// here we can place general coordinator functions diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index 470b2c0..10ff1a4 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -1,7 +1,3 @@ -mod buyer_utils; -pub mod general_utils; -mod seller_utils; - use cdk::nuts::Token; use super::*; diff --git a/client/src/escrow_client/seller_utils.rs b/client/src/escrow_client/seller_utils.rs deleted file mode 100644 index 0e3a5bd..0000000 --- a/client/src/escrow_client/seller_utils.rs +++ /dev/null @@ -1,3 +0,0 @@ -use super::*; - -// here we can place seller specific functions diff --git a/client/src/nostr/client_nostr_utils.rs b/client/src/nostr/client_nostr_utils.rs deleted file mode 100644 index b674552..0000000 --- a/client/src/nostr/client_nostr_utils.rs +++ /dev/null @@ -1,8 +0,0 @@ -use super::*; -use cashu_escrow_common::nostr::RegistrationMessage; - -pub async fn parse_registration_message( - pk_message_json: &str, -) -> anyhow::Result { - Ok(serde_json::from_str(pk_message_json)?) -} diff --git a/client/src/nostr/mod.rs b/client/src/nostr/mod.rs index 6b3e2ad..5467054 100644 --- a/client/src/nostr/mod.rs +++ b/client/src/nostr/mod.rs @@ -1,8 +1,5 @@ -mod client_nostr_utils; - use cashu_escrow_common::nostr::RegistrationMessage; -use self::client_nostr_utils::*; use super::*; // here we can somehow make NostrInstance generic to be either a full Nostr Client or only a Nostr Signer depending on @@ -69,7 +66,7 @@ impl ClientNostrInstance { .decrypt_msg(&event.content, &event.author()) { debug!("Received event: {:?}", &decrypted); - if let Ok(registration_message) = parse_registration_message(&decrypted).await { + if let Ok(registration_message) = serde_json::from_str(&decrypted) { self.nostr_client.client.unsubscribe(subscription_id).await; return Ok(registration_message); } From 2a88dbca51ff04cf5fd044253103f762af8d2e16 Mon Sep 17 00:00:00 2001 From: rodant Date: Wed, 14 Aug 2024 13:11:02 +0200 Subject: [PATCH 21/43] Remove PubkeyMessage and rearrange models. --- client/src/ecash/mod.rs | 15 ++++----- client/src/escrow_client/mod.rs | 21 ++----------- client/src/main.rs | 5 ++- client/src/nostr/mod.rs | 13 ++++---- common/src/lib.rs | 36 +++++++++++++--------- common/src/model.rs | 37 +++++++++++++++++++++++ common/src/nostr/mod.rs | 26 ++++++---------- coordinator/src/escrow_coordinator/mod.rs | 27 +++++++++-------- 8 files changed, 101 insertions(+), 79 deletions(-) create mode 100644 common/src/model.rs diff --git a/client/src/ecash/mod.rs b/client/src/ecash/mod.rs index e6b1f60..6f40f35 100644 --- a/client/src/ecash/mod.rs +++ b/client/src/ecash/mod.rs @@ -1,6 +1,7 @@ use super::*; -use cdk::nuts::PublicKey as EscrowPubkey; +use crate::common::model::EscrowRegistration; +use cdk::nuts::PublicKey; use cdk::{ amount::SplitTarget, cdk_database::WalletMemoryDatabase, @@ -13,7 +14,7 @@ use std::sync::Arc; #[derive(Debug)] pub struct ClientEcashWallet { - secret: SecretKey, + _secret: SecretKey, pub wallet: Wallet, pub trade_pubkey: String, } @@ -21,15 +22,15 @@ pub struct ClientEcashWallet { impl ClientEcashWallet { pub async fn new(mint_url: &str) -> anyhow::Result { let localstore = WalletMemoryDatabase::default(); - let secret = SecretKey::generate(); - let trade_pubkey: String = secret.public_key().to_string(); + let _secret = SecretKey::generate(); + let trade_pubkey: String = _secret.public_key().to_string(); let seed = rand::thread_rng().gen::<[u8; 32]>(); info!("Trade ecash pubkey: {}", trade_pubkey); let wallet = Wallet::new(mint_url, CurrencyUnit::Sat, Arc::new(localstore), &seed); Ok(Self { - secret, + _secret, wallet, trade_pubkey, }) @@ -40,8 +41,8 @@ impl ClientEcashWallet { contract: &TradeContract, escrow_registration: &EscrowRegistration, ) -> anyhow::Result { - let seller_pubkey = EscrowPubkey::from_str(&contract.seller_ecash_public_key)?; - let buyer_pubkey = EscrowPubkey::from_str(&contract.buyer_ecash_public_key)?; + let seller_pubkey = PublicKey::from_str(&contract.seller_ecash_public_key)?; + let buyer_pubkey = PublicKey::from_str(&contract.buyer_ecash_public_key)?; let coordinator_escrow_pubkey = escrow_registration.coordinator_escrow_pubkey; let start_timestamp = escrow_registration.escrow_start_time; diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index 10ff1a4..467909e 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -1,3 +1,4 @@ +use crate::common::model::EscrowRegistration; use cdk::nuts::Token; use super::*; @@ -8,20 +9,6 @@ pub enum TradeMode { Seller, } -pub struct EscrowRegistration { - pub coordinator_escrow_pubkey: EcashPubkey, - pub escrow_start_time: Timestamp, -} - -impl EscrowRegistration { - pub fn new(coordinator_escrow_pubkey: EcashPubkey, escrow_start_time: Timestamp) -> Self { - Self { - coordinator_escrow_pubkey, - escrow_start_time, - } - } -} - pub struct EscrowClient { nostr_instance: ClientNostrInstance, // can either be a Nostr Client or Nostr note signer (without networking) ecash_wallet: ClientEcashWallet, @@ -61,15 +48,11 @@ impl EscrowClient { .submit_escrow_contract(&self.escrow_contract, coordinator_pk) .await?; - let registration_message = self + let escrow_registration = self .nostr_instance .receive_registration_message(coordinator_pk) .await?; - let escrow_registration = EscrowRegistration::new( - EcashPubkey::from_hex(registration_message.coordinator_escrow_pubkey)?, - registration_message.escrow_start_ts, - ); self.escrow_registration = Some(escrow_registration); Ok(()) } diff --git a/client/src/main.rs b/client/src/main.rs index 3c6ff5a..30cd173 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -7,17 +7,16 @@ use std::env; use anyhow::anyhow; use cashu_escrow_common as common; -use cdk::nuts::PublicKey as EcashPubkey; use cli::trade_contract::FromClientCliInput; use cli::ClientCliInput; -use common::{cli::get_user_input, nostr::NostrClient, TradeContract}; +use common::model::TradeContract; +use common::{cli::get_user_input, nostr::NostrClient}; use dotenv::dotenv; use ecash::ClientEcashWallet; use escrow_client::*; use log::{debug, info}; use nostr::ClientNostrInstance; use nostr_sdk::prelude::*; -use nostr_sdk::PublicKey as NostrPubkey; #[tokio::main] async fn main() -> anyhow::Result<()> { diff --git a/client/src/nostr/mod.rs b/client/src/nostr/mod.rs index 5467054..83030f9 100644 --- a/client/src/nostr/mod.rs +++ b/client/src/nostr/mod.rs @@ -1,4 +1,5 @@ -use cashu_escrow_common::nostr::RegistrationMessage; +use cashu_escrow_common::model::EscrowRegistration; +use nostr_sdk::PublicKey; use super::*; @@ -31,7 +32,7 @@ impl ClientNostrInstance { pub async fn submit_escrow_contract( &self, contract: &TradeContract, - coordinator_pk: &NostrPubkey, + coordinator_pk: &PublicKey, ) -> anyhow::Result<()> { let coordinator_pk_bech32 = coordinator_pk.to_bech32()?; self.nostr_client @@ -44,8 +45,8 @@ impl ClientNostrInstance { // the escrow service is confirmed by the coordinator pub async fn receive_registration_message( &self, - coordinator_pk: &NostrPubkey, - ) -> anyhow::Result { + coordinator_pk: &PublicKey, + ) -> anyhow::Result { let filter_note = Filter::new() .kind(Kind::EncryptedDirectMessage) .since(Timestamp::now()) @@ -66,9 +67,9 @@ impl ClientNostrInstance { .decrypt_msg(&event.content, &event.author()) { debug!("Received event: {:?}", &decrypted); - if let Ok(registration_message) = serde_json::from_str(&decrypted) { + if let Ok(escrow_registration) = serde_json::from_str(&decrypted) { self.nostr_client.client.unsubscribe(subscription_id).await; - return Ok(registration_message); + return Ok(escrow_registration); } } } diff --git a/common/src/lib.rs b/common/src/lib.rs index 847840a..fcf55c1 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,17 +1,23 @@ -use nostr_sdk::PublicKey; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct TradeContract { - pub trade_description: String, - pub trade_amount_sat: u64, - pub npubkey_seller: PublicKey, - pub npubkey_buyer: PublicKey, - pub npubkey_coordinator: PublicKey, - pub time_limit: u64, - pub seller_ecash_public_key: String, - pub buyer_ecash_public_key: String, -} - pub mod cli; +pub mod model; pub mod nostr; + +mod cdk_pubkey_serde { + use cdk::nuts::PublicKey; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(pk: &PublicKey, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&pk.to_hex()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let pubkey_hex = String::deserialize(deserializer)?; + PublicKey::from_hex(pubkey_hex).map_err(serde::de::Error::custom) + } +} diff --git a/common/src/model.rs b/common/src/model.rs new file mode 100644 index 0000000..21795e1 --- /dev/null +++ b/common/src/model.rs @@ -0,0 +1,37 @@ +use cdk::nuts::PublicKey as CDKPubkey; +use nostr_sdk::{PublicKey as NostrPubkey, Timestamp}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct TradeContract { + pub trade_description: String, + pub trade_amount_sat: u64, + pub npubkey_seller: NostrPubkey, + pub npubkey_buyer: NostrPubkey, + pub npubkey_coordinator: NostrPubkey, + pub time_limit: u64, + pub seller_ecash_public_key: String, + pub buyer_ecash_public_key: String, +} + +#[derive(Serialize, Deserialize)] +pub struct EscrowRegistration { + pub escrow_id_hex: String, + #[serde(with = "crate::cdk_pubkey_serde")] + pub coordinator_escrow_pubkey: CDKPubkey, + pub escrow_start_time: Timestamp, +} + +impl EscrowRegistration { + pub fn new( + trade_id_hex: String, + coordinator_escrow_pubkey: CDKPubkey, + escrow_start_time: Timestamp, + ) -> Self { + Self { + escrow_id_hex: trade_id_hex, + coordinator_escrow_pubkey, + escrow_start_time, + } + } +} diff --git a/common/src/nostr/mod.rs b/common/src/nostr/mod.rs index b8be800..1124f2f 100644 --- a/common/src/nostr/mod.rs +++ b/common/src/nostr/mod.rs @@ -1,20 +1,11 @@ -use crate::TradeContract; -use ndk::{nostr::nips::nip04, prelude::*}; -use nostr_sdk as ndk; -use serde::{Deserialize, Serialize}; +use crate::model::{EscrowRegistration, TradeContract}; +use nostr_sdk::{nostr::nips::nip04, prelude::*}; pub struct NostrClient { keypair: Keys, pub client: Client, } -#[derive(Serialize, Deserialize)] -pub struct RegistrationMessage { - pub coordinator_escrow_pubkey: String, - pub trade_id_hex: String, - pub escrow_start_ts: Timestamp, -} - impl NostrClient { pub async fn new(nsec: &String) -> anyhow::Result { let keypair = Keys::parse(nsec)?; @@ -54,16 +45,17 @@ impl NostrClient { id: &[u8; 32], trade_pk: &str, ) -> anyhow::Result<()> { - let message = serde_json::to_string(&RegistrationMessage { - coordinator_escrow_pubkey: trade_pk.to_string(), - trade_id_hex: hex::encode(id), - escrow_start_ts: Timestamp::now(), + let registration_json = serde_json::to_string(&EscrowRegistration { + escrow_id_hex: hex::encode(id), + coordinator_escrow_pubkey: cdk::nuts::PublicKey::from_hex(trade_pk)?, + escrow_start_time: Timestamp::now(), })?; + // todo: replace deprecated method self.client - .send_direct_msg(receivers.0, &message, None) + .send_direct_msg(receivers.0, ®istration_json, None) .await?; self.client - .send_direct_msg(receivers.1, &message, None) + .send_direct_msg(receivers.1, ®istration_json, None) .await?; Ok(()) } diff --git a/coordinator/src/escrow_coordinator/mod.rs b/coordinator/src/escrow_coordinator/mod.rs index 0dffe50..ae06632 100644 --- a/coordinator/src/escrow_coordinator/mod.rs +++ b/coordinator/src/escrow_coordinator/mod.rs @@ -1,10 +1,11 @@ use super::*; -use cashu_escrow_common::TradeContract; -use cdk::nuts::SecretKey; +use cashu_escrow_common::model::TradeContract; +use cdk::nuts::SecretKey as CDKSecretKey; use hashes::hex::DisplayHex; +use ndk::prelude::PublicKey as NostrPubkey; use ndk::prelude::*; +use ndk::{Filter, Kind, RelayPoolNotification}; use nostr_sdk as ndk; -use nostr_sdk::{Filter, Kind, RelayPoolNotification}; use sha2::{Digest, Sha256}; use std::collections::HashMap; @@ -14,9 +15,9 @@ pub struct EscrowCoordinator { active_contracts: HashMap<[u8; 32], ActiveTade>, } -pub struct ActiveTade { - pub trade_contract: TradeContract, - pub coordinator_secret: SecretKey, +struct ActiveTade { + _trade_contract: TradeContract, + _coordinator_secret: CDKSecretKey, } impl EscrowCoordinator { @@ -33,7 +34,7 @@ impl EscrowCoordinator { .kind(Kind::EncryptedDirectMessage) .custom_tag( SingleLetterTag::lowercase(Alphabet::P), - [PublicKey::from_bech32(&self.nostr_client.get_npub()?)?.to_hex()], + [NostrPubkey::from_bech32(&self.nostr_client.get_npub()?)?.to_hex()], ) .since(Timestamp::now()); @@ -50,7 +51,9 @@ impl EscrowCoordinator { .decrypt_msg(&event.content, &event.author()) { dbg!("Received event: {:?}", &decrypted); - if let Ok((contract_hash, contract)) = self.parse(decrypted.as_str()).await { + if let Ok((contract_hash, contract)) = + self.parse_contract(decrypted.as_str()).await + { if self.pending_contracts.contains_key(&contract_hash) { self.pending_contracts.remove(&contract_hash); self.begin_trade(&contract_hash, &contract).await?; @@ -64,7 +67,7 @@ impl EscrowCoordinator { Ok(()) } - async fn parse(&self, content: &str) -> anyhow::Result<([u8; 32], TradeContract)> { + async fn parse_contract(&self, content: &str) -> anyhow::Result<([u8; 32], TradeContract)> { let trade: TradeContract = serde_json::from_str(content)?; // create a Sha256 object @@ -85,12 +88,12 @@ impl EscrowCoordinator { "Beginning trade: {}", contract_hash.to_hex_string(hashes::hex::Case::Lower) ); - let contract_secret = SecretKey::generate(); + let contract_secret = CDKSecretKey::generate(); self.active_contracts.insert( contract_hash.clone(), ActiveTade { - trade_contract: trade.clone(), - coordinator_secret: contract_secret.clone(), + _trade_contract: trade.clone(), + _coordinator_secret: contract_secret.clone(), }, ); self.nostr_client From 7ee5b40f7628475371c2f37be9d1da0785acd5e1 Mon Sep 17 00:00:00 2001 From: rodant Date: Wed, 14 Aug 2024 16:52:49 +0200 Subject: [PATCH 22/43] Fix remaining warnings. --- client/src/nostr/mod.rs | 6 +++--- common/src/nostr/mod.rs | 12 ++++++------ coordinator/src/escrow_coordinator/mod.rs | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/client/src/nostr/mod.rs b/client/src/nostr/mod.rs index 83030f9..fc5560a 100644 --- a/client/src/nostr/mod.rs +++ b/client/src/nostr/mod.rs @@ -36,7 +36,7 @@ impl ClientNostrInstance { ) -> anyhow::Result<()> { let coordinator_pk_bech32 = coordinator_pk.to_bech32()?; self.nostr_client - .send_escrow_contract(contract, &coordinator_pk_bech32) + .send_trade_contract(contract, &coordinator_pk_bech32) .await?; Ok(()) } @@ -48,7 +48,7 @@ impl ClientNostrInstance { coordinator_pk: &PublicKey, ) -> anyhow::Result { let filter_note = Filter::new() - .kind(Kind::EncryptedDirectMessage) + .kind(Kind::PrivateDirectMessage) .since(Timestamp::now()) .author(*coordinator_pk); @@ -95,7 +95,7 @@ impl ClientNostrInstance { metadata: &EscrowRegistration, ) -> anyhow::Result { let filter_note = Filter::new() - .kind(Kind::EncryptedDirectMessage) + .kind(Kind::PrivateDirectMessage) .since(metadata.escrow_start_time) .author(contract.npubkey_buyer); diff --git a/common/src/nostr/mod.rs b/common/src/nostr/mod.rs index 1124f2f..97d09f7 100644 --- a/common/src/nostr/mod.rs +++ b/common/src/nostr/mod.rs @@ -39,7 +39,7 @@ impl NostrClient { } // coordinator specific function? - pub async fn send_escrow_pubkeys( + pub async fn send_escrow_registration( &self, receivers: (PublicKey, PublicKey), id: &[u8; 32], @@ -52,16 +52,16 @@ impl NostrClient { })?; // todo: replace deprecated method self.client - .send_direct_msg(receivers.0, ®istration_json, None) + .send_private_msg(receivers.0, ®istration_json, None) .await?; self.client - .send_direct_msg(receivers.1, ®istration_json, None) + .send_private_msg(receivers.1, ®istration_json, None) .await?; Ok(()) } // client specific function? - pub async fn send_escrow_contract( + pub async fn send_trade_contract( &self, contract: &TradeContract, coordinator_pk_bech32: &str, @@ -69,7 +69,7 @@ impl NostrClient { let message = serde_json::to_string(contract)?; dbg!("sending contract to coordinator..."); self.client - .send_direct_msg( + .send_private_msg( PublicKey::from_bech32(coordinator_pk_bech32)?, &message, None, @@ -85,7 +85,7 @@ impl NostrClient { token: &str, ) -> anyhow::Result<()> { self.client - .send_direct_msg(seller_npubkey, token, None) + .send_private_msg(seller_npubkey, token, None) .await?; Ok(()) } diff --git a/coordinator/src/escrow_coordinator/mod.rs b/coordinator/src/escrow_coordinator/mod.rs index ae06632..75c9691 100644 --- a/coordinator/src/escrow_coordinator/mod.rs +++ b/coordinator/src/escrow_coordinator/mod.rs @@ -31,7 +31,7 @@ impl EscrowCoordinator { pub async fn run(&mut self) -> anyhow::Result<()> { let filter_note = Filter::new() - .kind(Kind::EncryptedDirectMessage) + .kind(Kind::PrivateDirectMessage) .custom_tag( SingleLetterTag::lowercase(Alphabet::P), [NostrPubkey::from_bech32(&self.nostr_client.get_npub()?)?.to_hex()], @@ -97,7 +97,7 @@ impl EscrowCoordinator { }, ); self.nostr_client - .send_escrow_pubkeys( + .send_escrow_registration( (trade.npubkey_buyer, trade.npubkey_seller), contract_hash, &contract_secret.public_key().to_hex(), From a27ea9a308b26acb8735ab5622d82c33963d4058 Mon Sep 17 00:00:00 2001 From: rodant Date: Thu, 15 Aug 2024 17:13:09 +0200 Subject: [PATCH 23/43] bump up nostr_sdk version and revert to nip04 direct messages, nip17 doesn't work. --- Cargo.lock | 28 +++++++++++------------ client/Cargo.toml | 2 +- client/src/nostr/mod.rs | 10 ++++---- common/Cargo.toml | 2 +- common/src/nostr/mod.rs | 8 +++---- coordinator/Cargo.toml | 2 +- coordinator/src/escrow_coordinator/mod.rs | 4 ++-- 7 files changed, 29 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7f11288..c1efb12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1143,9 +1143,9 @@ checksum = "e664971378a3987224f7a0e10059782035e89899ae403718ee07de85bec42afe" [[package]] name = "nostr" -version = "0.32.1" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7948938314ee0392f378ab1a5d58b4707f2207550bc410b1629a80a4f28af7d" +checksum = "f08db214560a34bf7c4c1fea09a8461b9412bae58ba06e99ce3177d89fa1e0a6" dependencies = [ "aes", "base64 0.21.7", @@ -1173,9 +1173,9 @@ dependencies = [ [[package]] name = "nostr-database" -version = "0.32.0" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88a72f92fbd5d2514db36e07a864646f1c1f44931c4a5ea195f6961029af4b3" +checksum = "50eebf5020d70891e3c229128de5fc73af632b651d02742383b314d3d0c7e953" dependencies = [ "async-trait", "lru", @@ -1187,9 +1187,9 @@ dependencies = [ [[package]] name = "nostr-relay-pool" -version = "0.32.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7b7bf72b02a24ccc7cf87033fa5ddfd57001c7d8c2e757321a7ca7a6df39876" +checksum = "afa5502a3df456790ca16d90cc688a677117d57ab56b079dcfa091390ac9f202" dependencies = [ "async-utility", "async-wsocket", @@ -1203,9 +1203,9 @@ dependencies = [ [[package]] name = "nostr-sdk" -version = "0.32.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "005915a59ee6401f23ba510c3a9ac4a07b457f80dfe1dc05cd2c8fdbde439246" +checksum = "b427dceefbbb49a9dd98abb8c4e40d25fdd467e99821aaad88615252bdb915bd" dependencies = [ "async-utility", "atomic-destructor", @@ -1223,9 +1223,9 @@ dependencies = [ [[package]] name = "nostr-signer" -version = "0.32.1" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f99449c2077bef43c02c8f9a9386d01c87e7ad8ece70d7de87a2c59771b4c0fe" +checksum = "665268b316f41cd8fa791be54b6c7935c5a239461708c380a699d6677be9af38" dependencies = [ "async-utility", "nostr", @@ -1237,9 +1237,9 @@ dependencies = [ [[package]] name = "nostr-zapper" -version = "0.32.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430c2527c0efd2e7f1a421b0c7df01a03b334a79c60c39cc7a1ca684f720f2bf" +checksum = "69922e74f8eab1f9d287008c0c06acdec87277a2d8f44bd9d38e003422aea0ab" dependencies = [ "async-trait", "nostr", @@ -1273,9 +1273,9 @@ dependencies = [ [[package]] name = "nwc" -version = "0.32.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fb91e4be3f6b872fc23c7714bbb500a58a1d59f458eb6eb9dd249fbec42fc2" +checksum = "bb2e04b3edb5e9572e95b62842430625f1718e8a4a3596a30aeb04e6734764ea" dependencies = [ "async-utility", "nostr", diff --git a/client/Cargo.toml b/client/Cargo.toml index f185856..ec53a1f 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -nostr-sdk = { version = "0.32.0", features = ["nip04"] } +nostr-sdk = { version = "0.33.0", features = ["nip04"] } cdk = "0.1.1" dotenv = "0.15.0" anyhow = "1.0.86" diff --git a/client/src/nostr/mod.rs b/client/src/nostr/mod.rs index fc5560a..f840f7f 100644 --- a/client/src/nostr/mod.rs +++ b/client/src/nostr/mod.rs @@ -48,7 +48,7 @@ impl ClientNostrInstance { coordinator_pk: &PublicKey, ) -> anyhow::Result { let filter_note = Filter::new() - .kind(Kind::PrivateDirectMessage) + .kind(Kind::EncryptedDirectMessage) .since(Timestamp::now()) .author(*coordinator_pk); @@ -56,7 +56,8 @@ impl ClientNostrInstance { .nostr_client .client .subscribe(vec![filter_note], None) - .await; + .await? + .val; let mut notifications = self.nostr_client.client.notifications(); @@ -95,7 +96,7 @@ impl ClientNostrInstance { metadata: &EscrowRegistration, ) -> anyhow::Result { let filter_note = Filter::new() - .kind(Kind::PrivateDirectMessage) + .kind(Kind::EncryptedDirectMessage) .since(metadata.escrow_start_time) .author(contract.npubkey_buyer); @@ -103,7 +104,8 @@ impl ClientNostrInstance { .nostr_client .client .subscribe(vec![filter_note], None) - .await; + .await? + .val; let mut notifications = self.nostr_client.client.notifications(); diff --git a/common/Cargo.toml b/common/Cargo.toml index f85e479..84e8430 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -nostr-sdk = { version = "0.32.0", features = ["nip04"] } +nostr-sdk = { version = "0.33.0", features = ["nip04"] } cdk = "0.1.1" anyhow = "1.0.86" tokio = "1.38.0" diff --git a/common/src/nostr/mod.rs b/common/src/nostr/mod.rs index 97d09f7..b05f6b0 100644 --- a/common/src/nostr/mod.rs +++ b/common/src/nostr/mod.rs @@ -52,10 +52,10 @@ impl NostrClient { })?; // todo: replace deprecated method self.client - .send_private_msg(receivers.0, ®istration_json, None) + .send_direct_msg(receivers.0, ®istration_json, None) .await?; self.client - .send_private_msg(receivers.1, ®istration_json, None) + .send_direct_msg(receivers.1, ®istration_json, None) .await?; Ok(()) } @@ -69,7 +69,7 @@ impl NostrClient { let message = serde_json::to_string(contract)?; dbg!("sending contract to coordinator..."); self.client - .send_private_msg( + .send_direct_msg( PublicKey::from_bech32(coordinator_pk_bech32)?, &message, None, @@ -85,7 +85,7 @@ impl NostrClient { token: &str, ) -> anyhow::Result<()> { self.client - .send_private_msg(seller_npubkey, token, None) + .send_direct_msg(seller_npubkey, token, None) .await?; Ok(()) } diff --git a/coordinator/Cargo.toml b/coordinator/Cargo.toml index f597119..ff6ec1f 100644 --- a/coordinator/Cargo.toml +++ b/coordinator/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -nostr-sdk = { version = "0.32.0", features = ["nip04"] } +nostr-sdk = { version = "0.33.0", features = ["nip04"] } cdk = "0.1.1" dotenv = "0.15.0" anyhow = "1.0.86" diff --git a/coordinator/src/escrow_coordinator/mod.rs b/coordinator/src/escrow_coordinator/mod.rs index 75c9691..c4a5dc4 100644 --- a/coordinator/src/escrow_coordinator/mod.rs +++ b/coordinator/src/escrow_coordinator/mod.rs @@ -31,7 +31,7 @@ impl EscrowCoordinator { pub async fn run(&mut self) -> anyhow::Result<()> { let filter_note = Filter::new() - .kind(Kind::PrivateDirectMessage) + .kind(Kind::EncryptedDirectMessage) .custom_tag( SingleLetterTag::lowercase(Alphabet::P), [NostrPubkey::from_bech32(&self.nostr_client.get_npub()?)?.to_hex()], @@ -41,7 +41,7 @@ impl EscrowCoordinator { self.nostr_client .client .subscribe(vec![filter_note], None) - .await; + .await?; let mut notifications = self.nostr_client.client.notifications(); while let Ok(notification) = notifications.recv().await { From 85ce4d01cbae5eb902f0210f9e38e398339d47b0 Mon Sep 17 00:00:00 2001 From: rodant Date: Fri, 16 Aug 2024 10:57:09 +0200 Subject: [PATCH 24/43] working version with nip17 private direct messages. --- client/Cargo.toml | 2 +- client/src/escrow_client/mod.rs | 3 ++- client/src/nostr/mod.rs | 33 +++++++++++------------ common/Cargo.toml | 2 +- common/src/model.rs | 2 +- common/src/nostr/mod.rs | 18 ++++--------- coordinator/Cargo.toml | 2 +- coordinator/src/escrow_coordinator/mod.rs | 18 +++++-------- 8 files changed, 34 insertions(+), 46 deletions(-) diff --git a/client/Cargo.toml b/client/Cargo.toml index ec53a1f..2f46d12 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -nostr-sdk = { version = "0.33.0", features = ["nip04"] } +nostr-sdk = { version = "0.33.0", features = [] } cdk = "0.1.1" dotenv = "0.15.0" anyhow = "1.0.86" diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index 467909e..433a303 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -48,9 +48,10 @@ impl EscrowClient { .submit_escrow_contract(&self.escrow_contract, coordinator_pk) .await?; + let my_pubkey = PublicKey::from_bech32(self.nostr_instance.nostr_client.get_npub()?)?; let escrow_registration = self .nostr_instance - .receive_registration_message(coordinator_pk) + .receive_registration_message(&my_pubkey) .await?; self.escrow_registration = Some(escrow_registration); diff --git a/client/src/nostr/mod.rs b/client/src/nostr/mod.rs index f840f7f..23e8df3 100644 --- a/client/src/nostr/mod.rs +++ b/client/src/nostr/mod.rs @@ -45,12 +45,12 @@ impl ClientNostrInstance { // the escrow service is confirmed by the coordinator pub async fn receive_registration_message( &self, - coordinator_pk: &PublicKey, + receiver_pk: &PublicKey, ) -> anyhow::Result { let filter_note = Filter::new() - .kind(Kind::EncryptedDirectMessage) - .since(Timestamp::now()) - .author(*coordinator_pk); + .kind(Kind::GiftWrap) + .pubkey(*receiver_pk) + .limit(0); let subscription_id = self .nostr_client @@ -63,12 +63,12 @@ impl ClientNostrInstance { while let Ok(notification) = notifications.recv().await { if let RelayPoolNotification::Event { event, .. } = notification { - if let Some(decrypted) = self - .nostr_client - .decrypt_msg(&event.content, &event.author()) + if let Ok(unwrapped_gift) = self.nostr_client.client.unwrap_gift_wrap(&event).await { - debug!("Received event: {:?}", &decrypted); - if let Ok(escrow_registration) = serde_json::from_str(&decrypted) { + if let Ok(escrow_registration) = + serde_json::from_str(&unwrapped_gift.rumor.content) + { + debug!("Received escrow registration: {:?}", &escrow_registration); self.nostr_client.client.unsubscribe(subscription_id).await; return Ok(escrow_registration); } @@ -96,9 +96,9 @@ impl ClientNostrInstance { metadata: &EscrowRegistration, ) -> anyhow::Result { let filter_note = Filter::new() - .kind(Kind::EncryptedDirectMessage) - .since(metadata.escrow_start_time) - .author(contract.npubkey_buyer); + .kind(Kind::GiftWrap) + .pubkey(contract.npubkey_seller) + .limit(0); let subscription_id = self .nostr_client @@ -111,14 +111,13 @@ impl ClientNostrInstance { while let Ok(notification) = notifications.recv().await { if let RelayPoolNotification::Event { event, .. } = notification { - if let Some(decrypted) = self - .nostr_client - .decrypt_msg(&event.content, &event.author()) + if let Ok(unwrapped_gift) = self.nostr_client.client.unwrap_gift_wrap(&event).await { - debug!("Received token event: {:?}", &decrypted); + let escrow_token = &unwrapped_gift.rumor.content; if let Ok(escrow_token) = - wallet.validate_escrow_token(&decrypted, contract, metadata) + wallet.validate_escrow_token(escrow_token, contract, metadata) { + debug!("Received token event: {:?}", escrow_token); self.nostr_client.client.unsubscribe(subscription_id).await; return Ok(escrow_token); } diff --git a/common/Cargo.toml b/common/Cargo.toml index 84e8430..abfa5f4 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -nostr-sdk = { version = "0.33.0", features = ["nip04"] } +nostr-sdk = { version = "0.33.0", features = [] } cdk = "0.1.1" anyhow = "1.0.86" tokio = "1.38.0" diff --git a/common/src/model.rs b/common/src/model.rs index 21795e1..d7dfe93 100644 --- a/common/src/model.rs +++ b/common/src/model.rs @@ -14,7 +14,7 @@ pub struct TradeContract { pub buyer_ecash_public_key: String, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct EscrowRegistration { pub escrow_id_hex: String, #[serde(with = "crate::cdk_pubkey_serde")] diff --git a/common/src/nostr/mod.rs b/common/src/nostr/mod.rs index b05f6b0..474dfa7 100644 --- a/common/src/nostr/mod.rs +++ b/common/src/nostr/mod.rs @@ -1,5 +1,5 @@ use crate::model::{EscrowRegistration, TradeContract}; -use nostr_sdk::{nostr::nips::nip04, prelude::*}; +use nostr_sdk::prelude::*; pub struct NostrClient { keypair: Keys, @@ -30,14 +30,6 @@ impl NostrClient { Ok(self.keypair.public_key().to_bech32()?) } - pub fn decrypt_msg(&self, msg: &str, sender_pk: &PublicKey) -> Option { - let secret_key = self - .keypair - .secret_key() - .expect("The key pair must be set if we have a valid instance."); - nip04::decrypt(secret_key, sender_pk, msg).ok() - } - // coordinator specific function? pub async fn send_escrow_registration( &self, @@ -52,10 +44,10 @@ impl NostrClient { })?; // todo: replace deprecated method self.client - .send_direct_msg(receivers.0, ®istration_json, None) + .send_private_msg(receivers.0, ®istration_json, None) .await?; self.client - .send_direct_msg(receivers.1, ®istration_json, None) + .send_private_msg(receivers.1, ®istration_json, None) .await?; Ok(()) } @@ -69,7 +61,7 @@ impl NostrClient { let message = serde_json::to_string(contract)?; dbg!("sending contract to coordinator..."); self.client - .send_direct_msg( + .send_private_msg( PublicKey::from_bech32(coordinator_pk_bech32)?, &message, None, @@ -85,7 +77,7 @@ impl NostrClient { token: &str, ) -> anyhow::Result<()> { self.client - .send_direct_msg(seller_npubkey, token, None) + .send_private_msg(seller_npubkey, token, None) .await?; Ok(()) } diff --git a/coordinator/Cargo.toml b/coordinator/Cargo.toml index ff6ec1f..e1c31e3 100644 --- a/coordinator/Cargo.toml +++ b/coordinator/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -nostr-sdk = { version = "0.33.0", features = ["nip04"] } +nostr-sdk = { version = "0.33.0", features = [] } cdk = "0.1.1" dotenv = "0.15.0" anyhow = "1.0.86" diff --git a/coordinator/src/escrow_coordinator/mod.rs b/coordinator/src/escrow_coordinator/mod.rs index c4a5dc4..26e95ab 100644 --- a/coordinator/src/escrow_coordinator/mod.rs +++ b/coordinator/src/escrow_coordinator/mod.rs @@ -30,13 +30,11 @@ impl EscrowCoordinator { } pub async fn run(&mut self) -> anyhow::Result<()> { + let my_pubkey = NostrPubkey::from_bech32(&self.nostr_client.get_npub()?)?; let filter_note = Filter::new() - .kind(Kind::EncryptedDirectMessage) - .custom_tag( - SingleLetterTag::lowercase(Alphabet::P), - [NostrPubkey::from_bech32(&self.nostr_client.get_npub()?)?.to_hex()], - ) - .since(Timestamp::now()); + .kind(Kind::GiftWrap) + .pubkey(my_pubkey) + .limit(0); self.nostr_client .client @@ -46,14 +44,12 @@ impl EscrowCoordinator { while let Ok(notification) = notifications.recv().await { if let RelayPoolNotification::Event { event, .. } = notification { - if let Some(decrypted) = self - .nostr_client - .decrypt_msg(&event.content, &event.author()) + if let Ok(unwrapped_gift) = self.nostr_client.client.unwrap_gift_wrap(&event).await { - dbg!("Received event: {:?}", &decrypted); if let Ok((contract_hash, contract)) = - self.parse_contract(decrypted.as_str()).await + self.parse_contract(&unwrapped_gift.rumor.content).await { + dbg!("Received contract: {}", &contract.trade_description); if self.pending_contracts.contains_key(&contract_hash) { self.pending_contracts.remove(&contract_hash); self.begin_trade(&contract_hash, &contract).await?; From a9714b7865dd60168a1dcec1cbdbef474ffa153d Mon Sep 17 00:00:00 2001 From: rodant Date: Sun, 18 Aug 2024 22:52:53 +0200 Subject: [PATCH 25/43] Make func sync after review comment. --- client/src/escrow_client/mod.rs | 2 +- client/src/nostr/mod.rs | 69 ++++------------------- common/src/nostr/mod.rs | 31 ++++++++++ coordinator/src/escrow_coordinator/mod.rs | 26 ++++----- 4 files changed, 55 insertions(+), 73 deletions(-) diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index 433a303..4f75c22 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -51,7 +51,7 @@ impl EscrowClient { let my_pubkey = PublicKey::from_bech32(self.nostr_instance.nostr_client.get_npub()?)?; let escrow_registration = self .nostr_instance - .receive_registration_message(&my_pubkey) + .receive_registration_message(my_pubkey) .await?; self.escrow_registration = Some(escrow_registration); diff --git a/client/src/nostr/mod.rs b/client/src/nostr/mod.rs index 23e8df3..f8df8c3 100644 --- a/client/src/nostr/mod.rs +++ b/client/src/nostr/mod.rs @@ -45,37 +45,13 @@ impl ClientNostrInstance { // the escrow service is confirmed by the coordinator pub async fn receive_registration_message( &self, - receiver_pk: &PublicKey, + receiver_pk: PublicKey, ) -> anyhow::Result { - let filter_note = Filter::new() - .kind(Kind::GiftWrap) - .pubkey(*receiver_pk) - .limit(0); - - let subscription_id = self + let message = self .nostr_client - .client - .subscribe(vec![filter_note], None) - .await? - .val; - - let mut notifications = self.nostr_client.client.notifications(); - - while let Ok(notification) = notifications.recv().await { - if let RelayPoolNotification::Event { event, .. } = notification { - if let Ok(unwrapped_gift) = self.nostr_client.client.unwrap_gift_wrap(&event).await - { - if let Ok(escrow_registration) = - serde_json::from_str(&unwrapped_gift.rumor.content) - { - debug!("Received escrow registration: {:?}", &escrow_registration); - self.nostr_client.client.unsubscribe(subscription_id).await; - return Ok(escrow_registration); - } - } - } - } - Err(anyhow!("No valid escrow coordinator public key received")) + .receive_escrow_message(receiver_pk) + .await?; + Ok(serde_json::from_str(&message)?) } pub async fn submit_trade_token_to_seller( @@ -93,37 +69,12 @@ impl ClientNostrInstance { &self, wallet: &ClientEcashWallet, contract: &TradeContract, - metadata: &EscrowRegistration, + registration: &EscrowRegistration, ) -> anyhow::Result { - let filter_note = Filter::new() - .kind(Kind::GiftWrap) - .pubkey(contract.npubkey_seller) - .limit(0); - - let subscription_id = self + let message = self .nostr_client - .client - .subscribe(vec![filter_note], None) - .await? - .val; - - let mut notifications = self.nostr_client.client.notifications(); - - while let Ok(notification) = notifications.recv().await { - if let RelayPoolNotification::Event { event, .. } = notification { - if let Ok(unwrapped_gift) = self.nostr_client.client.unwrap_gift_wrap(&event).await - { - let escrow_token = &unwrapped_gift.rumor.content; - if let Ok(escrow_token) = - wallet.validate_escrow_token(escrow_token, contract, metadata) - { - debug!("Received token event: {:?}", escrow_token); - self.nostr_client.client.unsubscribe(subscription_id).await; - return Ok(escrow_token); - } - } - } - } - Err(anyhow!("No valid escrow token received")) + .receive_escrow_message(contract.npubkey_buyer) + .await?; + wallet.validate_escrow_token(&message, contract, registration) } } diff --git a/common/src/nostr/mod.rs b/common/src/nostr/mod.rs index 474dfa7..85f2494 100644 --- a/common/src/nostr/mod.rs +++ b/common/src/nostr/mod.rs @@ -1,4 +1,5 @@ use crate::model::{EscrowRegistration, TradeContract}; +use anyhow::anyhow; use nostr_sdk::prelude::*; pub struct NostrClient { @@ -30,6 +31,36 @@ impl NostrClient { Ok(self.keypair.public_key().to_bech32()?) } + pub async fn receive_escrow_message( + &self, + receiver_pubkey: PublicKey, + ) -> anyhow::Result { + let message_filter = Filter::new() + .kind(Kind::GiftWrap) + .pubkey(receiver_pubkey) + .limit(0); + + let subscription_id = self.client.subscribe(vec![message_filter], None).await?.val; + + let mut notifications = self.client.notifications(); + + while let Ok(notification) = notifications.recv().await { + if let RelayPoolNotification::Event { event, .. } = notification { + let rumor = self.client.unwrap_gift_wrap(&event).await?.rumor; + if rumor.kind == Kind::PrivateDirectMessage { + { + self.client.unsubscribe(subscription_id.clone()).await; + return Ok(rumor.content); + } + } + } + } + { + self.client.unsubscribe(subscription_id.clone()).await; + Err(anyhow!("Got no valid receiver public key")) + } + } + // coordinator specific function? pub async fn send_escrow_registration( &self, diff --git a/coordinator/src/escrow_coordinator/mod.rs b/coordinator/src/escrow_coordinator/mod.rs index 26e95ab..be95432 100644 --- a/coordinator/src/escrow_coordinator/mod.rs +++ b/coordinator/src/escrow_coordinator/mod.rs @@ -47,7 +47,7 @@ impl EscrowCoordinator { if let Ok(unwrapped_gift) = self.nostr_client.client.unwrap_gift_wrap(&event).await { if let Ok((contract_hash, contract)) = - self.parse_contract(&unwrapped_gift.rumor.content).await + parse_contract(&unwrapped_gift.rumor.content) { dbg!("Received contract: {}", &contract.trade_description); if self.pending_contracts.contains_key(&contract_hash) { @@ -63,18 +63,6 @@ impl EscrowCoordinator { Ok(()) } - async fn parse_contract(&self, content: &str) -> anyhow::Result<([u8; 32], TradeContract)> { - let trade: TradeContract = serde_json::from_str(content)?; - - // create a Sha256 object - let mut hasher = Sha256::new(); - // write input message - hasher.update(content.as_bytes()); - // read hash digest and consume hasher - let trade_hash: [u8; 32] = hasher.finalize().into(); - Ok((trade_hash, trade)) - } - async fn begin_trade( &mut self, contract_hash: &[u8; 32], @@ -106,3 +94,15 @@ impl EscrowCoordinator { // Ok(()) // } } + +fn parse_contract(content: &str) -> anyhow::Result<([u8; 32], TradeContract)> { + let contract: TradeContract = serde_json::from_str(content)?; + + // create a Sha256 object + let mut hasher = Sha256::new(); + // write input message + hasher.update(content.as_bytes()); + // read hash digest and consume hasher + let trade_hash: [u8; 32] = hasher.finalize().into(); + Ok((trade_hash, contract)) +} From 13082e74ea3996d8b03475d04c5d783a723a6f4f Mon Sep 17 00:00:00 2001 From: rodant Date: Tue, 20 Aug 2024 11:28:13 +0200 Subject: [PATCH 26/43] Introduce timeout and improvements in receive_escrow_message --- client/src/nostr/mod.rs | 16 +++++++++------- common/src/nostr/mod.rs | 32 ++++++++++++++++++++------------ 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/client/src/nostr/mod.rs b/client/src/nostr/mod.rs index f8df8c3..8910c1d 100644 --- a/client/src/nostr/mod.rs +++ b/client/src/nostr/mod.rs @@ -1,4 +1,6 @@ +use anyhow::Result; use cashu_escrow_common::model::EscrowRegistration; +use cdk::nuts::Token; use nostr_sdk::PublicKey; use super::*; @@ -14,7 +16,7 @@ pub struct ClientNostrInstance { } impl ClientNostrInstance { - pub async fn from_client_cli_input(cli_input: &ClientCliInput) -> anyhow::Result { + pub async fn from_client_cli_input(cli_input: &ClientCliInput) -> Result { let nostr_client = NostrClient::new( &cli_input .trader_nostr_keys @@ -33,7 +35,7 @@ impl ClientNostrInstance { &self, contract: &TradeContract, coordinator_pk: &PublicKey, - ) -> anyhow::Result<()> { + ) -> Result<()> { let coordinator_pk_bech32 = coordinator_pk.to_bech32()?; self.nostr_client .send_trade_contract(contract, &coordinator_pk_bech32) @@ -46,10 +48,10 @@ impl ClientNostrInstance { pub async fn receive_registration_message( &self, receiver_pk: PublicKey, - ) -> anyhow::Result { + ) -> Result { let message = self .nostr_client - .receive_escrow_message(receiver_pk) + .receive_escrow_message(receiver_pk, 10) .await?; Ok(serde_json::from_str(&message)?) } @@ -58,7 +60,7 @@ impl ClientNostrInstance { &self, seller_npubkey: PublicKey, token: &str, - ) -> anyhow::Result<()> { + ) -> Result<()> { self.nostr_client .send_trade_token_to_seller(seller_npubkey, token) .await?; @@ -70,10 +72,10 @@ impl ClientNostrInstance { wallet: &ClientEcashWallet, contract: &TradeContract, registration: &EscrowRegistration, - ) -> anyhow::Result { + ) -> Result { let message = self .nostr_client - .receive_escrow_message(contract.npubkey_buyer) + .receive_escrow_message(contract.npubkey_buyer, 10) .await?; wallet.validate_escrow_token(&message, contract, registration) } diff --git a/common/src/nostr/mod.rs b/common/src/nostr/mod.rs index 85f2494..e5e4ffc 100644 --- a/common/src/nostr/mod.rs +++ b/common/src/nostr/mod.rs @@ -1,6 +1,9 @@ +use std::time::Duration; + use crate::model::{EscrowRegistration, TradeContract}; use anyhow::anyhow; use nostr_sdk::prelude::*; +use tokio::time::timeout; pub struct NostrClient { keypair: Keys, @@ -34,6 +37,7 @@ impl NostrClient { pub async fn receive_escrow_message( &self, receiver_pubkey: PublicKey, + timeout_secs: u64, ) -> anyhow::Result { let message_filter = Filter::new() .kind(Kind::GiftWrap) @@ -44,21 +48,25 @@ impl NostrClient { let mut notifications = self.client.notifications(); - while let Ok(notification) = notifications.recv().await { - if let RelayPoolNotification::Event { event, .. } = notification { - let rumor = self.client.unwrap_gift_wrap(&event).await?.rumor; - if rumor.kind == Kind::PrivateDirectMessage { - { - self.client.unsubscribe(subscription_id.clone()).await; - return Ok(rumor.content); + let loop_future = async { + loop { + if let Ok(notification) = notifications.recv().await { + if let RelayPoolNotification::Event { event, .. } = notification { + let rumor = self.client.unwrap_gift_wrap(&event).await?.rumor; + if rumor.kind == Kind::PrivateDirectMessage { + break Ok(rumor.content) as anyhow::Result; + } } } } - } - { - self.client.unsubscribe(subscription_id.clone()).await; - Err(anyhow!("Got no valid receiver public key")) - } + }; + let result = match timeout(Duration::from_secs(timeout_secs), loop_future).await { + Ok(result) => result, + Err(e) => Err(anyhow!("Timeout, {}", e)), + }; + self.client.unsubscribe(subscription_id.clone()).await; + + result } // coordinator specific function? From 542a2be6aad5c44890c3a6a054a00e9a8ceaf64c Mon Sep 17 00:00:00 2001 From: rodant Date: Tue, 20 Aug 2024 11:58:30 +0200 Subject: [PATCH 27/43] Better having a get public key method than a get npub. --- client/src/escrow_client/mod.rs | 2 +- common/src/nostr/mod.rs | 4 ++-- coordinator/src/escrow_coordinator/mod.rs | 2 +- coordinator/src/main.rs | 6 +++++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index 4f75c22..19352ae 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -48,7 +48,7 @@ impl EscrowClient { .submit_escrow_contract(&self.escrow_contract, coordinator_pk) .await?; - let my_pubkey = PublicKey::from_bech32(self.nostr_instance.nostr_client.get_npub()?)?; + let my_pubkey = self.nostr_instance.nostr_client.public_key(); let escrow_registration = self .nostr_instance .receive_registration_message(my_pubkey) diff --git a/common/src/nostr/mod.rs b/common/src/nostr/mod.rs index e5e4ffc..cfaea85 100644 --- a/common/src/nostr/mod.rs +++ b/common/src/nostr/mod.rs @@ -30,8 +30,8 @@ impl NostrClient { Ok(Self { keypair, client }) } - pub fn get_npub(&self) -> anyhow::Result { - Ok(self.keypair.public_key().to_bech32()?) + pub fn public_key(&self) -> PublicKey { + self.keypair.public_key() } pub async fn receive_escrow_message( diff --git a/coordinator/src/escrow_coordinator/mod.rs b/coordinator/src/escrow_coordinator/mod.rs index be95432..0157798 100644 --- a/coordinator/src/escrow_coordinator/mod.rs +++ b/coordinator/src/escrow_coordinator/mod.rs @@ -30,7 +30,7 @@ impl EscrowCoordinator { } pub async fn run(&mut self) -> anyhow::Result<()> { - let my_pubkey = NostrPubkey::from_bech32(&self.nostr_client.get_npub()?)?; + let my_pubkey = self.nostr_client.public_key(); let filter_note = Filter::new() .kind(Kind::GiftWrap) .pubkey(my_pubkey) diff --git a/coordinator/src/main.rs b/coordinator/src/main.rs index 5abd451..f05206e 100644 --- a/coordinator/src/main.rs +++ b/coordinator/src/main.rs @@ -5,12 +5,16 @@ use std::env; use cashu_escrow_common::nostr::NostrClient; use dotenv::dotenv; use escrow_coordinator::EscrowCoordinator; +use nostr_sdk::ToBech32; #[tokio::main] async fn main() -> anyhow::Result<()> { dotenv().ok(); let nostr_client = NostrClient::new(&env::var("ESCROW_NSEC")?).await?; - println!("Coordinator npub: {}", nostr_client.get_npub()?); + println!( + "Coordinator npub: {}", + nostr_client.public_key().to_bech32()? + ); println!("Starting service and waiting for trades..."); return EscrowCoordinator::setup(nostr_client).await?.run().await; } From f001e9cd179851da84ecc6ba30e82449d2256c81 Mon Sep 17 00:00:00 2001 From: rodant Date: Tue, 20 Aug 2024 13:22:54 +0200 Subject: [PATCH 28/43] Remove dependency to cli in ClientNostrClient. --- client/src/main.rs | 2 +- client/src/nostr/mod.rs | 11 ++--------- common/src/nostr/mod.rs | 12 +++++------- coordinator/src/escrow_coordinator/mod.rs | 1 - coordinator/src/main.rs | 7 ++++--- 5 files changed, 12 insertions(+), 21 deletions(-) diff --git a/client/src/main.rs b/client/src/main.rs index 30cd173..dd4c52b 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -34,7 +34,7 @@ async fn main() -> anyhow::Result<()> { let cli_input = ClientCliInput::parse().await?; let escrow_contract = TradeContract::from_client_cli_input(&cli_input, escrow_wallet.trade_pubkey.clone())?; - let nostr_instance = ClientNostrInstance::from_client_cli_input(&cli_input).await?; + let nostr_instance = ClientNostrInstance::new(cli_input.trader_nostr_keys).await?; let mut escrow_client = EscrowClient::new( nostr_instance, escrow_wallet, diff --git a/client/src/nostr/mod.rs b/client/src/nostr/mod.rs index 8910c1d..9258095 100644 --- a/client/src/nostr/mod.rs +++ b/client/src/nostr/mod.rs @@ -16,15 +16,8 @@ pub struct ClientNostrInstance { } impl ClientNostrInstance { - pub async fn from_client_cli_input(cli_input: &ClientCliInput) -> Result { - let nostr_client = NostrClient::new( - &cli_input - .trader_nostr_keys - .secret_key() - .unwrap() - .to_bech32()?, - ) - .await?; + pub async fn new(trader_keys: Keys) -> Result { + let nostr_client = NostrClient::new(trader_keys).await?; Ok(Self { nostr_client }) } diff --git a/common/src/nostr/mod.rs b/common/src/nostr/mod.rs index cfaea85..0365ab9 100644 --- a/common/src/nostr/mod.rs +++ b/common/src/nostr/mod.rs @@ -6,15 +6,13 @@ use nostr_sdk::prelude::*; use tokio::time::timeout; pub struct NostrClient { - keypair: Keys, + keys: Keys, pub client: Client, } impl NostrClient { - pub async fn new(nsec: &String) -> anyhow::Result { - let keypair = Keys::parse(nsec)?; - - let client = Client::new(&keypair); + pub async fn new(keys: Keys) -> anyhow::Result { + let client = Client::new(&keys); client.add_relay("wss://relay.damus.io").await?; client.add_relay("wss://relay.primal.net").await?; @@ -27,11 +25,11 @@ impl NostrClient { // Connect to relays client.connect().await; - Ok(Self { keypair, client }) + Ok(Self { keys, client }) } pub fn public_key(&self) -> PublicKey { - self.keypair.public_key() + self.keys.public_key() } pub async fn receive_escrow_message( diff --git a/coordinator/src/escrow_coordinator/mod.rs b/coordinator/src/escrow_coordinator/mod.rs index 0157798..17dbfb5 100644 --- a/coordinator/src/escrow_coordinator/mod.rs +++ b/coordinator/src/escrow_coordinator/mod.rs @@ -2,7 +2,6 @@ use super::*; use cashu_escrow_common::model::TradeContract; use cdk::nuts::SecretKey as CDKSecretKey; use hashes::hex::DisplayHex; -use ndk::prelude::PublicKey as NostrPubkey; use ndk::prelude::*; use ndk::{Filter, Kind, RelayPoolNotification}; use nostr_sdk as ndk; diff --git a/coordinator/src/main.rs b/coordinator/src/main.rs index f05206e..f435fe7 100644 --- a/coordinator/src/main.rs +++ b/coordinator/src/main.rs @@ -1,16 +1,17 @@ mod escrow_coordinator; -use std::env; +use std::{env, str::FromStr}; use cashu_escrow_common::nostr::NostrClient; use dotenv::dotenv; use escrow_coordinator::EscrowCoordinator; -use nostr_sdk::ToBech32; +use nostr_sdk::{Keys, ToBech32}; #[tokio::main] async fn main() -> anyhow::Result<()> { dotenv().ok(); - let nostr_client = NostrClient::new(&env::var("ESCROW_NSEC")?).await?; + let keys = Keys::from_str(&env::var("ESCROW_NSEC")?)?; + let nostr_client = NostrClient::new(keys).await?; println!( "Coordinator npub: {}", nostr_client.public_key().to_bech32()? From 5014664ea5088ee4c9ed843697668cf43a058913 Mon Sep 17 00:00:00 2001 From: rodant Date: Tue, 20 Aug 2024 13:31:05 +0200 Subject: [PATCH 29/43] Hide nostr client in ClientNostrInstance. --- client/src/escrow_client/mod.rs | 2 +- client/src/nostr/mod.rs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index 19352ae..4b68c41 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -48,7 +48,7 @@ impl EscrowClient { .submit_escrow_contract(&self.escrow_contract, coordinator_pk) .await?; - let my_pubkey = self.nostr_instance.nostr_client.public_key(); + let my_pubkey = self.nostr_instance.public_key(); let escrow_registration = self .nostr_instance .receive_registration_message(my_pubkey) diff --git a/client/src/nostr/mod.rs b/client/src/nostr/mod.rs index 9258095..0c397de 100644 --- a/client/src/nostr/mod.rs +++ b/client/src/nostr/mod.rs @@ -12,7 +12,7 @@ use super::*; // As they are only used by the client. pub struct ClientNostrInstance { - pub nostr_client: NostrClient, + nostr_client: NostrClient, } impl ClientNostrInstance { @@ -72,4 +72,8 @@ impl ClientNostrInstance { .await?; wallet.validate_escrow_token(&message, contract, registration) } + + pub(crate) fn public_key(&self) -> PublicKey { + self.nostr_client.public_key() + } } From 9ae1ba47ea9e2cc56cfb267b72c904ff2762c884 Mon Sep 17 00:00:00 2001 From: rodant Date: Wed, 21 Aug 2024 13:08:55 +0200 Subject: [PATCH 30/43] Improve the coordinator loop for running forever. --- coordinator/src/escrow_coordinator/mod.rs | 35 ++++++++++++++--------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/coordinator/src/escrow_coordinator/mod.rs b/coordinator/src/escrow_coordinator/mod.rs index 17dbfb5..8a7f44a 100644 --- a/coordinator/src/escrow_coordinator/mod.rs +++ b/coordinator/src/escrow_coordinator/mod.rs @@ -41,25 +41,34 @@ impl EscrowCoordinator { .await?; let mut notifications = self.nostr_client.client.notifications(); - while let Ok(notification) = notifications.recv().await { - if let RelayPoolNotification::Event { event, .. } = notification { - if let Ok(unwrapped_gift) = self.nostr_client.client.unwrap_gift_wrap(&event).await - { - if let Ok((contract_hash, contract)) = - parse_contract(&unwrapped_gift.rumor.content) + loop { + if let Ok(notification) = notifications.recv().await { + if let RelayPoolNotification::Event { event, .. } = notification { + if let Ok(unwrapped_gift) = + self.nostr_client.client.unwrap_gift_wrap(&event).await { - dbg!("Received contract: {}", &contract.trade_description); - if self.pending_contracts.contains_key(&contract_hash) { - self.pending_contracts.remove(&contract_hash); - self.begin_trade(&contract_hash, &contract).await?; - } else { - self.pending_contracts.insert(contract_hash, contract); + let rumor = unwrapped_gift.rumor; + if rumor.kind == Kind::PrivateDirectMessage { + if let Ok((contract_hash, contract)) = parse_contract(&rumor.content) { + dbg!("Received contract: {}", &contract.trade_description); + if self.pending_contracts.contains_key(&contract_hash) { + self.pending_contracts.remove(&contract_hash); + let _ = self + .begin_trade(&contract_hash, &contract) + .await + .inspect_err(|e| { + //todo: use logger instead + println!("Got error while beginning a trade: {}", e); + }); + } else { + self.pending_contracts.insert(contract_hash, contract); + } + } } } } } } - Ok(()) } async fn begin_trade( From 0d7a2a2ce5becf0ba6328946f71ef9a819cd77df Mon Sep 17 00:00:00 2001 From: rodant Date: Wed, 21 Aug 2024 13:34:24 +0200 Subject: [PATCH 31/43] Improvement for error situations. --- coordinator/src/escrow_coordinator/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/coordinator/src/escrow_coordinator/mod.rs b/coordinator/src/escrow_coordinator/mod.rs index 8a7f44a..772e6d3 100644 --- a/coordinator/src/escrow_coordinator/mod.rs +++ b/coordinator/src/escrow_coordinator/mod.rs @@ -1,4 +1,5 @@ use super::*; +use anyhow::anyhow; use cashu_escrow_common::model::TradeContract; use cdk::nuts::SecretKey as CDKSecretKey; use hashes::hex::DisplayHex; @@ -66,6 +67,10 @@ impl EscrowCoordinator { } } } + } else if RelayPoolNotification::Shutdown == notification { + break Err(anyhow!( + "Got shutdown notification, breaking coordinator lopp!" + )); } } } From 645158c9fb2ba02325886f9061d8e883bb3b7277 Mon Sep 17 00:00:00 2001 From: rodant Date: Wed, 21 Aug 2024 13:48:09 +0200 Subject: [PATCH 32/43] Bumb up nostr_sdk version. --- Cargo.lock | 133 ++++++++++------------------------------- client/Cargo.toml | 2 +- common/Cargo.toml | 2 +- coordinator/Cargo.toml | 2 +- 4 files changed, 35 insertions(+), 104 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c1efb12..6480c7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,31 +160,22 @@ dependencies = [ [[package]] name = "async-wsocket" -version = "0.5.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3445f8f330db8e5f3be7912f170f32e43fec90d995c71ced1ec3b8394b4a873c" +checksum = "5725a0615e4eb98e82e9cb963529398114e3fccfbf0e8b9111d605e2ac443e46" dependencies = [ "async-utility", + "futures", "futures-util", + "js-sys", "thiserror", "tokio", "tokio-rustls", "tokio-socks", "tokio-tungstenite", "url", - "wasm-ws", - "webpki-roots", -] - -[[package]] -name = "async_io_stream" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" -dependencies = [ - "futures", - "pharos", - "rustc_version", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -663,7 +654,6 @@ checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", - "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -686,34 +676,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" -[[package]] -name = "futures-executor" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" -[[package]] -name = "futures-macro" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "futures-sink" version = "0.3.30" @@ -735,7 +703,6 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", - "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1143,9 +1110,9 @@ checksum = "e664971378a3987224f7a0e10059782035e89899ae403718ee07de85bec42afe" [[package]] name = "nostr" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f08db214560a34bf7c4c1fea09a8461b9412bae58ba06e99ce3177d89fa1e0a6" +checksum = "5897e4142fcc33c4f1d58ad17f665e87dcba70de7e370c0bda1aa0fb73212c2a" dependencies = [ "aes", "base64 0.21.7", @@ -1173,9 +1140,9 @@ dependencies = [ [[package]] name = "nostr-database" -version = "0.33.1" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50eebf5020d70891e3c229128de5fc73af632b651d02742383b314d3d0c7e953" +checksum = "1926ef55392f3eea1bbe4a1358b64bbf12dd6eb554f40f483941a102c6263fc6" dependencies = [ "async-trait", "lru", @@ -1187,9 +1154,9 @@ dependencies = [ [[package]] name = "nostr-relay-pool" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afa5502a3df456790ca16d90cc688a677117d57ab56b079dcfa091390ac9f202" +checksum = "c6480cf60564957a2a64bd050d047ee0717e08dced7a389e22ef4e9fc104edd2" dependencies = [ "async-utility", "async-wsocket", @@ -1198,14 +1165,15 @@ dependencies = [ "nostr-database", "thiserror", "tokio", + "tokio-stream", "tracing", ] [[package]] name = "nostr-sdk" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b427dceefbbb49a9dd98abb8c4e40d25fdd467e99821aaad88615252bdb915bd" +checksum = "ca0c0c5f8ddbdfc064ea71883191ec53de6ed52b5dca10ab07f0810b99e91acc" dependencies = [ "async-utility", "atomic-destructor", @@ -1223,9 +1191,9 @@ dependencies = [ [[package]] name = "nostr-signer" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "665268b316f41cd8fa791be54b6c7935c5a239461708c380a699d6677be9af38" +checksum = "5c30294a7be7d9d5ac777954812f5c7b4ae2a1e583a62e33537f87d98ab23729" dependencies = [ "async-utility", "nostr", @@ -1237,9 +1205,9 @@ dependencies = [ [[package]] name = "nostr-zapper" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69922e74f8eab1f9d287008c0c06acdec87277a2d8f44bd9d38e003422aea0ab" +checksum = "dcf3ba30e807145e9cb924faf8fb0719e460f613088e99c753b67c2a9929c5b7" dependencies = [ "async-trait", "nostr", @@ -1273,9 +1241,9 @@ dependencies = [ [[package]] name = "nwc" -version = "0.33.0" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2e04b3edb5e9572e95b62842430625f1718e8a4a3596a30aeb04e6734764ea" +checksum = "9a16ac06bc273fcd4ead47c0c5a58b6cc7db2247fc7a64dd9bc11cf18e3efeb4" dependencies = [ "async-utility", "nostr", @@ -1333,16 +1301,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pharos" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" -dependencies = [ - "futures", - "rustc_version", -] - [[package]] name = "pin-project" version = "1.1.5" @@ -1592,15 +1550,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - [[package]] name = "rustls" version = "0.23.11" @@ -1711,18 +1660,6 @@ dependencies = [ "cc", ] -[[package]] -name = "semver" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" - -[[package]] -name = "send_wrapper" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" - [[package]] name = "serde" version = "1.0.204" @@ -1996,6 +1933,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-tungstenite" version = "0.23.1" @@ -2260,23 +2208,6 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" -[[package]] -name = "wasm-ws" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688c5806d1b06b4f3d90d015e23364dc5d3af412ee64abba6dde8fdc01637e33" -dependencies = [ - "async_io_stream", - "futures", - "js-sys", - "pharos", - "send_wrapper", - "thiserror", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "web-sys" version = "0.3.69" diff --git a/client/Cargo.toml b/client/Cargo.toml index 2f46d12..eb05e38 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -nostr-sdk = { version = "0.33.0", features = [] } +nostr-sdk = { version = "0.34.0", features = [] } cdk = "0.1.1" dotenv = "0.15.0" anyhow = "1.0.86" diff --git a/common/Cargo.toml b/common/Cargo.toml index abfa5f4..d075761 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -nostr-sdk = { version = "0.33.0", features = [] } +nostr-sdk = { version = "0.34.0", features = [] } cdk = "0.1.1" anyhow = "1.0.86" tokio = "1.38.0" diff --git a/coordinator/Cargo.toml b/coordinator/Cargo.toml index e1c31e3..0b8448e 100644 --- a/coordinator/Cargo.toml +++ b/coordinator/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -nostr-sdk = { version = "0.33.0", features = [] } +nostr-sdk = { version = "0.34.0", features = [] } cdk = "0.1.1" dotenv = "0.15.0" anyhow = "1.0.86" From 5e200367f249584cdc4b43a32a7cdcf41206b80e Mon Sep 17 00:00:00 2001 From: rodant Date: Thu, 22 Aug 2024 13:06:03 +0200 Subject: [PATCH 33/43] Handle some error cases. --- Cargo.toml | 7 ++- coordinator/Cargo.toml | 4 -- coordinator/src/escrow_coordinator/mod.rs | 67 ++++++++++++++--------- 3 files changed, 46 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 99dd7e3..b042908 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,9 @@ members = [ "client", "coordinator", "common", -] \ No newline at end of file +] + +[profile.release] +lto = true +opt-level = 3 +strip = true \ No newline at end of file diff --git a/coordinator/Cargo.toml b/coordinator/Cargo.toml index 0b8448e..807a47a 100644 --- a/coordinator/Cargo.toml +++ b/coordinator/Cargo.toml @@ -18,7 +18,3 @@ sha2 = "0.10.8" cashu_escrow_common = { path = "../common" } -[profile.release] -lto = true -opt-level = 3 -strip = true diff --git a/coordinator/src/escrow_coordinator/mod.rs b/coordinator/src/escrow_coordinator/mod.rs index 772e6d3..f0d7add 100644 --- a/coordinator/src/escrow_coordinator/mod.rs +++ b/coordinator/src/escrow_coordinator/mod.rs @@ -8,6 +8,7 @@ use ndk::{Filter, Kind, RelayPoolNotification}; use nostr_sdk as ndk; use sha2::{Digest, Sha256}; use std::collections::HashMap; +use tokio::sync::broadcast::error::RecvError; pub struct EscrowCoordinator { nostr_client: NostrClient, @@ -43,34 +44,50 @@ impl EscrowCoordinator { let mut notifications = self.nostr_client.client.notifications(); loop { - if let Ok(notification) = notifications.recv().await { - if let RelayPoolNotification::Event { event, .. } = notification { - if let Ok(unwrapped_gift) = - self.nostr_client.client.unwrap_gift_wrap(&event).await - { - let rumor = unwrapped_gift.rumor; - if rumor.kind == Kind::PrivateDirectMessage { - if let Ok((contract_hash, contract)) = parse_contract(&rumor.content) { - dbg!("Received contract: {}", &contract.trade_description); - if self.pending_contracts.contains_key(&contract_hash) { - self.pending_contracts.remove(&contract_hash); - let _ = self - .begin_trade(&contract_hash, &contract) - .await - .inspect_err(|e| { - //todo: use logger instead - println!("Got error while beginning a trade: {}", e); - }); - } else { - self.pending_contracts.insert(contract_hash, contract); + match notifications.recv().await { + Ok(notification) => { + if let RelayPoolNotification::Event { event, .. } = notification { + if let Ok(unwrapped_gift) = + self.nostr_client.client.unwrap_gift_wrap(&event).await + { + let rumor = unwrapped_gift.rumor; + if rumor.kind == Kind::PrivateDirectMessage { + if let Ok((contract_hash, contract)) = + parse_contract(&rumor.content) + { + dbg!("Received contract: {}", &contract.trade_description); + if self.pending_contracts.contains_key(&contract_hash) { + self.pending_contracts.remove(&contract_hash); + let _ = self + .begin_trade(&contract_hash, &contract) + .await + .inspect_err(|e| { + //todo: use logger instead + println!( + "Got error while beginning a trade: {}", + e + ); + }); + } else { + self.pending_contracts.insert(contract_hash, contract); + } } } } + } else if RelayPoolNotification::Shutdown == notification { + break Err(anyhow!( + "Got shutdown notification, breaking coordinator lopp!" + )); } - } else if RelayPoolNotification::Shutdown == notification { + } + Err(RecvError::Closed) => { break Err(anyhow!( - "Got shutdown notification, breaking coordinator lopp!" - )); + "Got closed error from channel, breaking coordinator lopp!" + )) + } + Err(RecvError::Lagged(count)) => { + //todo: use logger instead + println!("Lost {} events, resuming after that...", count); } } } @@ -102,10 +119,6 @@ impl EscrowCoordinator { .await?; Ok(()) } - - // pub async fn subscribe(&self) -> anyhow::Result<()> { - // Ok(()) - // } } fn parse_contract(content: &str) -> anyhow::Result<([u8; 32], TradeContract)> { From 6a0e0614cd68ad87c0270b2b98107986ecb0bc8e Mon Sep 17 00:00:00 2001 From: rodant Date: Thu, 22 Aug 2024 16:54:07 +0200 Subject: [PATCH 34/43] Avoid unneeded async func. --- coordinator/src/escrow_coordinator/mod.rs | 24 +++++++++++------------ coordinator/src/main.rs | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/coordinator/src/escrow_coordinator/mod.rs b/coordinator/src/escrow_coordinator/mod.rs index f0d7add..9909e91 100644 --- a/coordinator/src/escrow_coordinator/mod.rs +++ b/coordinator/src/escrow_coordinator/mod.rs @@ -22,7 +22,7 @@ struct ActiveTade { } impl EscrowCoordinator { - pub async fn setup(nostr_client: NostrClient) -> anyhow::Result { + pub fn new(nostr_client: NostrClient) -> anyhow::Result { Ok(Self { nostr_client, pending_contracts: HashMap::new(), @@ -53,7 +53,7 @@ impl EscrowCoordinator { let rumor = unwrapped_gift.rumor; if rumor.kind == Kind::PrivateDirectMessage { if let Ok((contract_hash, contract)) = - parse_contract(&rumor.content) + EscrowCoordinator::parse_contract(&rumor.content) { dbg!("Received contract: {}", &contract.trade_description); if self.pending_contracts.contains_key(&contract_hash) { @@ -119,16 +119,16 @@ impl EscrowCoordinator { .await?; Ok(()) } -} -fn parse_contract(content: &str) -> anyhow::Result<([u8; 32], TradeContract)> { - let contract: TradeContract = serde_json::from_str(content)?; + fn parse_contract(content: &str) -> anyhow::Result<([u8; 32], TradeContract)> { + let contract: TradeContract = serde_json::from_str(content)?; - // create a Sha256 object - let mut hasher = Sha256::new(); - // write input message - hasher.update(content.as_bytes()); - // read hash digest and consume hasher - let trade_hash: [u8; 32] = hasher.finalize().into(); - Ok((trade_hash, contract)) + // create a Sha256 object + let mut hasher = Sha256::new(); + // write input message + hasher.update(content.as_bytes()); + // read hash digest and consume hasher + let trade_hash: [u8; 32] = hasher.finalize().into(); + Ok((trade_hash, contract)) + } } diff --git a/coordinator/src/main.rs b/coordinator/src/main.rs index f435fe7..d2569d4 100644 --- a/coordinator/src/main.rs +++ b/coordinator/src/main.rs @@ -17,5 +17,5 @@ async fn main() -> anyhow::Result<()> { nostr_client.public_key().to_bech32()? ); println!("Starting service and waiting for trades..."); - return EscrowCoordinator::setup(nostr_client).await?.run().await; + return EscrowCoordinator::new(nostr_client)?.run().await; } From 5aaa6566aab88f1b5302847e0445f53d3f981638 Mon Sep 17 00:00:00 2001 From: rodant Date: Thu, 22 Aug 2024 18:54:39 +0200 Subject: [PATCH 35/43] Remove nostr instance and clean up escrow client. --- client/src/escrow_client/mod.rs | 41 +++++++++-------- client/src/main.rs | 12 ++--- client/src/nostr/mod.rs | 79 --------------------------------- common/src/nostr/mod.rs | 32 +------------ 4 files changed, 24 insertions(+), 140 deletions(-) delete mode 100644 client/src/nostr/mod.rs diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index 4b68c41..21a80b3 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -10,7 +10,7 @@ pub enum TradeMode { } pub struct EscrowClient { - nostr_instance: ClientNostrInstance, // can either be a Nostr Client or Nostr note signer (without networking) + nostr_client: NostrClient, // can either be a Nostr Client or Nostr note signer (without networking) ecash_wallet: ClientEcashWallet, escrow_registration: Option, escrow_contract: TradeContract, @@ -21,13 +21,13 @@ pub struct EscrowClient { impl EscrowClient { // creates the inital state: the coordinator data isn't present. pub fn new( - nostr_instance: ClientNostrInstance, + nostr_client: NostrClient, ecash_wallet: ClientEcashWallet, escrow_contract: TradeContract, trade_mode: TradeMode, ) -> Self { Self { - nostr_instance, + nostr_client, ecash_wallet, escrow_registration: None, escrow_contract, @@ -42,19 +42,19 @@ impl EscrowClient { /// After this state the trade contract is effectfull as well, possible coordinator fees must be payed. pub async fn register_trade(&mut self) -> anyhow::Result<()> { let coordinator_pk = &self.escrow_contract.npubkey_coordinator; - - // submits the trade contract to the coordinator to initiate the escrow service - self.nostr_instance - .submit_escrow_contract(&self.escrow_contract, coordinator_pk) + let message = serde_json::to_string(&self.escrow_contract)?; + dbg!("sending contract to coordinator..."); + self.nostr_client + .client + .send_private_msg(*coordinator_pk, &message, None) .await?; - let my_pubkey = self.nostr_instance.public_key(); - let escrow_registration = self - .nostr_instance - .receive_registration_message(my_pubkey) + let my_pubkey = self.nostr_client.public_key(); + let message = self + .nostr_client + .receive_escrow_message(my_pubkey, 10) .await?; - - self.escrow_registration = Some(escrow_registration); + self.escrow_registration = Some(serde_json::from_str(&message)?); Ok(()) } @@ -93,8 +93,9 @@ impl EscrowClient { debug!("Sending token to the seller: {}", escrow_token.as_str()); - self.nostr_instance - .submit_trade_token_to_seller(escrow_contract.npubkey_seller, &escrow_token) + self.nostr_client + .client + .send_private_msg(escrow_contract.npubkey_seller, &escrow_token, None) .await?; Ok(escrow_token) @@ -111,13 +112,11 @@ impl EscrowClient { .ok_or(anyhow!("Escrow registration not set, wrong state"))?; let wallet = &self.ecash_wallet; - let escrow_token = self - .nostr_instance - // todo: split method in receive and validate steps, single responsability principle. - .await_and_validate_escrow_token(wallet, escrow_contract, client_registration) + let message = self + .nostr_client + .receive_escrow_message(escrow_contract.npubkey_buyer, 10) .await?; - - Ok(escrow_token) + wallet.validate_escrow_token(&message, escrow_contract, client_registration) } /// Depending on the trade mode deliver product/service or sign the token after receiving the service. diff --git a/client/src/main.rs b/client/src/main.rs index dd4c52b..343e04b 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,7 +1,6 @@ mod cli; mod ecash; mod escrow_client; -mod nostr; use std::env; @@ -15,7 +14,6 @@ use dotenv::dotenv; use ecash::ClientEcashWallet; use escrow_client::*; use log::{debug, info}; -use nostr::ClientNostrInstance; use nostr_sdk::prelude::*; #[tokio::main] @@ -34,13 +32,9 @@ async fn main() -> anyhow::Result<()> { let cli_input = ClientCliInput::parse().await?; let escrow_contract = TradeContract::from_client_cli_input(&cli_input, escrow_wallet.trade_pubkey.clone())?; - let nostr_instance = ClientNostrInstance::new(cli_input.trader_nostr_keys).await?; - let mut escrow_client = EscrowClient::new( - nostr_instance, - escrow_wallet, - escrow_contract, - cli_input.mode, - ); + let nostr_client = NostrClient::new(cli_input.trader_nostr_keys).await?; + let mut escrow_client = + EscrowClient::new(nostr_client, escrow_wallet, escrow_contract, cli_input.mode); escrow_client.register_trade().await?; debug!("Common trade registration completed"); diff --git a/client/src/nostr/mod.rs b/client/src/nostr/mod.rs deleted file mode 100644 index 0c397de..0000000 --- a/client/src/nostr/mod.rs +++ /dev/null @@ -1,79 +0,0 @@ -use anyhow::Result; -use cashu_escrow_common::model::EscrowRegistration; -use cdk::nuts::Token; -use nostr_sdk::PublicKey; - -use super::*; - -// here we can somehow make NostrInstance generic to be either a full Nostr Client or only a Nostr Signer depending on -// compilation flags or env variables? - -// should we keep the sending functions in the common crate? -// As they are only used by the client. - -pub struct ClientNostrInstance { - nostr_client: NostrClient, -} - -impl ClientNostrInstance { - pub async fn new(trader_keys: Keys) -> Result { - let nostr_client = NostrClient::new(trader_keys).await?; - Ok(Self { nostr_client }) - } - - // here submit_escrow_contract calls send_escrow_contract to submit the contract via nostr - // maybe this could be some kind of wasm callable function to just return a - // signed event depending on the setup - pub async fn submit_escrow_contract( - &self, - contract: &TradeContract, - coordinator_pk: &PublicKey, - ) -> Result<()> { - let coordinator_pk_bech32 = coordinator_pk.to_bech32()?; - self.nostr_client - .send_trade_contract(contract, &coordinator_pk_bech32) - .await?; - Ok(()) - } - - // await the answer to the submitted contract, once the coordinator returns the ecash public key - // the escrow service is confirmed by the coordinator - pub async fn receive_registration_message( - &self, - receiver_pk: PublicKey, - ) -> Result { - let message = self - .nostr_client - .receive_escrow_message(receiver_pk, 10) - .await?; - Ok(serde_json::from_str(&message)?) - } - - pub async fn submit_trade_token_to_seller( - &self, - seller_npubkey: PublicKey, - token: &str, - ) -> Result<()> { - self.nostr_client - .send_trade_token_to_seller(seller_npubkey, token) - .await?; - Ok(()) - } - - pub async fn await_and_validate_escrow_token( - &self, - wallet: &ClientEcashWallet, - contract: &TradeContract, - registration: &EscrowRegistration, - ) -> Result { - let message = self - .nostr_client - .receive_escrow_message(contract.npubkey_buyer, 10) - .await?; - wallet.validate_escrow_token(&message, contract, registration) - } - - pub(crate) fn public_key(&self) -> PublicKey { - self.nostr_client.public_key() - } -} diff --git a/common/src/nostr/mod.rs b/common/src/nostr/mod.rs index 0365ab9..7521eaa 100644 --- a/common/src/nostr/mod.rs +++ b/common/src/nostr/mod.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use crate::model::{EscrowRegistration, TradeContract}; +use crate::model::EscrowRegistration; use anyhow::anyhow; use nostr_sdk::prelude::*; use tokio::time::timeout; @@ -88,34 +88,4 @@ impl NostrClient { .await?; Ok(()) } - - // client specific function? - pub async fn send_trade_contract( - &self, - contract: &TradeContract, - coordinator_pk_bech32: &str, - ) -> anyhow::Result<()> { - let message = serde_json::to_string(contract)?; - dbg!("sending contract to coordinator..."); - self.client - .send_private_msg( - PublicKey::from_bech32(coordinator_pk_bech32)?, - &message, - None, - ) - .await?; - Ok(()) - } - - // client specific function? - pub async fn send_trade_token_to_seller( - &self, - seller_npubkey: PublicKey, - token: &str, - ) -> anyhow::Result<()> { - self.client - .send_private_msg(seller_npubkey, token, None) - .await?; - Ok(()) - } } From 3bcfde413584eba8201329fdf199921afe664f4e Mon Sep 17 00:00:00 2001 From: rodant Date: Mon, 26 Aug 2024 17:24:25 +0200 Subject: [PATCH 36/43] Fix race condition in receiving registration and introduce escrow client states. --- client/src/escrow_client/mod.rs | 113 ++++++++++++++++++-------------- client/src/main.rs | 20 +++--- 2 files changed, 75 insertions(+), 58 deletions(-) diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index 21a80b3..2d1cba7 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use crate::common::model::EscrowRegistration; use cdk::nuts::Token; @@ -9,27 +11,21 @@ pub enum TradeMode { Seller, } -pub struct EscrowClient { - nostr_client: NostrClient, // can either be a Nostr Client or Nostr note signer (without networking) +pub struct InitEscrowClient { ecash_wallet: ClientEcashWallet, - escrow_registration: Option, escrow_contract: TradeContract, trade_mode: TradeMode, } -// todo: model EscrowClient as an state machine (stm). This will improve testability too. -impl EscrowClient { - // creates the inital state: the coordinator data isn't present. +/// Initial Escrow Client state. +impl InitEscrowClient { pub fn new( - nostr_client: NostrClient, ecash_wallet: ClientEcashWallet, escrow_contract: TradeContract, trade_mode: TradeMode, ) -> Self { Self { - nostr_client, ecash_wallet, - escrow_registration: None, escrow_contract, trade_mode, } @@ -40,38 +36,57 @@ impl EscrowClient { /// After this the coordinator data is set, state trade registered. /// /// After this state the trade contract is effectfull as well, possible coordinator fees must be payed. - pub async fn register_trade(&mut self) -> anyhow::Result<()> { + pub async fn register_trade( + &self, + nostr_client: Arc, + ) -> anyhow::Result { + let my_pubkey = nostr_client.public_key(); + let nostr_client_ref = nostr_client.clone(); + let reg_msg_fut = + tokio::spawn( + async move { nostr_client_ref.receive_escrow_message(my_pubkey, 10).await }, + ); + let coordinator_pk = &self.escrow_contract.npubkey_coordinator; - let message = serde_json::to_string(&self.escrow_contract)?; + let contract_message = serde_json::to_string(&self.escrow_contract)?; dbg!("sending contract to coordinator..."); - self.nostr_client + nostr_client .client - .send_private_msg(*coordinator_pk, &message, None) + .send_private_msg(*coordinator_pk, &contract_message, None) .await?; - let my_pubkey = self.nostr_client.public_key(); - let message = self - .nostr_client - .receive_escrow_message(my_pubkey, 10) - .await?; - self.escrow_registration = Some(serde_json::from_str(&message)?); - Ok(()) + let registration_message = reg_msg_fut.await??; + let escrow_registration = serde_json::from_str(®istration_message)?; + Ok(RegisteredEscrowClient { + prev_state: self, + escrow_registration, + }) } +} +pub struct RegisteredEscrowClient<'a> { + prev_state: &'a InitEscrowClient, + escrow_registration: EscrowRegistration, +} + +impl<'a> RegisteredEscrowClient<'a> { /// Depending on the trade mode sends or receives the trade token. /// /// After this the state is token sent or received. - pub async fn exchange_trade_token(&self) -> std::result::Result<(), anyhow::Error> { - match self.trade_mode { + pub async fn exchange_trade_token( + &self, + nostr_client: &NostrClient, + ) -> anyhow::Result { + match self.prev_state.trade_mode { TradeMode::Buyer => { - // todo: store the sent token in this instance - self.send_trade_token().await?; - Ok(()) + // todo: store the sent token in next instance + self.send_trade_token(nostr_client).await?; + Ok(TokenExchangedEscrowClient { _prev_state: self }) } TradeMode::Seller => { - // todo: store the received token in this instance - self.receive_and_validate_trade_token().await?; - Ok(()) + // todo: store the received token in next instance + self.receive_and_validate_trade_token(nostr_client).await?; + Ok(TokenExchangedEscrowClient { _prev_state: self }) } } } @@ -79,21 +94,17 @@ impl EscrowClient { /// State change for the buyer. The state after that is token sent. /// /// Returns the sent trade token by this [`EscrowClient`]. - async fn send_trade_token(&self) -> anyhow::Result { - let escrow_contract = &self.escrow_contract; - let escrow_registration = self - .escrow_registration - .as_ref() - .ok_or(anyhow!("Escrow registration not set, wrong state"))?; - + async fn send_trade_token(&self, nostr_client: &NostrClient) -> anyhow::Result { + let escrow_contract = &self.prev_state.escrow_contract; let escrow_token = self + .prev_state .ecash_wallet - .create_escrow_token(escrow_contract, escrow_registration) + .create_escrow_token(escrow_contract, &self.escrow_registration) .await?; - debug!("Sending token to the seller: {}", escrow_token.as_str()); + debug!("Sending token to the seller: {}", escrow_token); - self.nostr_client + nostr_client .client .send_private_msg(escrow_contract.npubkey_seller, &escrow_token, None) .await?; @@ -104,21 +115,25 @@ impl EscrowClient { /// State change for a seller. The state after this is token received. /// /// Returns the received trade token by this [`EscrowClient`]. - async fn receive_and_validate_trade_token(&self) -> anyhow::Result { - let escrow_contract = &self.escrow_contract; - let client_registration = self - .escrow_registration - .as_ref() - .ok_or(anyhow!("Escrow registration not set, wrong state"))?; - let wallet = &self.ecash_wallet; - - let message = self - .nostr_client + async fn receive_and_validate_trade_token( + &self, + nostr_client: &NostrClient, + ) -> anyhow::Result { + let escrow_contract = &self.prev_state.escrow_contract; + let wallet = &self.prev_state.ecash_wallet; + + let message = nostr_client .receive_escrow_message(escrow_contract.npubkey_buyer, 10) .await?; - wallet.validate_escrow_token(&message, escrow_contract, client_registration) + wallet.validate_escrow_token(&message, escrow_contract, &self.escrow_registration) } +} + +pub struct TokenExchangedEscrowClient<'a> { + _prev_state: &'a RegisteredEscrowClient<'a>, +} +impl<'a> TokenExchangedEscrowClient<'a> { /// Depending on the trade mode deliver product/service or sign the token after receiving the service. /// /// The state after this operation is duties fulfilled. diff --git a/client/src/main.rs b/client/src/main.rs index 343e04b..795eddc 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -3,8 +3,10 @@ mod ecash; mod escrow_client; use std::env; +use std::sync::Arc; use anyhow::anyhow; +use async_utility::futures_util::{FutureExt, TryFutureExt}; use cashu_escrow_common as common; use cli::trade_contract::FromClientCliInput; use cli::ClientCliInput; @@ -33,13 +35,13 @@ async fn main() -> anyhow::Result<()> { let escrow_contract = TradeContract::from_client_cli_input(&cli_input, escrow_wallet.trade_pubkey.clone())?; let nostr_client = NostrClient::new(cli_input.trader_nostr_keys).await?; - let mut escrow_client = - EscrowClient::new(nostr_client, escrow_wallet, escrow_contract, cli_input.mode); - - escrow_client.register_trade().await?; - debug!("Common trade registration completed"); - - escrow_client.exchange_trade_token().await?; - - escrow_client.do_your_trade_duties().await + let nostr_client_arc = Arc::new(nostr_client); + InitEscrowClient::new(escrow_wallet, escrow_contract, cli_input.mode) + .register_trade(nostr_client_arc.clone()) + .await? + .exchange_trade_token(&nostr_client_arc) + .await? + .do_your_trade_duties() + .await?; + Ok(()) } From d038b127de9e2ecb06bb759b4f3556a8b414c08d Mon Sep 17 00:00:00 2001 From: rodant Date: Mon, 26 Aug 2024 17:27:19 +0200 Subject: [PATCH 37/43] remove unused imports. --- client/src/main.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/src/main.rs b/client/src/main.rs index 795eddc..87ff81a 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -5,8 +5,6 @@ mod escrow_client; use std::env; use std::sync::Arc; -use anyhow::anyhow; -use async_utility::futures_util::{FutureExt, TryFutureExt}; use cashu_escrow_common as common; use cli::trade_contract::FromClientCliInput; use cli::ClientCliInput; From 8d79e1c9b7d07f9661fd42c0da4e1363d2516eef Mon Sep 17 00:00:00 2001 From: rodant Date: Fri, 30 Aug 2024 17:16:59 +0200 Subject: [PATCH 38/43] Deposit enough funds and proceed to a delivery. --- client/src/escrow_client/mod.rs | 21 +++++++++++++-------- client/src/main.rs | 13 +++++++++++-- common/src/nostr/mod.rs | 8 ++------ 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index 2d1cba7..c3f4058 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -5,7 +5,7 @@ use cdk::nuts::Token; use super::*; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum TradeMode { Buyer, Seller, @@ -40,12 +40,9 @@ impl InitEscrowClient { &self, nostr_client: Arc, ) -> anyhow::Result { - let my_pubkey = nostr_client.public_key(); let nostr_client_ref = nostr_client.clone(); let reg_msg_fut = - tokio::spawn( - async move { nostr_client_ref.receive_escrow_message(my_pubkey, 10).await }, - ); + tokio::spawn(async move { nostr_client_ref.receive_escrow_message(20).await }); let coordinator_pk = &self.escrow_contract.npubkey_coordinator; let contract_message = serde_json::to_string(&self.escrow_contract)?; @@ -108,6 +105,7 @@ impl<'a> RegisteredEscrowClient<'a> { .client .send_private_msg(escrow_contract.npubkey_seller, &escrow_token, None) .await?; + dbg!("Sent Token to seller"); Ok(escrow_token) } @@ -122,9 +120,8 @@ impl<'a> RegisteredEscrowClient<'a> { let escrow_contract = &self.prev_state.escrow_contract; let wallet = &self.prev_state.ecash_wallet; - let message = nostr_client - .receive_escrow_message(escrow_contract.npubkey_buyer, 10) - .await?; + let message = nostr_client.receive_escrow_message(20).await?; + dbg!("Received Token, vaidating it..."); wallet.validate_escrow_token(&message, escrow_contract, &self.escrow_registration) } } @@ -142,6 +139,14 @@ impl<'a> TokenExchangedEscrowClient<'a> { // await signature or begin dispute // todo: as buyer either send signature or begin dispute + match self._prev_state.prev_state.trade_mode { + TradeMode::Buyer => { + dbg!("Payed invoince and waiting for delivery..."); + } + TradeMode::Seller => { + dbg!("Got payment and proceding with delivery..."); + } + } Ok(()) } } diff --git a/client/src/main.rs b/client/src/main.rs index 87ff81a..84c78cd 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -6,6 +6,7 @@ use std::env; use std::sync::Arc; use cashu_escrow_common as common; +use cdk::amount::{Amount, SplitTarget}; use cli::trade_contract::FromClientCliInput; use cli::ClientCliInput; use common::model::TradeContract; @@ -27,9 +28,17 @@ async fn main() -> anyhow::Result<()> { let mint_url = env::var("MINT_URL")?; let escrow_wallet = ClientEcashWallet::new(&mint_url).await?; - //todo: Ensure to have enough funds in the wallet. The buyer must probably transfer some ecash to the escrow wallet. - let cli_input = ClientCliInput::parse().await?; + + //Ensure to have enough funds in the wallet. + if cli_input.mode == TradeMode::Buyer { + let mint_quote = escrow_wallet.wallet.mint_quote(Amount::from(5000)).await?; + escrow_wallet + .wallet + .mint(&mint_quote.id, SplitTarget::None, None) + .await?; + } + let escrow_contract = TradeContract::from_client_cli_input(&cli_input, escrow_wallet.trade_pubkey.clone())?; let nostr_client = NostrClient::new(cli_input.trader_nostr_keys).await?; diff --git a/common/src/nostr/mod.rs b/common/src/nostr/mod.rs index 7521eaa..081e17f 100644 --- a/common/src/nostr/mod.rs +++ b/common/src/nostr/mod.rs @@ -32,14 +32,10 @@ impl NostrClient { self.keys.public_key() } - pub async fn receive_escrow_message( - &self, - receiver_pubkey: PublicKey, - timeout_secs: u64, - ) -> anyhow::Result { + pub async fn receive_escrow_message(&self, timeout_secs: u64) -> anyhow::Result { let message_filter = Filter::new() .kind(Kind::GiftWrap) - .pubkey(receiver_pubkey) + .pubkey(self.keys.public_key()) .limit(0); let subscription_id = self.client.subscribe(vec![message_filter], None).await?.val; From 09396a7b7913c676708c1e164fcaf4cb71895c0d Mon Sep 17 00:00:00 2001 From: rodant Date: Sun, 1 Sep 2024 11:51:25 +0200 Subject: [PATCH 39/43] Fix typos. --- coordinator/src/escrow_coordinator/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coordinator/src/escrow_coordinator/mod.rs b/coordinator/src/escrow_coordinator/mod.rs index 9909e91..e9474b1 100644 --- a/coordinator/src/escrow_coordinator/mod.rs +++ b/coordinator/src/escrow_coordinator/mod.rs @@ -76,13 +76,13 @@ impl EscrowCoordinator { } } else if RelayPoolNotification::Shutdown == notification { break Err(anyhow!( - "Got shutdown notification, breaking coordinator lopp!" + "Got shutdown notification, breaking coordinator loop!" )); } } Err(RecvError::Closed) => { break Err(anyhow!( - "Got closed error from channel, breaking coordinator lopp!" + "Got closed error from channel, breaking coordinator loop!" )) } Err(RecvError::Lagged(count)) => { From f4b75cbc6c9c66bc6394d5e49da6b8a4bc02df54 Mon Sep 17 00:00:00 2001 From: rodant Date: Sun, 1 Sep 2024 11:55:39 +0200 Subject: [PATCH 40/43] Update coordinator/src/escrow_coordinator/mod.rs Co-authored-by: Felix <51097237+f321x@users.noreply.github.com> --- coordinator/src/escrow_coordinator/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coordinator/src/escrow_coordinator/mod.rs b/coordinator/src/escrow_coordinator/mod.rs index e9474b1..7ad0364 100644 --- a/coordinator/src/escrow_coordinator/mod.rs +++ b/coordinator/src/escrow_coordinator/mod.rs @@ -87,7 +87,7 @@ impl EscrowCoordinator { } Err(RecvError::Lagged(count)) => { //todo: use logger instead - println!("Lost {} events, resuming after that...", count); + eprintln!("Lost {} events, resuming after that...", count); } } } From e2e43825502ddd93dd2c1de3fa49f96674510820 Mon Sep 17 00:00:00 2001 From: rodant Date: Mon, 2 Sep 2024 13:05:26 +0200 Subject: [PATCH 41/43] Initialize the notifications receiver at creation and pull events on demand. We don't need to spawn tasks anymore. --- client/src/escrow_client/mod.rs | 20 +++++++++----------- client/src/main.rs | 8 +++----- common/src/nostr/mod.rs | 33 ++++++++++++++++++++------------- 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index c3f4058..ac5b942 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use crate::common::model::EscrowRegistration; use cdk::nuts::Token; @@ -38,12 +36,8 @@ impl InitEscrowClient { /// After this state the trade contract is effectfull as well, possible coordinator fees must be payed. pub async fn register_trade( &self, - nostr_client: Arc, + nostr_client: &mut NostrClient, ) -> anyhow::Result { - let nostr_client_ref = nostr_client.clone(); - let reg_msg_fut = - tokio::spawn(async move { nostr_client_ref.receive_escrow_message(20).await }); - let coordinator_pk = &self.escrow_contract.npubkey_coordinator; let contract_message = serde_json::to_string(&self.escrow_contract)?; dbg!("sending contract to coordinator..."); @@ -52,8 +46,12 @@ impl InitEscrowClient { .send_private_msg(*coordinator_pk, &contract_message, None) .await?; - let registration_message = reg_msg_fut.await??; - let escrow_registration = serde_json::from_str(®istration_message)?; + let registration_message = nostr_client.receive_escrow_message(200).await?; + let escrow_registration: EscrowRegistration = serde_json::from_str(®istration_message)?; + dbg!( + "Received registration: {}", + &escrow_registration.escrow_id_hex + ); Ok(RegisteredEscrowClient { prev_state: self, escrow_registration, @@ -72,7 +70,7 @@ impl<'a> RegisteredEscrowClient<'a> { /// After this the state is token sent or received. pub async fn exchange_trade_token( &self, - nostr_client: &NostrClient, + nostr_client: &mut NostrClient, ) -> anyhow::Result { match self.prev_state.trade_mode { TradeMode::Buyer => { @@ -115,7 +113,7 @@ impl<'a> RegisteredEscrowClient<'a> { /// Returns the received trade token by this [`EscrowClient`]. async fn receive_and_validate_trade_token( &self, - nostr_client: &NostrClient, + nostr_client: &mut NostrClient, ) -> anyhow::Result { let escrow_contract = &self.prev_state.escrow_contract; let wallet = &self.prev_state.ecash_wallet; diff --git a/client/src/main.rs b/client/src/main.rs index 84c78cd..a780925 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -3,7 +3,6 @@ mod ecash; mod escrow_client; use std::env; -use std::sync::Arc; use cashu_escrow_common as common; use cdk::amount::{Amount, SplitTarget}; @@ -41,12 +40,11 @@ async fn main() -> anyhow::Result<()> { let escrow_contract = TradeContract::from_client_cli_input(&cli_input, escrow_wallet.trade_pubkey.clone())?; - let nostr_client = NostrClient::new(cli_input.trader_nostr_keys).await?; - let nostr_client_arc = Arc::new(nostr_client); + let mut nostr_client = NostrClient::new(cli_input.trader_nostr_keys).await?; InitEscrowClient::new(escrow_wallet, escrow_contract, cli_input.mode) - .register_trade(nostr_client_arc.clone()) + .register_trade(&mut nostr_client) .await? - .exchange_trade_token(&nostr_client_arc) + .exchange_trade_token(&mut nostr_client) .await? .do_your_trade_duties() .await?; diff --git a/common/src/nostr/mod.rs b/common/src/nostr/mod.rs index 081e17f..a0e9b82 100644 --- a/common/src/nostr/mod.rs +++ b/common/src/nostr/mod.rs @@ -3,11 +3,13 @@ use std::time::Duration; use crate::model::EscrowRegistration; use anyhow::anyhow; use nostr_sdk::prelude::*; -use tokio::time::timeout; +use tokio::{sync::broadcast::Receiver, time::timeout}; pub struct NostrClient { keys: Keys, pub client: Client, + _subscription_id: SubscriptionId, + notifications_receiver: Receiver, } impl NostrClient { @@ -25,26 +27,31 @@ impl NostrClient { // Connect to relays client.connect().await; - Ok(Self { keys, client }) - } - - pub fn public_key(&self) -> PublicKey { - self.keys.public_key() - } - pub async fn receive_escrow_message(&self, timeout_secs: u64) -> anyhow::Result { let message_filter = Filter::new() .kind(Kind::GiftWrap) - .pubkey(self.keys.public_key()) + .pubkey(keys.public_key()) .limit(0); - let subscription_id = self.client.subscribe(vec![message_filter], None).await?.val; + let _subscription_id = client.subscribe(vec![message_filter], None).await?.val; + let notifications_receiver = client.notifications(); - let mut notifications = self.client.notifications(); + Ok(Self { + keys, + client, + _subscription_id, + notifications_receiver, + }) + } + + pub fn public_key(&self) -> PublicKey { + self.keys.public_key() + } + pub async fn receive_escrow_message(&mut self, timeout_secs: u64) -> anyhow::Result { let loop_future = async { loop { - if let Ok(notification) = notifications.recv().await { + if let Ok(notification) = self.notifications_receiver.recv().await { if let RelayPoolNotification::Event { event, .. } = notification { let rumor = self.client.unwrap_gift_wrap(&event).await?.rumor; if rumor.kind == Kind::PrivateDirectMessage { @@ -52,13 +59,13 @@ impl NostrClient { } } } + //todo: in case of RecvErr::Close reset the subscription } }; let result = match timeout(Duration::from_secs(timeout_secs), loop_future).await { Ok(result) => result, Err(e) => Err(anyhow!("Timeout, {}", e)), }; - self.client.unsubscribe(subscription_id.clone()).await; result } From e1d295e5a5f03cc7bbcb6eb164b9946e42a259ae Mon Sep 17 00:00:00 2001 From: rodant Date: Mon, 2 Sep 2024 17:17:40 +0200 Subject: [PATCH 42/43] Define nostr client as field of the escrow client and pass all its fields to the next states. --- client/src/escrow_client/mod.rs | 82 +++++++++++++++++++-------------- client/src/main.rs | 9 ++-- 2 files changed, 52 insertions(+), 39 deletions(-) diff --git a/client/src/escrow_client/mod.rs b/client/src/escrow_client/mod.rs index ac5b942..1bf1d36 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -10,6 +10,7 @@ pub enum TradeMode { } pub struct InitEscrowClient { + nostr_client: NostrClient, ecash_wallet: ClientEcashWallet, escrow_contract: TradeContract, trade_mode: TradeMode, @@ -18,11 +19,13 @@ pub struct InitEscrowClient { /// Initial Escrow Client state. impl InitEscrowClient { pub fn new( + nostr_client: NostrClient, ecash_wallet: ClientEcashWallet, escrow_contract: TradeContract, trade_mode: TradeMode, ) -> Self { Self { + nostr_client, ecash_wallet, escrow_contract, trade_mode, @@ -34,54 +37,64 @@ impl InitEscrowClient { /// After this the coordinator data is set, state trade registered. /// /// After this state the trade contract is effectfull as well, possible coordinator fees must be payed. - pub async fn register_trade( - &self, - nostr_client: &mut NostrClient, - ) -> anyhow::Result { + pub async fn register_trade(mut self) -> anyhow::Result { let coordinator_pk = &self.escrow_contract.npubkey_coordinator; let contract_message = serde_json::to_string(&self.escrow_contract)?; dbg!("sending contract to coordinator..."); - nostr_client + self.nostr_client .client .send_private_msg(*coordinator_pk, &contract_message, None) .await?; - let registration_message = nostr_client.receive_escrow_message(200).await?; + let registration_message = self.nostr_client.receive_escrow_message(20).await?; let escrow_registration: EscrowRegistration = serde_json::from_str(®istration_message)?; dbg!( "Received registration: {}", &escrow_registration.escrow_id_hex ); Ok(RegisteredEscrowClient { - prev_state: self, + nostr_client: self.nostr_client, + ecash_wallet: self.ecash_wallet, + escrow_contract: self.escrow_contract, + trade_mode: self.trade_mode, escrow_registration, }) } } -pub struct RegisteredEscrowClient<'a> { - prev_state: &'a InitEscrowClient, +pub struct RegisteredEscrowClient { + nostr_client: NostrClient, + ecash_wallet: ClientEcashWallet, + escrow_contract: TradeContract, + trade_mode: TradeMode, escrow_registration: EscrowRegistration, } -impl<'a> RegisteredEscrowClient<'a> { +impl RegisteredEscrowClient { /// Depending on the trade mode sends or receives the trade token. /// /// After this the state is token sent or received. - pub async fn exchange_trade_token( - &self, - nostr_client: &mut NostrClient, - ) -> anyhow::Result { - match self.prev_state.trade_mode { + pub async fn exchange_trade_token(mut self) -> anyhow::Result { + match self.trade_mode { TradeMode::Buyer => { // todo: store the sent token in next instance - self.send_trade_token(nostr_client).await?; - Ok(TokenExchangedEscrowClient { _prev_state: self }) + self.send_trade_token().await?; + Ok(TokenExchangedEscrowClient { + _nostr_client: self.nostr_client, + _ecash_wallet: self.ecash_wallet, + _escrow_contract: self.escrow_contract, + trade_mode: self.trade_mode, + }) } TradeMode::Seller => { // todo: store the received token in next instance - self.receive_and_validate_trade_token(nostr_client).await?; - Ok(TokenExchangedEscrowClient { _prev_state: self }) + self.receive_and_validate_trade_token().await?; + Ok(TokenExchangedEscrowClient { + _nostr_client: self.nostr_client, + _ecash_wallet: self.ecash_wallet, + _escrow_contract: self.escrow_contract, + trade_mode: self.trade_mode, + }) } } } @@ -89,17 +102,16 @@ impl<'a> RegisteredEscrowClient<'a> { /// State change for the buyer. The state after that is token sent. /// /// Returns the sent trade token by this [`EscrowClient`]. - async fn send_trade_token(&self, nostr_client: &NostrClient) -> anyhow::Result { - let escrow_contract = &self.prev_state.escrow_contract; + async fn send_trade_token(&self) -> anyhow::Result { + let escrow_contract = &self.escrow_contract; let escrow_token = self - .prev_state .ecash_wallet .create_escrow_token(escrow_contract, &self.escrow_registration) .await?; debug!("Sending token to the seller: {}", escrow_token); - nostr_client + self.nostr_client .client .send_private_msg(escrow_contract.npubkey_seller, &escrow_token, None) .await?; @@ -111,24 +123,24 @@ impl<'a> RegisteredEscrowClient<'a> { /// State change for a seller. The state after this is token received. /// /// Returns the received trade token by this [`EscrowClient`]. - async fn receive_and_validate_trade_token( - &self, - nostr_client: &mut NostrClient, - ) -> anyhow::Result { - let escrow_contract = &self.prev_state.escrow_contract; - let wallet = &self.prev_state.ecash_wallet; - - let message = nostr_client.receive_escrow_message(20).await?; + async fn receive_and_validate_trade_token(&mut self) -> anyhow::Result { + let escrow_contract = &self.escrow_contract; + let wallet = &self.ecash_wallet; + + let message = self.nostr_client.receive_escrow_message(20).await?; dbg!("Received Token, vaidating it..."); wallet.validate_escrow_token(&message, escrow_contract, &self.escrow_registration) } } -pub struct TokenExchangedEscrowClient<'a> { - _prev_state: &'a RegisteredEscrowClient<'a>, +pub struct TokenExchangedEscrowClient { + _nostr_client: NostrClient, + _ecash_wallet: ClientEcashWallet, + _escrow_contract: TradeContract, + trade_mode: TradeMode, } -impl<'a> TokenExchangedEscrowClient<'a> { +impl TokenExchangedEscrowClient { /// Depending on the trade mode deliver product/service or sign the token after receiving the service. /// /// The state after this operation is duties fulfilled. @@ -137,7 +149,7 @@ impl<'a> TokenExchangedEscrowClient<'a> { // await signature or begin dispute // todo: as buyer either send signature or begin dispute - match self._prev_state.prev_state.trade_mode { + match self.trade_mode { TradeMode::Buyer => { dbg!("Payed invoince and waiting for delivery..."); } diff --git a/client/src/main.rs b/client/src/main.rs index a780925..fb77115 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -40,11 +40,12 @@ async fn main() -> anyhow::Result<()> { let escrow_contract = TradeContract::from_client_cli_input(&cli_input, escrow_wallet.trade_pubkey.clone())?; - let mut nostr_client = NostrClient::new(cli_input.trader_nostr_keys).await?; - InitEscrowClient::new(escrow_wallet, escrow_contract, cli_input.mode) - .register_trade(&mut nostr_client) + let nostr_client = NostrClient::new(cli_input.trader_nostr_keys).await?; + + InitEscrowClient::new(nostr_client, escrow_wallet, escrow_contract, cli_input.mode) + .register_trade() .await? - .exchange_trade_token(&mut nostr_client) + .exchange_trade_token() .await? .do_your_trade_duties() .await?; From 94d2424cdfca80f33571c441077839ca56bbc9c4 Mon Sep 17 00:00:00 2001 From: rodant Date: Tue, 3 Sep 2024 10:01:48 +0200 Subject: [PATCH 43/43] Handle receice errors from a subscription. --- common/src/nostr/mod.rs | 52 ++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/common/src/nostr/mod.rs b/common/src/nostr/mod.rs index a0e9b82..3571bc5 100644 --- a/common/src/nostr/mod.rs +++ b/common/src/nostr/mod.rs @@ -3,12 +3,15 @@ use std::time::Duration; use crate::model::EscrowRegistration; use anyhow::anyhow; use nostr_sdk::prelude::*; -use tokio::{sync::broadcast::Receiver, time::timeout}; +use tokio::{ + sync::broadcast::{error::RecvError, Receiver}, + time::timeout, +}; pub struct NostrClient { keys: Keys, pub client: Client, - _subscription_id: SubscriptionId, + subscription_id: SubscriptionId, notifications_receiver: Receiver, } @@ -28,18 +31,12 @@ impl NostrClient { // Connect to relays client.connect().await; - let message_filter = Filter::new() - .kind(Kind::GiftWrap) - .pubkey(keys.public_key()) - .limit(0); - - let _subscription_id = client.subscribe(vec![message_filter], None).await?.val; - let notifications_receiver = client.notifications(); + let (_subscription_id, notifications_receiver) = init_subscription(&keys, &client).await?; Ok(Self { keys, client, - _subscription_id, + subscription_id: _subscription_id, notifications_receiver, }) } @@ -51,15 +48,25 @@ impl NostrClient { pub async fn receive_escrow_message(&mut self, timeout_secs: u64) -> anyhow::Result { let loop_future = async { loop { - if let Ok(notification) = self.notifications_receiver.recv().await { - if let RelayPoolNotification::Event { event, .. } = notification { - let rumor = self.client.unwrap_gift_wrap(&event).await?.rumor; - if rumor.kind == Kind::PrivateDirectMessage { - break Ok(rumor.content) as anyhow::Result; + match self.notifications_receiver.recv().await { + Ok(notification) => { + if let RelayPoolNotification::Event { event, .. } = notification { + let rumor = self.client.unwrap_gift_wrap(&event).await?.rumor; + if rumor.kind == Kind::PrivateDirectMessage { + break Ok(rumor.content) as anyhow::Result; + } } } + Err(RecvError::Closed) => { + eprintln!("Relay pool closed subscription, restarting a new one..."); + self.client.unsubscribe(self.subscription_id.clone()).await; + (self.subscription_id, self.notifications_receiver) = + init_subscription(&self.keys, &self.client).await?; + } + Err(RecvError::Lagged(count)) => { + dbg!("Lost {} events, proceeding after that...", count); + } } - //todo: in case of RecvErr::Close reset the subscription } }; let result = match timeout(Duration::from_secs(timeout_secs), loop_future).await { @@ -92,3 +99,16 @@ impl NostrClient { Ok(()) } } + +async fn init_subscription( + keys: &Keys, + client: &Client, +) -> Result<(SubscriptionId, Receiver), anyhow::Error> { + let message_filter = Filter::new() + .kind(Kind::GiftWrap) + .pubkey(keys.public_key()) + .limit(0); + let _subscription_id = client.subscribe(vec![message_filter], None).await?.val; + let notifications_receiver = client.notifications(); + Ok((_subscription_id, notifications_receiver)) +}