diff --git a/Cargo.lock b/Cargo.lock index b6b8768..6480c7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,6 +50,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.18" @@ -71,6 +80,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "anyhow" version = "1.0.86" @@ -102,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]] @@ -315,6 +364,8 @@ dependencies = [ "cashu_escrow_common", "cdk", "dotenv", + "env_logger", + "log", "nostr-sdk", "rand", "serde", @@ -445,6 +496,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -545,6 +602,29 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -574,7 +654,6 @@ checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", - "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -597,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" @@ -646,7 +703,6 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", - "futures-macro", "futures-sink", "futures-task", "memchr", @@ -785,6 +841,12 @@ version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "1.4.1" @@ -931,6 +993,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itoa" version = "1.0.11" @@ -1042,9 +1110,9 @@ checksum = "e664971378a3987224f7a0e10059782035e89899ae403718ee07de85bec42afe" [[package]] name = "nostr" -version = "0.32.1" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7948938314ee0392f378ab1a5d58b4707f2207550bc410b1629a80a4f28af7d" +checksum = "5897e4142fcc33c4f1d58ad17f665e87dcba70de7e370c0bda1aa0fb73212c2a" dependencies = [ "aes", "base64 0.21.7", @@ -1072,9 +1140,9 @@ dependencies = [ [[package]] name = "nostr-database" -version = "0.32.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88a72f92fbd5d2514db36e07a864646f1c1f44931c4a5ea195f6961029af4b3" +checksum = "1926ef55392f3eea1bbe4a1358b64bbf12dd6eb554f40f483941a102c6263fc6" dependencies = [ "async-trait", "lru", @@ -1086,9 +1154,9 @@ dependencies = [ [[package]] name = "nostr-relay-pool" -version = "0.32.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7b7bf72b02a24ccc7cf87033fa5ddfd57001c7d8c2e757321a7ca7a6df39876" +checksum = "c6480cf60564957a2a64bd050d047ee0717e08dced7a389e22ef4e9fc104edd2" dependencies = [ "async-utility", "async-wsocket", @@ -1097,14 +1165,15 @@ dependencies = [ "nostr-database", "thiserror", "tokio", + "tokio-stream", "tracing", ] [[package]] name = "nostr-sdk" -version = "0.32.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "005915a59ee6401f23ba510c3a9ac4a07b457f80dfe1dc05cd2c8fdbde439246" +checksum = "ca0c0c5f8ddbdfc064ea71883191ec53de6ed52b5dca10ab07f0810b99e91acc" dependencies = [ "async-utility", "atomic-destructor", @@ -1122,9 +1191,9 @@ dependencies = [ [[package]] name = "nostr-signer" -version = "0.32.1" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f99449c2077bef43c02c8f9a9386d01c87e7ad8ece70d7de87a2c59771b4c0fe" +checksum = "5c30294a7be7d9d5ac777954812f5c7b4ae2a1e583a62e33537f87d98ab23729" dependencies = [ "async-utility", "nostr", @@ -1136,9 +1205,9 @@ dependencies = [ [[package]] name = "nostr-zapper" -version = "0.32.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430c2527c0efd2e7f1a421b0c7df01a03b334a79c60c39cc7a1ca684f720f2bf" +checksum = "dcf3ba30e807145e9cb924faf8fb0719e460f613088e99c753b67c2a9929c5b7" dependencies = [ "async-trait", "nostr", @@ -1172,9 +1241,9 @@ dependencies = [ [[package]] name = "nwc" -version = "0.32.0" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fb91e4be3f6b872fc23c7714bbb500a58a1d59f458eb6eb9dd249fbec42fc2" +checksum = "9a16ac06bc273fcd4ead47c0c5a58b6cc7db2247fc7a64dd9bc11cf18e3efeb4" dependencies = [ "async-utility", "nostr", @@ -1232,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" @@ -1392,6 +1451,35 @@ dependencies = [ "getrandom", ] +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + [[package]] name = "reqwest" version = "0.12.5" @@ -1462,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" @@ -1581,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" @@ -1866,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" @@ -2028,6 +2106,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.10.0" @@ -2124,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/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/client/Cargo.toml b/client/Cargo.toml index f704f37..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.32.0", features = ["nip04"] } +nostr-sdk = { version = "0.34.0", features = [] } cdk = "0.1.1" dotenv = "0.15.0" anyhow = "1.0.86" @@ -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..17bd2f6 --- /dev/null +++ b/client/src/cli/mod.rs @@ -0,0 +1,89 @@ +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; + +#[derive(Debug)] +struct RawCliInput { + buyer_npub: String, + seller_npub: String, + partner_ecash_pubkey: String, + coordinator_npub: String, + nostr_nsec: String, + mode: TradeMode, +} + +#[derive(Debug)] +pub struct ClientCliInput { + pub mode: TradeMode, + pub trader_nostr_keys: NostrKeys, + pub ecash_pubkey_partner: 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 partner_ecash_pubkey: String; + 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")?; + partner_ecash_pubkey = get_user_input("Enter seller's ecash pubkey: ").await?; + TradeMode::Buyer + } + "2" => { + nostr_nsec = env::var("SELLER_NSEC")?; + partner_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, + partner_ecash_pubkey, + coordinator_npub, + nostr_nsec, + mode, + }) + } +} + +impl ClientCliInput { + pub async fn parse() -> anyhow::Result { + let raw_input = RawCliInput::parse().await?; + debug!("Raw parsed CLI input: {:?}", raw_input); + + 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)?; + let trade_partner_nostr_pubkey = match raw_input.mode { + TradeMode::Buyer => NostrPubkey::from_bech32(&raw_input.seller_npub)?, + TradeMode::Seller => NostrPubkey::from_bech32(&raw_input.buyer_npub)?, + }; + + Ok(Self { + mode: raw_input.mode, + trader_nostr_keys, + ecash_pubkey_partner, + 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..e22e601 --- /dev/null +++ b/client/src/cli/trade_contract.rs @@ -0,0 +1,47 @@ +use super::*; + +pub trait FromClientCliInput { + 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, + trade_pubkey: String, + ) -> anyhow::Result { + debug!("Constructing hard coded client trade contract..."); + let npubkey_seller: PublicKey; + let npubkey_buyer: PublicKey; + + match cli_input.mode { + TradeMode::Buyer => { + npubkey_seller = cli_input.trade_partner_nostr_pubkey; + npubkey_buyer = cli_input.trader_nostr_keys.public_key(); + } + TradeMode::Seller => { + npubkey_buyer = cli_input.trade_partner_nostr_pubkey; + npubkey_seller = cli_input.trader_nostr_keys.public_key(); + } + } + + 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_amount_sat: 5000, + 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/ecash/mod.rs b/client/src/ecash/mod.rs index 7a3e943..6f40f35 100644 --- a/client/src/ecash/mod.rs +++ b/client/src/ecash/mod.rs @@ -1,52 +1,58 @@ use super::*; -use cdk::secp256k1::rand::Rng; +use crate::common::model::EscrowRegistration; +use cdk::nuts::PublicKey; 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 escrow_client::EscrowUser; use std::str::FromStr; use std::sync::Arc; -pub struct EcashWallet { - secret: SecretKey, +#[derive(Debug)] +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(); - 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]>(); - println!("Trade ecash pubkey: {}", trade_pubkey); + 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, }) } - async fn assemble_escrow_conditions( + fn assemble_escrow_conditions( &self, - user: &EscrowUser, + contract: &TradeContract, + escrow_registration: &EscrowRegistration, ) -> 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 = 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; + + 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, coordinator_escrow_pubkey]), Some(vec![buyer_pubkey]), Some(2), Some(SigFlag::SigAll), @@ -55,13 +61,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_registration: &EscrowRegistration, + ) -> anyhow::Result { + let spending_conditions = self.assemble_escrow_conditions(contract, escrow_registration)?; 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, ) @@ -69,13 +79,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_registration: &EscrowRegistration, ) -> 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_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 54aa1e7..1bf1d36 100644 --- a/client/src/escrow_client/mod.rs +++ b/client/src/escrow_client/mod.rs @@ -1,155 +1,162 @@ +use crate::common::model::EscrowRegistration; +use cdk::nuts::Token; + use super::*; -use cashu_escrow_common::nostr::PubkeyMessage; -use cdk::nuts::PublicKey; -pub enum Trader { - Buyer(EscrowUser), - Seller(EscrowUser), +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum TradeMode { + Buyer, + Seller, } -pub struct EscrowUser { - pub escrow_coordinator_npub: String, - pub escrow_pk_ts: (PublicKey, Timestamp), - pub contract: TradeContract, - pub wallet: EcashWallet, - pub nostr_client: NostrClient, +pub struct InitEscrowClient { + nostr_client: NostrClient, + ecash_wallet: ClientEcashWallet, + escrow_contract: TradeContract, + trade_mode: TradeMode, } -impl Trader { - pub async fn init_trade(&self) -> anyhow::Result<()> { - match self { - Trader::Buyer(config) => { - self.buyer_pipeline(config).await?; - Ok(()) - } - Trader::Seller(config) => { - self.seller_pipeline(config).await?; - Ok(()) - } +/// 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, } } - 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()); - - config - .nostr_client - .submit_trade_token_to_seller(&config.contract.npub_seller, &token) + /// 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_contract.npubkey_coordinator; + let contract_message = serde_json::to_string(&self.escrow_contract)?; + dbg!("sending contract to coordinator..."); + self.nostr_client + .client + .send_private_msg(*coordinator_pk, &contract_message, None) .await?; - // either send signature or begin dispute - Ok(()) + 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 { + nostr_client: self.nostr_client, + ecash_wallet: self.ecash_wallet, + escrow_contract: self.escrow_contract, + trade_mode: self.trade_mode, + escrow_registration, + }) } +} - 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 - - // await signature or begin dispute - Ok(()) - } +pub struct RegisteredEscrowClient { + nostr_client: NostrClient, + ecash_wallet: ClientEcashWallet, + escrow_contract: TradeContract, + trade_mode: TradeMode, + escrow_registration: EscrowRegistration, } -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?; - - Ok(Self { - escrow_coordinator_npub, - escrow_pk_ts, - contract, - wallet, - nostr_client, - }) +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(mut self) -> anyhow::Result { + match self.trade_mode { + TradeMode::Buyer => { + // todo: store the sent token in next instance + 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().await?; + Ok(TokenExchangedEscrowClient { + _nostr_client: self.nostr_client, + _ecash_wallet: self.ecash_wallet, + _escrow_contract: self.escrow_contract, + trade_mode: self.trade_mode, + }) + } + } } - 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) + /// 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_token = self + .ecash_wallet + .create_escrow_token(escrow_contract, &self.escrow_registration) .await?; - let escrow_coordinator_pk = - Self::receive_escrow_coordinator_pk(nostr_client, escrow_coordinator_npub).await?; - Ok(escrow_coordinator_pk) - } + debug!("Sending token to the seller: {}", escrow_token); + + self.nostr_client + .client + .send_private_msg(escrow_contract.npubkey_seller, &escrow_token, None) + .await?; + dbg!("Sent Token to seller"); - 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)) + Ok(escrow_token) } - 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")) + /// 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(&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) } +} - 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); - } - } +pub struct TokenExchangedEscrowClient { + _nostr_client: NostrClient, + _ecash_wallet: ClientEcashWallet, + _escrow_contract: TradeContract, + trade_mode: TradeMode, +} + +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. + 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 + match self.trade_mode { + TradeMode::Buyer => { + dbg!("Payed invoince and waiting for delivery..."); + } + TradeMode::Seller => { + dbg!("Got payment and proceding with delivery..."); } } - Err(anyhow!("No valid escrow token received")) + Ok(()) } } diff --git a/client/src/main.rs b/client/src/main.rs index aa34a37..fb77115 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,75 +1,53 @@ +mod cli; mod ecash; mod escrow_client; use std::env; -use anyhow::anyhow; use cashu_escrow_common as common; -use common::cli::get_user_input; -use common::nostr::NostrClient; -use common::TradeContract; +use cdk::amount::{Amount, SplitTarget}; +use cli::trade_contract::FromClientCliInput; +use cli::ClientCliInput; +use common::model::TradeContract; +use common::{cli::get_user_input, nostr::NostrClient}; use dotenv::dotenv; -use ecash::EcashWallet; -use escrow_client::{EscrowUser, Trader}; +use ecash::ClientEcashWallet; +use escrow_client::*; +use log::{debug, info}; use nostr_sdk::prelude::*; #[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")?; + env_logger::builder() + .filter_module("client", log::LevelFilter::Debug) // logging level of the client + .filter_level(log::LevelFilter::Info) // logging level of all other crates + .init(); + let mint_url = env::var("MINT_URL")?; - let ecash_wallet = EcashWallet::new(mint_url.as_str()).await?; - let mut seller_ecash_pubkey: String = String::new(); - let mut buyer_ecash_pubkey: String = String::new(); - let nostr_client: NostrClient; + let escrow_wallet = ClientEcashWallet::new(&mint_url).await?; - 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 cli_input = ClientCliInput::parse().await?; - let contract = TradeContract { - trade_description: "Purchase of one Watermelon for 5000 satoshi. 3 days delivery to ..." - .to_string(), - trade_mint_url: mint_url, - 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, - }; + //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_user = - EscrowUser::new(contract, ecash_wallet, nostr_client, coordinator_npub).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?; - 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")), - } + InitEscrowClient::new(nostr_client, escrow_wallet, escrow_contract, cli_input.mode) + .register_trade() + .await? + .exchange_trade_token() + .await? + .do_your_trade_duties() + .await?; + Ok(()) } diff --git a/common/Cargo.toml b/common/Cargo.toml index f85e479..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.32.0", features = ["nip04"] } +nostr-sdk = { version = "0.34.0", features = [] } cdk = "0.1.1" anyhow = "1.0.86" tokio = "1.38.0" diff --git a/common/src/lib.rs b/common/src/lib.rs index 4cc5f65..fcf55c1 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,16 +1,23 @@ -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, - 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..d7dfe93 --- /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, Debug, Clone)] +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 6075888..3571bc5 100644 --- a/common/src/nostr/mod.rs +++ b/common/src/nostr/mod.rs @@ -1,25 +1,23 @@ -use crate::TradeContract; -use ndk::prelude::*; -use nostr_sdk as ndk; -use serde::{Deserialize, Serialize}; +use std::time::Duration; + +use crate::model::EscrowRegistration; +use anyhow::anyhow; +use nostr_sdk::prelude::*; +use tokio::{ + sync::broadcast::{error::RecvError, Receiver}, + time::timeout, +}; pub struct NostrClient { - keypair: Keys, + keys: Keys, pub client: Client, -} - -#[derive(Serialize, Deserialize)] -pub struct PubkeyMessage { - pub escrow_coordinator_pubkey: String, - pub trade_id_hex: String, - pub escrow_start_ts: Timestamp, + subscription_id: SubscriptionId, + notifications_receiver: Receiver, } 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?; @@ -28,71 +26,89 @@ 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; - Ok(Self { keypair, client }) + + let (_subscription_id, notifications_receiver) = init_subscription(&keys, &client).await?; + + Ok(Self { + keys, + client, + subscription_id: _subscription_id, + notifications_receiver, + }) } - pub fn get_npub(&self) -> anyhow::Result { - Ok(self.keypair.public_key().to_bech32()?) + pub fn public_key(&self) -> PublicKey { + self.keys.public_key() } - pub async fn decrypt_msg(&self, msg: &String, 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 + pub async fn receive_escrow_message(&mut self, timeout_secs: u64) -> anyhow::Result { + let loop_future = async { + loop { + 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); + } + } + } + }; + let result = match timeout(Duration::from_secs(timeout_secs), loop_future).await { + Ok(result) => result, + Err(e) => Err(anyhow!("Timeout, {}", e)), + }; + + result } - pub async fn send_escrow_pubkeys( + // coordinator specific function? + pub async fn send_escrow_registration( &self, - receivers: (&String, &String), + receivers: (PublicKey, PublicKey), id: &[u8; 32], - trade_pk: &String, + trade_pk: &str, ) -> anyhow::Result<()> { - let message = serde_json::to_string(&PubkeyMessage { - escrow_coordinator_pubkey: trade_pk.clone(), - 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(PublicKey::from_bech32(receivers.0)?, &message, None) + .send_private_msg(receivers.0, ®istration_json, None) .await?; self.client - .send_direct_msg(PublicKey::from_bech32(receivers.1)?, &message, None) - .await?; - Ok(()) - } - - pub async fn send_escrow_contract( - &self, - contract: &TradeContract, - coordinator_pk_bech32: &String, - ) -> anyhow::Result<()> { - let message = serde_json::to_string(contract)?; - dbg!("sending contract to coordinator..."); - self.client - .send_direct_msg( - PublicKey::from_bech32(coordinator_pk_bech32)?, - &message, - None, - ) + .send_private_msg(receivers.1, ®istration_json, None) .await?; Ok(()) } +} - pub async fn submit_trade_token_to_seller( - &self, - seller_npub: &String, - token: &String, - ) -> anyhow::Result<()> { - self.client - .send_direct_msg(PublicKey::from_bech32(seller_npub)?, token, None) - .await?; - 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)) } diff --git a/coordinator/Cargo.toml b/coordinator/Cargo.toml index f597119..807a47a 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.34.0", features = [] } cdk = "0.1.1" dotenv = "0.15.0" anyhow = "1.0.86" @@ -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 a9cb44d..7ad0364 100644 --- a/coordinator/src/escrow_coordinator/mod.rs +++ b/coordinator/src/escrow_coordinator/mod.rs @@ -1,12 +1,14 @@ use super::*; -use cashu_escrow_common::TradeContract; -use cdk::nuts::SecretKey; +use anyhow::anyhow; +use cashu_escrow_common::model::TradeContract; +use cdk::nuts::SecretKey as CDKSecretKey; use hashes::hex::DisplayHex; 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; +use tokio::sync::broadcast::error::RecvError; pub struct EscrowCoordinator { nostr_client: NostrClient, @@ -14,13 +16,13 @@ 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 { - 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(), @@ -29,52 +31,66 @@ impl EscrowCoordinator { } pub async fn run(&mut self) -> anyhow::Result<()> { + let my_pubkey = self.nostr_client.public_key(); let filter_note = Filter::new() - .kind(Kind::EncryptedDirectMessage) - .custom_tag( - SingleLetterTag::lowercase(Alphabet::P), - [PublicKey::from_bech32(&self.nostr_client.get_npub()?)?.to_hex()], - ) - .since(Timestamp::now()); + .kind(Kind::GiftWrap) + .pubkey(my_pubkey) + .limit(0); 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 { - if let RelayPoolNotification::Event { event, .. } = notification { - 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 { - 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); + loop { + 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)) = + EscrowCoordinator::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 loop!" + )); } } + Err(RecvError::Closed) => { + break Err(anyhow!( + "Got closed error from channel, breaking coordinator loop!" + )) + } + Err(RecvError::Lagged(count)) => { + //todo: use logger instead + eprintln!("Lost {} events, resuming after that...", count); + } } } - Ok(()) - } - - async fn parse(&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( @@ -86,17 +102,17 @@ 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 - .send_escrow_pubkeys( - (&trade.npub_buyer, &trade.npub_seller), + .send_escrow_registration( + (trade.npubkey_buyer, trade.npubkey_seller), contract_hash, &contract_secret.public_key().to_hex(), ) @@ -104,7 +120,15 @@ impl EscrowCoordinator { Ok(()) } - // pub async fn subscribe(&self) -> anyhow::Result<()> { - // 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)) + } } diff --git a/coordinator/src/main.rs b/coordinator/src/main.rs index 5abd451..d2569d4 100644 --- a/coordinator/src/main.rs +++ b/coordinator/src/main.rs @@ -1,16 +1,21 @@ 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::{Keys, 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()?); + 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()? + ); println!("Starting service and waiting for trades..."); - return EscrowCoordinator::setup(nostr_client).await?.run().await; + return EscrowCoordinator::new(nostr_client)?.run().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