diff --git a/bindings/cdk-js/src/nuts/nut10.rs b/bindings/cdk-js/src/nuts/nut10.rs index 06f99abad..f6016f398 100644 --- a/bindings/cdk-js/src/nuts/nut10.rs +++ b/bindings/cdk-js/src/nuts/nut10.rs @@ -14,6 +14,8 @@ impl From for JsKind { match inner { Kind::P2PK => JsKind::P2PK, Kind::HTLC => JsKind::HTLC, + Kind::DLC => todo!(), + Kind::SCT => todo!(), } } } diff --git a/crates/cdk-cli/Cargo.toml b/crates/cdk-cli/Cargo.toml index 45f0b1133..cafa4fe49 100644 --- a/crates/cdk-cli/Cargo.toml +++ b/crates/cdk-cli/Cargo.toml @@ -37,3 +37,8 @@ reqwest = { version = "0.12", default-features = false, features = [ "socks", ]} url = "2.3" +dlc-messages = { version = "0.5.0", features = ["use-serde"] } +lightning = "0.0.121" +schnorr_fun = { version = "0.9.2", features = ["bincode", "serde"] } +sha2 = "0.10.8" +dlc = "0.5.0" diff --git a/crates/cdk-cli/src/main.rs b/crates/cdk-cli/src/main.rs index 7ff70cda0..a16a23ca9 100644 --- a/crates/cdk-cli/src/main.rs +++ b/crates/cdk-cli/src/main.rs @@ -78,6 +78,8 @@ enum Commands { PayRequest(sub_commands::pay_request::PayRequestSubCommand), /// Create Payment request CreateRequest(sub_commands::create_request::CreateRequestSubCommand), + // Create and manage DLC offers + DLC(sub_commands::dlc::DLCSubCommand), } #[tokio::main] @@ -219,5 +221,8 @@ async fn main() -> Result<()> { Commands::CreateRequest(sub_command_args) => { sub_commands::create_request::create_request(&multi_mint_wallet, sub_command_args).await } + Commands::DLC(sub_command_args) => { + sub_commands::dlc::dlc(&multi_mint_wallet, sub_command_args).await + } } } diff --git a/crates/cdk-cli/src/sub_commands/dlc/mod.rs b/crates/cdk-cli/src/sub_commands/dlc/mod.rs new file mode 100644 index 000000000..a0e66fed9 --- /dev/null +++ b/crates/cdk-cli/src/sub_commands/dlc/mod.rs @@ -0,0 +1,930 @@ +use core::panic; +use std::io::{self, stdin, Write}; +use std::str::FromStr; +use std::time::Duration; +use std::vec; + +use anyhow::{Error, Result}; +use cdk::amount::{Amount, SplitTarget}; +use cdk::dhke::construct_proofs; +use cdk::mint_url::MintUrl; +use cdk::nuts::nutdlc::{DLCLeaf, DLCOutcome, DLCRoot, DLCTimeoutLeaf, PayoutStructure}; +use cdk::nuts::{self, BlindedMessage, PreMintSecrets, Proofs, State, Token}; +use cdk::secret; +use cdk::types::ProofInfo; +use cdk::wallet::multi_mint_wallet::WalletKey; +use cdk::wallet::{MultiMintWallet, Wallet}; +use clap::{Args, Subcommand}; +use dlc::secp256k1_zkp::hashes::sha256; +use dlc::{ + secp256k1_zkp::{All, Secp256k1}, + OracleInfo, +}; +use dlc_messages::oracle_msgs::{EventDescriptor, OracleAnnouncement, OracleAttestation}; +use nostr_sdk::{ + hashes::hex::{Case, DisplayHex}, + Client, EventId, Keys, PublicKey, SecretKey, +}; +use serde::{Deserialize, Serialize}; + +use super::balance::mint_balances; + +pub mod nostr_events; +pub mod utils; +const RELAYS: [&str; 1] = ["wss://relay.8333.space"]; + +#[derive(Args)] +pub struct DLCSubCommand { + #[command(subcommand)] + pub command: DLCCommands, +} + +#[derive(Subcommand)] +pub enum DLCCommands { + CreateBet { + key: String, + oracle_event_id: String, + counterparty_pubkey: String, + amount: u64, + //needs to show user outcomes an let user decide which outcome he wants + }, + ListOffers { + key: String, + }, + DeleteOffers { + key: String, + }, + AcceptBet { + key: String, + // the event id of the offered bet + event_id: String, + }, +} + +// I imagine this is what will be sent back and forth in the kind 8888 messages +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct UserBet { + pub id: i32, + pub oracle_announcement: OracleAnnouncement, + oracle_event_id: String, + alice_outcome: String, + blinding_factor: String, + dlc_root: String, + timeout: u64, + amount: u64, // TODO: use the Amount type + locked_ecash: Vec, + winning_payout_structure: PayoutStructure, + winning_counterparty_payout_structure: PayoutStructure, + timeout_payout_structure: PayoutStructure, +} + +/// To manage DLC contracts (ie. creating and accepting bets) +// TODO: Different name? +// TODO: put the wallet in here instead of passing it in every function +pub struct DLC { + keys: Keys, + nostr: Client, + secp: Secp256k1, +} + +impl DLC { + /// Create new [`DLC`] + pub async fn new(secret_key: &SecretKey) -> Result { + let keys = Keys::from_str(&secret_key.display_secret().to_string())?; + let nostr = Client::new(&keys.clone()); + for relay in RELAYS.iter() { + nostr.add_relay(relay.to_string()).await?; + } + nostr.connect().await; + let secp: Secp256k1 = Secp256k1::gen_new(); + + Ok(Self { keys, nostr, secp }) + } + + async fn create_funding_token( + &self, + wallet: &Wallet, + dlc_root: &DLCRoot, + amount: u64, + ) -> Result<(Token, secret::Secret), Error> { + let threshold = 1; // TOOD: this should come from payout structures + let dlc_conditions = + nuts::nut11::Conditions::new(None, None, None, None, None, Some(threshold))?; + + let dlc_secret = + nuts::nut10::Secret::new(nuts::Kind::DLC, dlc_root.to_string(), Some(dlc_conditions)); + // TODO: this will put the same secret into each proof. + // I'm not sure if the mint will allow us to spend multiple proofs with the same backup secret + // If not, we can use a p2pk backup, or new backup secret for each proof + let backup_secret = secret::Secret::generate(); + + // NOTE: .try_into() converts Nut10Secret to Secret + let dlc_secret: secret::Secret = dlc_secret.clone().try_into()?; + + let (sct_conditions, sct_proof) = nuts::nut11::SpendingConditions::new_dlc_sct( + vec![dlc_secret.clone(), backup_secret.clone()], + 0, + ); + + let available_proofs = wallet.get_unspent_proofs().await?; + + let include_fees = false; + + let selected = wallet + .select_proofs_to_send(Amount::from(amount), available_proofs, include_fees) + .await + .unwrap(); + + let mut funding_proofs = wallet + .swap( + Some(Amount::from(amount)), + SplitTarget::default(), + selected, + Some(sct_conditions), + include_fees, + ) + .await? + .unwrap(); + + for proof in &mut funding_proofs { + proof.add_sct_witness(dlc_secret.to_string(), sct_proof.clone()); + } + + let token = cdk::nuts::nut00::Token::new( + MintUrl::from_str("https://testnut.brownduff.rocks").unwrap(), + funding_proofs.clone(), + Some(String::from("dlc locking proofs")), + nuts::CurrencyUnit::Sat, + ); + + Ok((token, backup_secret)) + } + + fn compute_leaves( + &self, + announcement: OracleAnnouncement, + blinding_factor: dlc::secp256k1_zkp::Scalar, + winning_outcome: &String, + winning_payout_structure: PayoutStructure, + winning_counterparty_payout_structure: PayoutStructure, + timeout_payout_structure: PayoutStructure, + timeout: u64, + ) -> Result<(Vec, DLCTimeoutLeaf), Error> { + let oracle_info = OracleInfo { + public_key: announcement.oracle_public_key, + nonces: announcement.oracle_event.oracle_nonces.clone(), + }; + + let all_outcomes = if let EventDescriptor::EnumEvent(ref desc) = + announcement.oracle_event.event_descriptor + { + if !desc.outcomes.contains(&winning_outcome) { + return Err(Error::msg("Invalid winning outcome")); + } + desc.outcomes.clone() + } else { + return Err(Error::msg("Digit decomposition event not supported")); + }; + + let leaves: Vec = all_outcomes + .into_iter() + .map(|outcome| { + // hash the outcome + let msg = vec![ + dlc::secp256k1_zkp::Message::from_hashed_data::( + outcome.as_bytes(), + ), + ]; + + // get adaptor point + let point = dlc::get_adaptor_point_from_oracle_info( + &self.secp, + &[oracle_info.clone()], + &[msg], + ) + .unwrap(); + + // blind adaptor point with Ki_ = Ki + b * G + let blinded_point = point.add_exp_tweak(&self.secp, &blinding_factor).unwrap(); + + let payout = if winning_outcome.contains(&outcome) { + // we win + winning_payout_structure.clone() + } else { + // they win + winning_counterparty_payout_structure.clone() + }; + + DLCLeaf { + blinded_locking_point: cdk::nuts::PublicKey::from_slice( + &blinded_point.serialize(), + ) + .expect("valid public key"), + payout, + } + }) + .collect(); + let timeout_leaf = DLCTimeoutLeaf::new(&timeout, &timeout_payout_structure); + + Ok((leaves, timeout_leaf)) + } + + fn signatures_to_secret( + signatures: &[Vec], + ) -> Result { + let s_values = signatures + .iter() + .flatten() + .map(|x| match dlc::secp_utils::schnorrsig_decompose(x) { + Ok(v) => Ok(v.1), + Err(err) => Err(err), + }) + .collect::, dlc::Error>>()?; + let secret = dlc::secp256k1_zkp::SecretKey::from_slice(s_values[0])?; + + let result = s_values.iter().skip(1).fold(secret, |accum, s| { + let sec = dlc::secp256k1_zkp::SecretKey::from_slice(s).unwrap(); + accum + .add_tweak(&dlc::secp256k1_zkp::scalar::Scalar::from(sec)) + .unwrap() + }); + + Ok(result) + } + + /// Start a new DLC contract, and send to the counterparty + /// # Arguments + /// * `announcement` - OracleAnnouncement + /// * `announcement_id` - Id of kind 88 event + /// * `counterparty_pubkey` - hex encoded public key of counterparty + /// * `outcomes` - ??outcomes this user wants to bet on?? I think! + pub async fn create_bet( + &self, + wallet: &Wallet, + announcement: OracleAnnouncement, + announcement_id: EventId, + counterparty_pubkey: nostr_sdk::key::PublicKey, + outcomes: Vec, + amount: u64, + ) -> Result { + let winning_payout_structure = PayoutStructure::default(self.keys.public_key().to_string()); + let winning_counterparty_payout_structure = + PayoutStructure::default(counterparty_pubkey.to_string()); + // timeout set to 1 hour from event_maturity_epoch + let timeout = (announcement.oracle_event.event_maturity_epoch as u64) + + Duration::from_secs(60 * 60).as_secs(); + let timeout_payout_structure = PayoutStructure::default_timeout(vec![ + self.keys.public_key().to_string(), + counterparty_pubkey.to_string(), + ]); + + let blinding_factor = dlc::secp256k1_zkp::Scalar::random(); + let winning_outcome = outcomes.first().unwrap().clone(); + + let (leaves, timeout_leaf) = self.compute_leaves( + announcement.clone(), + blinding_factor, + &winning_outcome, + winning_payout_structure.clone(), + winning_counterparty_payout_structure.clone(), + timeout_payout_structure.clone(), + timeout, + )?; + + let dlc_root = DLCRoot::compute(leaves, Some(timeout_leaf)); + + let (token, _backup_secret) = self + .create_funding_token(&wallet, &dlc_root, amount) + .await?; + + // TODO: backup the backup secret + + let offer_dlc = UserBet { + id: 7, // TODO, + oracle_announcement: announcement.clone(), + oracle_event_id: announcement_id.to_string(), + alice_outcome: winning_outcome, + blinding_factor: blinding_factor.to_be_bytes().to_hex_string(Case::Lower), + dlc_root: dlc_root.to_string(), + timeout, + amount, + locked_ecash: vec![token], + winning_payout_structure, + winning_counterparty_payout_structure, + timeout_payout_structure, + }; + + let offer_dlc = serde_json::to_string(&offer_dlc)?; + + let offer_dlc_event = + nostr_events::create_dlc_msg_event(&self.keys, offer_dlc, &counterparty_pubkey)?; + + match self.nostr.send_event(offer_dlc_event).await { + Ok(event_id) => Ok(event_id.val), + Err(e) => Err(Error::from(e)), + } + } + + pub async fn accept_bet(&self, wallet: &Wallet, bet: &UserBet) -> Result<(), Error> { + // TODO: validate payout structures + // TODO: validate dlc_root + + let (funding_token, _backup_secret) = self + .create_funding_token(wallet, &DLCRoot::from_str(&bet.dlc_root)?, bet.amount) + .await?; + + // TODO: backup the backup secret + + let counterparty_funding_token = bet.locked_ecash.first().unwrap().clone(); + + /* extract proofs from both funding tokens */ + let mut dlc_inputs: Proofs = Vec::new(); + + dlc_inputs.extend(funding_token.proofs()); + dlc_inputs.extend(counterparty_funding_token.proofs()); + + let dlc_registration = nuts::nutdlc::DLC { + dlc_root: bet.dlc_root.clone(), + funding_amount: Amount::from(bet.amount), + unit: nuts::CurrencyUnit::Sat, + inputs: dlc_inputs, + }; + + println!("Registering DLC"); + + wallet.register_dlc(dlc_registration).await?; + + Ok(()) + } + + pub async fn settle_bet( + &self, + wallet: &Wallet, + bet: &UserBet, + attestation: OracleAttestation, + ) -> Result<(), Error> { + let blinding_factor_bytes: [u8; 32] = nostr_sdk::util::hex::decode(&bet.blinding_factor) + .map_err(|_| Error::msg("Invalid blinding factor"))? + .try_into() + .map_err(|_| Error::msg("Invalid blinding factor length"))?; + + let blinding_factor = dlc::secp256k1_zkp::Scalar::from_be_bytes(blinding_factor_bytes)?; + + assert_eq!( + bet.blinding_factor, + blinding_factor.to_be_bytes().to_hex_string(Case::Lower), + "Blinding factors do not match" + ); + + let (leaves, timeout_leaf) = self.compute_leaves( + bet.oracle_announcement.clone(), + blinding_factor, + &bet.alice_outcome, + bet.winning_payout_structure.clone(), + bet.winning_counterparty_payout_structure.clone(), + bet.timeout_payout_structure.clone(), + bet.timeout, + )?; + + let leaf_hashes: Vec<[u8; 32]> = leaves.iter().map(|l| l.hash()).collect(); + let leaf_hashes = vec![leaf_hashes[0], leaf_hashes[1], timeout_leaf.hash()]; + + let dlc_root = DLCRoot::compute(leaves.clone(), Some(timeout_leaf)); + + assert_eq!( + bet.dlc_root, + dlc_root.to_string(), + "Recomputed dlc_root does not match" + ); + + assert_eq!(attestation.outcomes[0], bet.alice_outcome, "Wrong outcome"); + + let merkle_proof = nuts::nutsct::merkle_prove(leaf_hashes, 0); + + let secret = DLC::signatures_to_secret(&[attestation.signatures])?; + let blinded_secret = secret.add_tweak(&blinding_factor).unwrap(); + + let outcome = DLCOutcome { + blinded_attestation_secret: blinded_secret.display_secret().to_string(), + payout_structure: leaves[0].payout.clone(), + }; + + wallet + .settle_dlc(&bet.dlc_root, outcome, merkle_proof) + .await?; + + Ok(()) + } + + async fn claim_payout(&self, wallet: &Wallet, bet: &UserBet) -> Result<()> { + let dlc_status = wallet.dlc_status(bet.dlc_root.clone()).await?; + + if !dlc_status.settled { + return Err(Error::msg("DLC not settled".to_string())); + } + + let our_debt = if let Some(debts) = dlc_status.debts { + let our_public_key = self.keys.public_key(); + debts + .iter() + .find(|(k, _)| { + /* we prefix our public key with "02" to convert our nostr key to 33 bytes */ + let key_without_prefix = &k[2..]; + key_without_prefix == our_public_key.to_string() + }) + .map(|(_, v)| *v) + .ok_or_else(|| Error::msg("Our public key not found in debts".to_string()))? + } else { + return Err(Error::msg("No debts in DLC".to_string())); + }; + + let dlc_root = DLCRoot::from_str(&bet.dlc_root)?.to_bytes(); + + let sig = self + .keys + .sign_schnorr(&nostr_sdk::secp256k1::Message::from_digest(dlc_root)); + + let keyset_id = wallet.get_active_mint_keyset().await?.id; + + let pre_mint_secrets = + PreMintSecrets::random(keyset_id, Amount::from(our_debt), &SplitTarget::None)?; + let outputs: Vec = pre_mint_secrets + .clone() + .secrets + .into_iter() + .map(|s| s.blinded_message) + .collect(); + + let payout = wallet + .claim_dlc_payout( + bet.dlc_root.clone(), + format!("02{}", self.keys.public_key().to_string()), + outputs.clone(), + Some(sig.to_string()), + ) + .await?; + + let keys = wallet.get_keyset_keys(keyset_id).await?; + + let proofs = construct_proofs( + payout.outputs.iter().map(|p| p.clone()).collect(), + pre_mint_secrets.rs(), + pre_mint_secrets.secrets(), + &keys, + )?; + + let proofs = proofs + .into_iter() + .map(|proof| { + ProofInfo::new( + proof, + wallet.mint_url.clone(), + State::Unspent, + nuts::CurrencyUnit::Sat, + ) + }) + .collect::, _>>()?; + + let total_claimed = proofs + .clone() + .iter() + .fold(Amount::ZERO, |acc, p| acc + p.proof.amount); + println!("Claimed {:?}", total_claimed); + + wallet.localstore.update_proofs(proofs, vec![]).await?; + Ok(()) + } +} + +pub async fn dlc(wallets: &MultiMintWallet, sub_command_args: &DLCSubCommand) -> Result<()> { + //let keys = + // Keys::parse("nsec15jldh0htg2qeeqmqd628js8386fu4xwpnuqddacc64gh0ezdum6qaw574p").unwrap(); + + let unit = nuts::CurrencyUnit::Sat; + + match &sub_command_args.command { + DLCCommands::CreateBet { + key, + oracle_event_id, + counterparty_pubkey, + amount, + } => { + let keys = Keys::parse(key).unwrap(); + let oracle_event_id = EventId::from_hex(oracle_event_id).unwrap(); + let counterparty_pubkey = PublicKey::from_hex(counterparty_pubkey).unwrap(); + + let dlc = DLC::new(keys.secret_key()).await?; + + let announcement_event = + match nostr_events::lookup_announcement_event(oracle_event_id, &dlc.nostr).await { + Some(Ok(event)) => event, + _ => panic!("Oracle announcement event not found"), + }; + + let oracle_announcement = + utils::oracle_announcement_from_str(&announcement_event.content); + + println!( + "Oracle announcement event content: {:?}", + oracle_announcement + ); + + // // TODO: get the outcomes from the oracle announcement??? + + let outcomes = match oracle_announcement.oracle_event.event_descriptor { + EventDescriptor::EnumEvent(ref e) => e.outcomes.clone(), + EventDescriptor::DigitDecompositionEvent(_) => unreachable!(), + }; + + for (i, outcome) in outcomes.clone().into_iter().enumerate() { + println!("outcome {i}: {outcome}"); + } + + let mut input_line = String::new(); + + println!("please select outcome by number"); + + stdin() + .read_line(&mut input_line) + .expect("Failed to read line"); + let choice: i32 = input_line.trim().parse().expect("Input not an integer"); + + let outcome_choice = vec![outcomes[choice as usize].clone()]; + + println!( + "You chose outcome {:?} to bet {} on", + outcome_choice, amount + ); + + /* let user pick which wallet to use */ + let mints_amounts = mint_balances(wallets, &unit).await?; + + println!("Enter a mint number to create a DLC offer for"); + + let mut user_input = String::new(); + io::stdout().flush().unwrap(); + stdin().read_line(&mut user_input)?; + + let mint_number: usize = user_input.trim().parse()?; + + if mint_number.gt(&(mints_amounts.len() - 1)) { + crate::bail!("Invalid mint number"); + } + + let mint_url = mints_amounts[mint_number].0.clone(); + + let wallet = match wallets + .get_wallet(&WalletKey::new(mint_url.clone(), unit)) + .await + { + Some(wallet) => wallet.clone(), + None => { + // let wallet = Wallet::new(&mint_url.to_string(), unit, localstore, seed, None)?; + + // multi_mint_wallet.add_wallet(wallet.clone()).await; + // wallet + todo!() + } + }; + + let event_id = dlc + .create_bet( + &wallet, + oracle_announcement, + oracle_event_id, + counterparty_pubkey, + outcomes, + *amount, + ) + .await?; + + println!("Event {} sent to {}", event_id, counterparty_pubkey); + } + DLCCommands::ListOffers { key } => { + let keys = Keys::parse(key).unwrap(); + + let dlc = DLC::new(keys.secret_key()).await?; + + let bets = nostr_events::list_dlc_offers(&keys, &dlc.nostr, None).await; + + println!("{:?}", bets); + } + DLCCommands::DeleteOffers { key } => { + let keys = Keys::parse(key).unwrap(); + + let dlc = DLC::new(keys.secret_key()).await?; + + let bets = nostr_events::delete_all_dlc_offers(&keys, &dlc.nostr).await; + + println!("{:?}", bets); + } + DLCCommands::AcceptBet { key, event_id } => { + let keys = Keys::parse(key).unwrap(); + let event_id = EventId::from_hex(event_id).unwrap(); + + let dlc = DLC::new(keys.secret_key()).await?; + + let bet = nostr_events::list_dlc_offers(&keys, &dlc.nostr, Some(event_id)) + .await + .unwrap() + .first() + .unwrap() + .clone(); + + /* let user pick which wallet to use */ + let mints_amounts = mint_balances(wallets, &unit).await?; + + println!("Enter a mint number to create a DLC offer for"); + + let mut user_input = String::new(); + io::stdout().flush().unwrap(); + stdin().read_line(&mut user_input)?; + + let mint_number: usize = user_input.trim().parse()?; + + if mint_number.gt(&(mints_amounts.len() - 1)) { + crate::bail!("Invalid mint number"); + } + + // TODO: wallet needs to be from same mint as bet + let mint_url = mints_amounts[mint_number].0.clone(); + + let wallet = match wallets + .get_wallet(&WalletKey::new(mint_url.clone(), unit)) + .await + { + Some(wallet) => wallet.clone(), + None => { + // let wallet = Wallet::new(&mint_url.to_string(), unit, localstore, seed, None)?; + + // multi_mint_wallet.add_wallet(wallet.clone()).await; + // wallet + todo!() + } + }; + + dlc.accept_bet(&wallet, &bet).await?; + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::{fs, str::FromStr, sync::Arc}; + + use bip39::Mnemonic; + use cdk::{ + cdk_database::{self, WalletDatabase}, + mint_url::MintUrl, + wallet::Wallet, + }; + use cdk_sqlite::WalletSqliteDatabase; + use dlc_messages::oracle_msgs::EventDescriptor; + use nostr_sdk::{Client, EventId, Keys}; + use rand::Rng; + + use super::*; + use crate::sub_commands::dlc::{ + nostr_events::{delete_all_dlc_offers, list_dlc_offers}, + utils::oracle_announcement_from_str, + DLC, + }; + + const DEFAULT_WORK_DIR: &str = ".cdk-cli"; + const MINT_URL: &str = "https://testnut.brownduff.rocks"; + + /// helper function to initialize wallets + async fn initialize_wallets() -> MultiMintWallet { + let work_dir = { + let home_dir = home::home_dir().unwrap(); + home_dir.join(DEFAULT_WORK_DIR) + }; + let localstore: Arc + Send + Sync> = { + let sql_path = work_dir.join("cdk-cli.sqlite"); + let sql = WalletSqliteDatabase::new(&sql_path).await.unwrap(); + + sql.migrate().await; + + Arc::new(sql) + }; + + let seed_path = work_dir.join("seed"); + + let mnemonic = match fs::metadata(seed_path.clone()) { + Ok(_) => { + let contents = fs::read_to_string(seed_path.clone()).unwrap(); + Mnemonic::from_str(&contents).unwrap() + } + Err(_e) => { + let mut rng = rand::thread_rng(); + let random_bytes: [u8; 32] = rng.gen(); + + let mnemnic = Mnemonic::from_entropy(&random_bytes).unwrap(); + tracing::info!("Using randomly generated seed you will not be able to restore"); + + mnemnic + } + }; + + let mut wallets: Vec = Vec::new(); + + let mints = localstore.get_mints().await.unwrap(); + + for (mint, _) in mints { + let wallet = Wallet::new( + &mint.to_string(), + cdk::nuts::CurrencyUnit::Sat, + localstore.clone(), + &mnemonic.to_seed_normalized(""), + None, + ) + .unwrap(); + + wallets.push(wallet); + } + + MultiMintWallet::new(wallets) + } + + #[tokio::test] + async fn test_full_flow() { + let multi_mint_wallet = initialize_wallets().await; + let wallet = multi_mint_wallet + .get_wallet(&WalletKey::new( + MintUrl::from_str(MINT_URL).unwrap(), + cdk::nuts::CurrencyUnit::Sat, + )) + .await + .unwrap(); + + let alice_keys = Keys::generate(); + let bob_keys = Keys::generate(); + + let alice_dlc = DLC::new(alice_keys.secret_key()).await.unwrap(); + let bob_dlc = DLC::new(bob_keys.secret_key()).await.unwrap(); + + let oracle_event_id = + EventId::from_hex("f6b983be1d9f984d269b66c80421c66a1ad9fcfecbc7d656f4cb7a8098d4d949") + .unwrap(); + + let announcement_event = match nostr_events::lookup_announcement_event( + oracle_event_id, + &alice_dlc.nostr, + ) + .await + { + Some(Ok(event)) => event, + _ => std::panic!("Oracle announcement event not found"), + }; + + let announcement = utils::oracle_announcement_from_str(&announcement_event.content); + + let descriptor = &announcement.oracle_event.event_descriptor; + + let outcomes = match descriptor { + EventDescriptor::EnumEvent(ref e) => e.outcomes.clone(), + EventDescriptor::DigitDecompositionEvent(_) => unreachable!(), + }; + let alice_outcome = &outcomes.clone()[0]; + + let amount = 7; + println!("Alice is creating a bet for {} sats", amount); + let offer_event_id = alice_dlc + .create_bet( + &wallet, + announcement, + oracle_event_id.clone(), + bob_keys.public_key(), + vec![alice_outcome.clone()], + amount, + ) + .await + .unwrap(); + + let bet = nostr_events::list_dlc_offers(&bob_keys, &bob_dlc.nostr, Some(offer_event_id)) + .await + .unwrap() + .first() + .unwrap() + .clone(); + + println!( + "Bob is accepting the bet and addiing {:?} sats to the contract", + amount + ); + bob_dlc.accept_bet(&wallet, &bet).await.unwrap(); + + let attestation_event = nostr_events::lookup_attestation_event( + EventId::from_hex(bet.oracle_event_id.clone()).unwrap(), + &alice_dlc.nostr, + ) + .await + .unwrap() + .unwrap(); + + let attestation = utils::oracle_attestation_from_str(&attestation_event.content); + + println!("Winning outcome is {:?}", attestation.outcomes[0]); + + println!("Alice is settling the bet"); + + alice_dlc + .settle_bet(&wallet, &bet, attestation) + .await + .unwrap(); + + println!("Alice is claiming payout"); + + alice_dlc.claim_payout(&wallet, &bet).await.unwrap(); + + nostr_events::delete_all_dlc_offers(&bob_keys, &bob_dlc.nostr).await; + nostr_events::delete_all_dlc_offers(&alice_keys, &alice_dlc.nostr).await; + } + + #[tokio::test] + async fn test_create_and_post_offer() { + let multi_mint_wallet = initialize_wallets().await; + let wallet = multi_mint_wallet + .get_wallet(&WalletKey::new( + MintUrl::from_str(MINT_URL).unwrap(), + cdk::nuts::CurrencyUnit::Sat, + )) + .await + .unwrap(); + const ANNOUNCEMENT: &str = "ypyyyX6pdZUM+OovHftxK9StImd8F7nxmr/eTeyR/5koOVVe/EaNw1MAeJm8LKDV1w74Fr+UJ+83bVP3ynNmjwKbtJr9eP5ie2Exmeod7kw4uNsuXcw6tqJF1FXH3fTF/dgiOwAByEOAEd95715DKrSLVdN/7cGtOlSRTQ0/LsW/p3BiVOdlpccA/dgGDAACBDEyMzQENDU2NwR0ZXN0"; + let announcement = oracle_announcement_from_str(ANNOUNCEMENT); + let announcement_id = + EventId::from_hex("d30e6c857a900ebefbf7dc3b678ead9215f4345476067e146ded973971286529") + .unwrap(); + let keys = Keys::generate(); + let counterparty_keys = Keys::generate(); + + let dlc = DLC::new(&keys.secret_key()).await.unwrap(); + + let descriptor = &announcement.oracle_event.event_descriptor; + + let outcomes = match descriptor { + EventDescriptor::EnumEvent(ref e) => e.outcomes.clone(), + EventDescriptor::DigitDecompositionEvent(_) => unreachable!(), + }; + let outcome1 = &outcomes.clone()[0]; + + let amount = 7; + let _event_id = dlc + .create_bet( + &wallet, + announcement, + announcement_id, + counterparty_keys.public_key(), + vec![outcome1.clone()], + amount, + ) + .await + .unwrap(); + + let client = Client::new(&Keys::generate()); + let relay = "wss://relay.8333.space"; + client.add_relay(relay.to_string()).await.unwrap(); + client.connect().await; + + let offers = list_dlc_offers(&counterparty_keys, &client, None) // error line 74:58 in nostr_events.rs + .await + .unwrap(); // if event exists should unwrap to event + + println!("{:?}", offers); + + assert!(offers.len() >= 1); + + /* clean up */ + delete_all_dlc_offers(&keys, &client).await; + } + + #[tokio::test] + async fn test_dlc_status() { + let multi_mint_wallet = initialize_wallets().await; + let wallet = multi_mint_wallet + .get_wallet(&WalletKey::new( + MintUrl::from_str(MINT_URL).unwrap(), + cdk::nuts::CurrencyUnit::Sat, + )) + .await + .unwrap(); + + let dlc_root = + String::from("1a494a3792ef8084fc2d7ad71c5bddfbfacd8a5bd420d98c4d30f2ad15e03006"); + + let dlc_status = wallet.dlc_status(dlc_root.clone()).await.unwrap(); + println!("DLC status: {:?}", dlc_status); + assert!(dlc_status.settled); + } +} + +// ALICE: +// - pub: d71b2434429b0f038ed35e0e3827bca5e65b6d44d1af9344f73b20ff7ffa93dd +// - priv: b9452287c9e4cf53cf935adbc2341931c68c19d8447fe571ccc8dd9b5ed85584 +// BOB: +// - pub: b3e6ae1bdfa18106dafe4992b77149a38623662f78f5f60ee436e457f7965ee2 +// - priv: 4e111131d31ad92ed5d37ab87d5046efa730f192f9c8f9b59f6c61caad1f8933 + +// anouncement_ID: d30e6c857a900ebefbf7dc3b678ead9215f4345476067e146ded973971286529 diff --git a/crates/cdk-cli/src/sub_commands/dlc/nostr_events.rs b/crates/cdk-cli/src/sub_commands/dlc/nostr_events.rs new file mode 100644 index 000000000..e2d020a6d --- /dev/null +++ b/crates/cdk-cli/src/sub_commands/dlc/nostr_events.rs @@ -0,0 +1,222 @@ +use std::time::Duration; +use std::vec; + +use crate::sub_commands::dlc::UserBet; +use nostr_sdk::base64::prelude::*; +use nostr_sdk::event::builder::Error; +use nostr_sdk::nips::nip04; +use nostr_sdk::{ + Client, Event, EventBuilder, EventId, EventSource, Filter, Keys, Kind, PublicKey, Tag, +}; + +/// Create Kind 8_888 event tagged with the counterparty pubkey +/// +/// see https://github.com/nostr-protocol/nips/blob/9157321a224bca77b3472a19de72885af9d6a91d/88.md#kind8_888 +/// +/// # Arguments +/// * `keys` - The Keys used to sign the event +/// * `msg` - The dlc message +/// * `counterparty_pubkey` - Public key to send this message to +pub fn create_dlc_msg_event( + keys: &Keys, + msg: String, + counterparty_pubkey: &PublicKey, +) -> Result { + // The DLC message is first serialized in binary, and then encrypted with NIP04. + let content = BASE64_STANDARD.encode(msg); + + let content: String = nip04::encrypt(&keys.secret_key(), counterparty_pubkey, content)?; + + EventBuilder::new( + Kind::Custom(8888), + content, + vec![Tag::public_key(*counterparty_pubkey)], + ) + .to_event(keys) +} + +pub async fn lookup_announcement_event( + event_id: EventId, + client: &Client, +) -> Option> { + let filter = Filter::new().id(event_id).kind(Kind::Custom(88)); + let events = client + .get_events_of( + vec![filter], + EventSource::both(Some(Duration::from_secs(10))), + ) + .await + .expect("get_events_of failed"); + if events.is_empty() { + return None; + } + Some(Ok(events.first().unwrap().clone())) +} + +pub async fn list_dlc_offers( + keys: &Keys, + client: &Client, + event_id: Option, +) -> Option> { + let filter = if let Some(event_id) = event_id { + Filter::new().id(event_id) + } else { + Filter::new() + .kind(Kind::Custom(8888)) + .pubkey(keys.public_key()) + }; + let events = client + .get_events_of( + vec![filter], + EventSource::both(Some(Duration::from_secs(10))), + ) + .await + .expect("get_events_of failed"); + + if events.is_empty() { + return None; + } + + let offers = events + .iter() + .map(|e| { + let decrypted = + nostr_sdk::nips::nip04::decrypt(keys.secret_key(), &e.pubkey, e.content.clone()) + .unwrap(); + + let decoded = BASE64_STANDARD.decode(&decrypted).unwrap(); + let decoded_str = std::str::from_utf8(&decoded).unwrap(); + serde_json::from_str::(decoded_str).unwrap() + }) + .collect(); + Some(offers) +} + +/// Used to reset the state of our offers on the relays in case we change types of UserBet +pub async fn delete_all_dlc_offers(keys: &Keys, client: &Client) -> Option> { + let filter = Filter::new() + .kind(Kind::Custom(8888)) + .author(keys.public_key()); + let events = client + .get_events_of( + vec![filter], + EventSource::both(Some(Duration::from_secs(10))), + ) + .await + .expect("get_events_of failed"); + + if events.is_empty() { + return None; + } + + let mut deleted: Vec = Vec::new(); + + for event in events { + let out = client.delete_event(event.id).await.unwrap(); + if out.success.len() > 0 { + deleted.push(event.id); + } + } + Some(deleted) +} + +/// Lookup the attestation event for a given announcement event id +pub async fn lookup_attestation_event( + announcement_event_id: EventId, + client: &Client, +) -> Option> { + let filter = Filter::new() + .event(announcement_event_id) + .kind(Kind::Custom(89)); + let events = client + .get_events_of( + vec![filter], + EventSource::both(Some(Duration::from_secs(10))), + ) + .await + .unwrap(); + + if events.len() != 1 { + return None; + } + + Some(Ok(events.first().unwrap().clone())) +} + +#[cfg(test)] +mod tests { + use super::*; + use nostr_sdk::{Client, EventId, Keys}; + + #[tokio::test] + async fn test_lookup_announcement_event() { + let announcement_id = + EventId::from_hex("d30e6c857a900ebefbf7dc3b678ead9215f4345476067e146ded973971286529") + .unwrap(); + + let client = Client::new(&Keys::generate()); + //let relay = "wss://relay.damus.io"; + let relay = "relay.nostrdice.com"; + client.add_relay(relay.to_string()).await.unwrap(); + client.connect().await; + let event = lookup_announcement_event(announcement_id, &client) + .await + .unwrap() + .unwrap(); + assert_eq!(event.id, announcement_id); + } + + #[test] + fn test_create_dlc_message_event() { + let keys = Keys::parse("4e111131d31ad92ed5d37ab87d5046efa730f192f9c8f9b59f6c61caad1f8933") + .unwrap(); + let counterparty_pubkey = Keys::generate().public_key(); + let msg = String::from("hello"); + + let msg = BASE64_STANDARD.encode(msg); + + let event = create_dlc_msg_event(&keys, msg.clone(), &counterparty_pubkey).unwrap(); + + assert_eq!(keys.public_key(), event.pubkey); + assert_eq!(Kind::Custom(8888), event.kind); + println!("{:?}", event) + } + + #[tokio::test] + async fn test_list_dlc_offers() { + let keys = Keys::generate(); + let counterparty_privkey = Keys::generate(); + let counterparty_pubkey = counterparty_privkey.public_key(); + let msg = String::from("hello"); + let msg = BASE64_STANDARD.encode(msg); + + let event = create_dlc_msg_event(&keys, msg.clone(), &counterparty_pubkey).unwrap(); + + let client = Client::new(&Keys::generate()); + //let relay = "wss://relay.damus.io"; + let relay = "relay.nostrdice.com"; + client.add_relay(relay.to_string()).await.unwrap(); + client.connect().await; + + let event_id = client.send_event(event).await.unwrap(); + + println!("event id: {:?}", event_id.to_hex()); + + let offers = list_dlc_offers(&counterparty_privkey, &client, None) + .await + .unwrap(); // error in line 74:58 + + assert!(offers.len() >= 1); + + /* clean up */ + delete_all_dlc_offers(&keys, &client).await; + } + + #[test] + fn test_deserialize_from_string() { + let str ="{\"id\":7,\"oracle_announcement\":{\"announcementSignature\":\"ca9cb2c97ea975950cf8ea2f1dfb712bd4ad22677c17b9f19abfde4dec91ff992839555efc468dc353007899bc2ca0d5d70ef816bf9427ef376d53f7ca73668f\",\"oraclePublicKey\":\"029bb49afd78fe627b613199ea1dee4c38b8db2e5dcc3ab6a245d455c7ddf4c5\",\"oracleEvent\":{\"oracleNonces\":[\"c8438011df79ef5e432ab48b55d37fedc1ad3a54914d0d3f2ec5bfa7706254e7\"],\"eventMaturityEpoch\":1705363200,\"eventDescriptor\":{\"enumEvent\":{\"outcomes\":[\"1234\",\"4567\"]}},\"eventId\":\"test\"}},\"oracle_event_id\":\"d30e6c857a900ebefbf7dc3b678ead9215f4345476067e146ded973971286529\",\"user_outcomes\":[\"1234\",\"4567\"],\"blinding_factor\":\"54333ffa98687d4e7dc46e480deb6c4093ce6fe9a9bfef8a1f5e6950d25e1c14\",\"dlc_root\":\"96e0a0737aaae1a83e389300ffea9eb9a571038719d6ff2fb25fb40144998bf2\",\"timeout\":1705366800}"; + let bet = serde_json::from_str::(str).unwrap(); + + println!("{:?}", bet); + } +} diff --git a/crates/cdk-cli/src/sub_commands/dlc/utils.rs b/crates/cdk-cli/src/sub_commands/dlc/utils.rs new file mode 100644 index 000000000..25f5c9347 --- /dev/null +++ b/crates/cdk-cli/src/sub_commands/dlc/utils.rs @@ -0,0 +1,68 @@ +use dlc::secp256k1_zkp::hashes::hex::FromHex; +use dlc_messages::oracle_msgs::{OracleAnnouncement, OracleAttestation}; +use lightning::util::ser::Readable; +use nostr_sdk::base64::prelude::*; +use std::io::Cursor; + +fn decode_bytes(str: &str) -> Result, nostr_sdk::base64::DecodeError> { + match FromHex::from_hex(str) { + Ok(bytes) => Ok(bytes), + Err(_) => Ok(BASE64_STANDARD.decode(str)?), + } +} + +/// Parses a string into an oracle announcement. +pub fn oracle_announcement_from_str(str: &str) -> OracleAnnouncement { + let bytes = decode_bytes(str).expect("Could not decode oracle announcement string"); + let mut cursor = Cursor::new(bytes); + + OracleAnnouncement::read(&mut cursor).expect("Could not parse oracle announcement") +} + +/// Parses a string into an oracle attestation. +pub fn oracle_attestation_from_str(str: &str) -> OracleAttestation { + let bytes = decode_bytes(str).expect("Could not decode oracle attestation string"); + let mut cursor = Cursor::new(bytes); + + OracleAttestation::read(&mut cursor).expect("Could not parse oracle attestation") +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + use dlc::secp256k1_zkp::schnorr::Signature; + use dlc_messages::oracle_msgs::EventDescriptor; + + const ANNOUNCEMENT: &str = "ypyyyX6pdZUM+OovHftxK9StImd8F7nxmr/eTeyR/5koOVVe/EaNw1MAeJm8LKDV1w74Fr+UJ+83bVP3ynNmjwKbtJr9eP5ie2Exmeod7kw4uNsuXcw6tqJF1FXH3fTF/dgiOwAByEOAEd95715DKrSLVdN/7cGtOlSRTQ0/LsW/p3BiVOdlpccA/dgGDAACBDEyMzQENDU2NwR0ZXN0"; + + #[test] + fn test_decode_oracle_announcement() { + let announcement = oracle_announcement_from_str(ANNOUNCEMENT); + println!("{:?}", announcement); + + assert_eq!( + announcement.announcement_signature, + Signature::from_str(&String::from("ca9cb2c97ea975950cf8ea2f1dfb712bd4ad22677c17b9f19abfde4dec91ff992839555efc468dc353007899bc2ca0d5d70ef816bf9427ef376d53f7ca73668f")).unwrap() + ); + + let descriptor = announcement.oracle_event.event_descriptor; + + match descriptor { + EventDescriptor::EnumEvent(e) => { + assert_eq!(e.outcomes.len(), 2); + } + EventDescriptor::DigitDecompositionEvent(..) => unreachable!(), + } + } + + #[test] + fn test_decode_oracle_attestation() { + let attestation = "f1d822d1b8bdddcfb07ea2890c11fb5682af346140cb9282365b0e4db950b6370001935e4441edce5bce4970b306bcb90f887a5dc0e01296869c988f83b2026b34efc3ce0d8cebda6af9338c7dbb46d2f47e2c131cff58926e2254d67b12979c48010001086f7574636f6d6531"; + let attestation = oracle_attestation_from_str(attestation); + + assert!(attestation.signatures.len() == 1); + assert!(attestation.outcomes.len() == 1); + } +} diff --git a/crates/cdk-cli/src/sub_commands/mod.rs b/crates/cdk-cli/src/sub_commands/mod.rs index 8256d0aea..6cf457ab6 100644 --- a/crates/cdk-cli/src/sub_commands/mod.rs +++ b/crates/cdk-cli/src/sub_commands/mod.rs @@ -4,6 +4,7 @@ pub mod check_spent; pub mod create_request; pub mod decode_request; pub mod decode_token; +pub mod dlc; pub mod list_mint_proofs; pub mod melt; pub mod mint; diff --git a/crates/cdk-cli/src/sub_commands/send.rs b/crates/cdk-cli/src/sub_commands/send.rs index f5bcf3e57..0bc7da45d 100644 --- a/crates/cdk-cli/src/sub_commands/send.rs +++ b/crates/cdk-cli/src/sub_commands/send.rs @@ -112,6 +112,7 @@ pub async fn send( refund_keys, sub_command_args.required_sigs, None, + None, ) .unwrap(); @@ -147,6 +148,7 @@ pub async fn send( refund_keys, sub_command_args.required_sigs, None, + None, ) .unwrap(); diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 0699cf7f7..807834ae5 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -338,6 +338,14 @@ impl Mint { Kind::HTLC => { proof.verify_htlc()?; } + + Kind::DLC => { + todo!() + } + + Kind::SCT => { + todo!() + } } } diff --git a/crates/cdk/src/nuts/mod.rs b/crates/cdk/src/nuts/mod.rs index 07518bff1..e7291ca73 100644 --- a/crates/cdk/src/nuts/mod.rs +++ b/crates/cdk/src/nuts/mod.rs @@ -19,6 +19,8 @@ pub mod nut13; pub mod nut14; pub mod nut15; pub mod nut18; +pub mod nutdlc; +pub mod nutsct; pub use nut00::{ BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, PreMint, PreMintSecrets, Proof, diff --git a/crates/cdk/src/nuts/nut00/mod.rs b/crates/cdk/src/nuts/nut00/mod.rs index d6c3dffdb..7dee746b8 100644 --- a/crates/cdk/src/nuts/nut00/mod.rs +++ b/crates/cdk/src/nuts/nut00/mod.rs @@ -19,6 +19,8 @@ use crate::nuts::nut01::{PublicKey, SecretKey}; use crate::nuts::nut11::{serde_p2pk_witness, P2PKWitness}; use crate::nuts::nut12::BlindSignatureDleq; use crate::nuts::nut14::{serde_htlc_witness, HTLCWitness}; +use crate::nuts::nutdlc::{serde_dlc_witness, DLCWitness}; +use crate::nuts::nutsct::{serde_sct_witness, SCTWitness}; use crate::nuts::{Id, ProofDleq}; use crate::secret::Secret; use crate::Amount; @@ -189,6 +191,13 @@ pub enum Witness { /// HTLC Witness #[serde(with = "serde_htlc_witness")] HTLCWitness(HTLCWitness), + /// DLC Witness + #[serde(with = "serde_dlc_witness")] + DLCWitness(DLCWitness), + + /// SCT Witness + #[serde(with = "serde_sct_witness")] // TODO: change this to sct + SCTWitness(SCTWitness), } impl Witness { @@ -203,6 +212,8 @@ impl Witness { sigs }); } + Self::DLCWitness(_) => todo!(), + Witness::SCTWitness(_) => todo!(), } } @@ -211,6 +222,8 @@ impl Witness { match self { Self::P2PKWitness(witness) => Some(witness.signatures.clone()), Self::HTLCWitness(witness) => witness.signatures.clone(), + Self::DLCWitness(_) => todo!(), + Witness::SCTWitness(_) => todo!(), } } @@ -219,6 +232,8 @@ impl Witness { match self { Self::P2PKWitness(_witness) => None, Self::HTLCWitness(witness) => Some(witness.preimage.clone()), + Self::DLCWitness(_) => todo!(), + Witness::SCTWitness(_) => todo!(), } } } diff --git a/crates/cdk/src/nuts/nut10.rs b/crates/cdk/src/nuts/nut10.rs index 541095c28..148196cf0 100644 --- a/crates/cdk/src/nuts/nut10.rs +++ b/crates/cdk/src/nuts/nut10.rs @@ -26,6 +26,10 @@ pub enum Kind { P2PK, /// NUT-14 HTLC HTLC, + /// NUT-dlc + DLC, + /// NUT-SCT + SCT, } /// Secert Date diff --git a/crates/cdk/src/nuts/nut11/mod.rs b/crates/cdk/src/nuts/nut11/mod.rs index 6d407e2b1..a780aeb70 100644 --- a/crates/cdk/src/nuts/nut11/mod.rs +++ b/crates/cdk/src/nuts/nut11/mod.rs @@ -18,6 +18,7 @@ use thiserror::Error; use super::nut00::Witness; use super::nut01::PublicKey; +use super::nutdlc::DLCRoot; #[cfg(feature = "mint")] use super::Proofs; use super::{Kind, Nut10Secret, Proof, SecretKey}; @@ -281,6 +282,20 @@ pub enum SpendingConditions { /// Additional Optional Spending [`Conditions`] conditions: Option, }, + + /// NUT-DLC Spending conditions + DLCConditions { + /// dlc root + data: String, + /// Additional Optional Spending [`Conditions`] + conditions: Option, + }, + + /// NUT-SCT Spending conditions + SCTConditions { + /// Merkle root hash of spending conditions + data: String, + }, } impl SpendingConditions { @@ -302,11 +317,51 @@ impl SpendingConditions { } } + /// New DLC [SpendingConditions] + pub fn new_dlc(dlc_root: &DLCRoot, conditions: Option) -> Self { + Self::DLCConditions { + data: dlc_root.to_string(), + conditions, + } + } + + /// New SCT [SpendingConditions] + pub fn new_sct(merkle_root: [u8; 32]) -> Self { + Self::SCTConditions { + data: hex::encode(merkle_root), + } + } + + /// New SCT [SpendingConditions] for a DLC + pub fn new_dlc_sct(secrets: Vec, secret_to_prove: usize) -> (Self, Vec) { + let leaf_hashes = crate::nuts::nutsct::sct_leaf_hashes(secrets.clone()); + let proof = crate::nuts::nutsct::merkle_prove(leaf_hashes.clone(), secret_to_prove); + let root = crate::nuts::nutsct::merkle_root(&leaf_hashes); + + let expected_proof = vec![Sha256Hash::hash(&secrets[1].to_bytes()).to_byte_array()]; + assert_eq!(proof, expected_proof); + + let proof = proof + .iter() + .map(|h| hex::encode(h)) + .collect::>(); + + let valid = + crate::nuts::nutsct::merkle_verify(&root, &leaf_hashes[secret_to_prove], &proof); + assert!(valid); + + let sct_conditions = SpendingConditions::new_sct(root); + + (sct_conditions, proof) + } + /// Kind of [SpendingConditions] pub fn kind(&self) -> Kind { match self { Self::P2PKConditions { .. } => Kind::P2PK, Self::HTLCConditions { .. } => Kind::HTLC, + Self::DLCConditions { .. } => Kind::DLC, + Self::SCTConditions { .. } => Kind::SCT, } } @@ -315,6 +370,8 @@ impl SpendingConditions { match self { Self::P2PKConditions { conditions, .. } => conditions.as_ref().and_then(|c| c.num_sigs), Self::HTLCConditions { conditions, .. } => conditions.as_ref().and_then(|c| c.num_sigs), + Self::DLCConditions { .. } => todo!(), + Self::SCTConditions { .. } => todo!(), } } @@ -330,6 +387,8 @@ impl SpendingConditions { Some(pubkeys) } Self::HTLCConditions { conditions, .. } => conditions.clone().and_then(|c| c.pubkeys), + Self::DLCConditions { .. } => todo!(), + Self::SCTConditions { .. } => todo!(), } } @@ -338,6 +397,8 @@ impl SpendingConditions { match self { Self::P2PKConditions { conditions, .. } => conditions.as_ref().and_then(|c| c.locktime), Self::HTLCConditions { conditions, .. } => conditions.as_ref().and_then(|c| c.locktime), + Self::DLCConditions { .. } => todo!(), + Self::SCTConditions { .. } => todo!(), } } @@ -350,6 +411,9 @@ impl SpendingConditions { Self::HTLCConditions { conditions, .. } => { conditions.clone().and_then(|c| c.refund_keys) } + + Self::DLCConditions { .. } => todo!(), + Self::SCTConditions { .. } => todo!(), } } } @@ -376,6 +440,13 @@ impl TryFrom for SpendingConditions { .map_err(|_| Error::InvalidHash)?, conditions: secret.secret_data.tags.and_then(|t| t.try_into().ok()), }), + Kind::DLC => Ok(Self::DLCConditions { + data: DLCRoot::from_str(&secret.secret_data.data)?.to_string(), + conditions: secret.secret_data.tags.and_then(|t| t.try_into().ok()), + }), + Kind::SCT => Ok(Self::SCTConditions { + data: secret.secret_data.data, + }), } } } @@ -389,6 +460,12 @@ impl From for super::nut10::Secret { SpendingConditions::HTLCConditions { data, conditions } => { super::nut10::Secret::new(Kind::HTLC, data.to_string(), conditions) } + SpendingConditions::DLCConditions { data, conditions } => { + super::nut10::Secret::new(Kind::DLC, data.to_string(), conditions) + } + SpendingConditions::SCTConditions { data } => { + super::nut10::Secret::new(Kind::SCT, data.to_string(), None::) + } } } } @@ -414,6 +491,9 @@ pub struct Conditions { /// /// Default [`SigFlag::SigInputs`] pub sig_flag: SigFlag, + /// DLC funding threshold + #[serde(skip_serializing_if = "Option::is_none")] + pub threshold: Option, } impl Conditions { @@ -424,6 +504,7 @@ impl Conditions { refund_keys: Option>, num_sigs: Option, sig_flag: Option, + threshold: Option, ) -> Result { if let Some(locktime) = locktime { if locktime.lt(&unix_time()) { @@ -437,6 +518,7 @@ impl Conditions { refund_keys, num_sigs, sig_flag: sig_flag.unwrap_or_default(), + threshold, }) } } @@ -448,6 +530,7 @@ impl From for Vec> { refund_keys, num_sigs, sig_flag, + threshold, } = conditions; let mut tags = Vec::new(); @@ -468,6 +551,10 @@ impl From for Vec> { tags.push(Tag::Refund(refund_keys).as_vec()) } tags.push(Tag::SigFlag(sig_flag).as_vec()); + if let Some(threshold) = threshold { + tags.push(Tag::DLCThreshold(threshold).as_vec()); + } + tags } } @@ -522,12 +609,22 @@ impl TryFrom>> for Conditions { None }; + let threshold = if let Some(tag) = tags.get(&TagKind::DLCThreshold) { + match tag { + Tag::DLCThreshold(threshold) => Some(*threshold), + _ => None, + } + } else { + None + }; + Ok(Conditions { locktime, pubkeys, refund_keys, num_sigs, sig_flag, + threshold, }) } } @@ -547,6 +644,9 @@ pub enum TagKind { Refund, /// Pubkey Pubkeys, + /// DLC funding threshold + #[serde(rename = "threshold")] + DLCThreshold, /// Custom tag kind Custom(String), } @@ -559,6 +659,7 @@ impl fmt::Display for TagKind { Self::Locktime => write!(f, "locktime"), Self::Refund => write!(f, "refund"), Self::Pubkeys => write!(f, "pubkeys"), + Self::DLCThreshold => write!(f, "threshold"), Self::Custom(kind) => write!(f, "{}", kind), } } @@ -575,6 +676,7 @@ where "locktime" => Self::Locktime, "refund" => Self::Refund, "pubkeys" => Self::Pubkeys, + "threshold" => Self::DLCThreshold, t => Self::Custom(t.to_owned()), } } @@ -683,6 +785,8 @@ pub enum Tag { Refund(Vec), /// Pubkeys [`Tag`] PubKeys(Vec), + /// DLC funding threshold [`Tag`] + DLCThreshold(u64), } impl Tag { @@ -694,6 +798,7 @@ impl Tag { Self::LockTime(_) => TagKind::Locktime, Self::Refund(_) => TagKind::Refund, Self::PubKeys(_) => TagKind::Pubkeys, + Self::DLCThreshold(_) => TagKind::DLCThreshold, } } @@ -737,6 +842,7 @@ where Ok(Self::PubKeys(pubkeys)) } + TagKind::DLCThreshold => Ok(Tag::DLCThreshold(tag[1].as_ref().parse()?)), _ => Err(Error::UnknownTag), } } @@ -763,6 +869,11 @@ impl From for Vec { } tag } + Tag::DLCThreshold(threshold) => { + let mut tag = vec![TagKind::DLCThreshold.to_string()]; + tag.push(threshold.to_string()); + tag + } } } } @@ -826,6 +937,7 @@ mod tests { .unwrap()]), num_sigs: Some(2), sig_flag: SigFlag::SigAll, + threshold: None, }; let secret: Nut10Secret = Nut10Secret::new(Kind::P2PK, data.to_string(), Some(conditions)); @@ -860,6 +972,7 @@ mod tests { refund_keys: Some(vec![v_key]), num_sigs: Some(2), sig_flag: SigFlag::SigInputs, + threshold: None, }; let secret: Secret = Nut10Secret::new(Kind::P2PK, v_key.to_string(), Some(conditions)) diff --git a/crates/cdk/src/nuts/nutdlc/mod.rs b/crates/cdk/src/nuts/nutdlc/mod.rs new file mode 100644 index 000000000..18173eeda --- /dev/null +++ b/crates/cdk/src/nuts/nutdlc/mod.rs @@ -0,0 +1,480 @@ +//! NUT-DLC: Discrete Log Contracts +//! +//! https://github.com/cashubtc/nuts/blob/a86a4e8ce0b9a76ce9b242d6c2c2ab846b3e1955/dlc.md + +use std::{collections::HashMap, str::FromStr}; + +use bitcoin::hashes::sha256::Hash as Sha256Hash; +use bitcoin::hashes::Hash; + +use super::nut00::Witness; +use super::{nut00::token::TokenV3Token, nut01::PublicKey, Proof, Proofs}; +use super::{nut10, BlindSignature, BlindedMessage, CurrencyUnit, Nut10Secret, SecretData}; +use crate::util::hex; +use crate::Amount; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::json; +use thiserror::Error; + +pub mod serde_dlc_witness; + +#[derive(Debug, Error)] +/// Errors for DLC +pub enum Error {} + +/// DLC Witness +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct DLCWitness { + /// DLC Secret + pub dlc_secret: SecretData, +} + +impl Proof { + /// Add DLC witness to proof + pub fn add_dlc_witness(&mut self, dlc_secret: Nut10Secret) { + let secret_data = match dlc_secret.kind { + nut10::Kind::DLC => dlc_secret.secret_data, + _ => todo!("this should error"), + }; + self.witness = Some(Witness::DLCWitness(DLCWitness { + dlc_secret: secret_data, + })); + } +} + +// Ti == SHA256(Ki_ || Pi) +#[derive(Clone, Debug)] +/// DLC leaf corresponding to a single outcome +pub struct DLCLeaf { + /// Blinded locking point - Ki_ = Ki + b*G + pub blinded_locking_point: PublicKey, // TODO: is this the right type to use? + /// Payouts for this outcome + pub payout: PayoutStructure, // JSON-encoded payout structure +} + +impl DLCLeaf { + /// SHA256(Ki_ || Pi) + pub fn hash(&self) -> [u8; 32] { + // Convert blinded_locking_point to bytes + let point_bytes = self.blinded_locking_point.to_bytes().to_vec(); + + // Concatenate point_bytes and payout string + let mut input = point_bytes; + input.extend(self.payout.as_bytes()); + + // Compute SHA256 hash + Sha256Hash::hash(&input).to_byte_array() + } +} + +// Tt = SHA256(hash_to_curve(t.to_bytes(8, 'big')) || Pt) +/// DLC leaf for the timeout condition +pub struct DLCTimeoutLeaf { + /// H2C of timeout + timeout_hash: PublicKey, + /// Payout structure for the timeout + payout: PayoutStructure, +} + +impl DLCTimeoutLeaf { + /// Create new [`DLCTimeoutLeaf`] + pub fn new(timeout: &u64, payout: &PayoutStructure) -> Self { + let timeout_hash = crate::dhke::hash_to_curve(&timeout.to_be_bytes()) + .expect("error calculating timeout hash"); + + Self { + timeout_hash, + payout: payout.clone(), + } + } + + /// SHA256(hash_to_curve(timeout) || Pt) + pub fn hash(&self) -> [u8; 32] { + let mut input = self.timeout_hash.to_bytes().to_vec(); + input.extend(self.payout.as_bytes()); + Sha256Hash::hash(&input).to_byte_array() + } +} + +/// Hash of all spending conditions and blinded locking points +#[derive(Serialize, Deserialize, Debug)] +pub struct DLCRoot([u8; 32]); + +impl DLCRoot { + /// new [`DLCRoot`] from [`DLCLeaf`]s and optional [`DLCTimeoutLeaf`] + pub fn compute(leaves: Vec, timeout_leaf: Option) -> Self { + let mut input: Vec<[u8; 32]> = Vec::new(); + for leaf in leaves { + input.push(leaf.hash()); + } + if let Some(timeout_leaf) = timeout_leaf { + input.push(timeout_leaf.hash()); + } + Self { + 0: crate::nuts::nutsct::merkle_root(&input), + } + } + + /// Convert to bytes + pub fn to_bytes(&self) -> [u8; 32] { + self.0 + } +} + +impl ToString for DLCRoot { + fn to_string(&self) -> String { + hex::encode(self.0) + } +} + +impl FromStr for DLCRoot { + type Err = crate::nuts::nut11::Error; + + fn from_str(s: &str) -> Result { + let bytes = hex::decode(s).map_err(|_| crate::nuts::nut11::Error::InvalidHash)?; + if bytes.len() != 32 { + return Err(crate::nuts::nut11::Error::InvalidHash); + } + let mut array = [0u8; 32]; + array.copy_from_slice(&bytes); + Ok(DLCRoot(array)) + } +} + +// struct DLCMerkleTree { +// root: DLCRoot, +// leaves: Vec, +// timeout_leaf: Option, +// } + +// NOTE: copied from nut00/token.rs TokenV3, should it be V3 or V4? +/// DLC Funding Token +pub struct DLCFundingToken { + /// Proofs in [`Token`] by mint + pub token: Vec, + /// Memo for token + // #[serde(skip_serializing_if = "Option::is_none")] + pub memo: Option, + /// Token Unit + // #[serde(skip_serializing_if = "Option::is_none")] + pub unit: Option, + /// DLC Root + pub dlc_root: DLCRoot, +} + +#[derive(Serialize, Deserialize, Debug)] +/// DLC +pub struct DLC { + /// DLC Root + pub dlc_root: String, + + /// Amount of funds locked in the contract + pub funding_amount: Amount, + + /// unit of the contract + pub unit: CurrencyUnit, + + /// Proofs funding the DLC + pub inputs: Proofs, // locked with DLC secret - only spendable in this DLC +} + +/// see https://github.com/cashubtc/nuts/blob/a86a4e8ce0b9a76ce9b242d6c2c2ab846b3e1955/dlc.md#mint-registration +#[derive(Serialize, Deserialize, Debug)] +/// POST request body for /v1/dlc/fund +pub struct PostDLCRegistrationRequest { + /// DLCs to register + pub registrations: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +/// Successfully funded DLC +pub struct FundedDLC { + /// DLC Root + pub dlc_root: String, + /// [`FundingProof`] from mint + pub funding_proof: FundingProof, +} + +// +#[derive(Serialize, Deserialize, Debug)] +/// Proof from the mint that the DLC was funded +/// +/// see https://github.com/cashubtc/nuts/blob/a86a4e8ce0b9a76ce9b242d6c2c2ab846b3e1955/dlc.md#funding-proofs +pub struct FundingProof { + /// Keyset Id + pub keyset: String, + ///BIP-340 signature of DLC root and funding amount + pub signature: String, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(untagged)] +/// [`DLCRegistrationResponse`] can be either a success or an error +pub enum DLCRegistrationResponse { + /// Success [`DLCRegistrationResponse`] + Success { + /// successfully [`FundedDLC`]s + funded: Vec, + }, + /// Error [`DLCRegistrationResponse`] + Error { + /// successfully [`FundedDLC`]s + funded: Vec, + /// [`DLCError`]s for inputs that failed to register + errors: Vec, + }, +} + +#[derive(Serialize, Deserialize, Debug)] +/// Error for [`DLCRegistrationResponse`] +pub struct DLCError { + /// DLC Root + pub dlc_root: String, + /// [`BadInput`]s + pub bad_inputs: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +/// [`BadInput`] for [`DLCError`] +pub struct BadInput { + /// Index of the input that failed + pub index: u32, + /// Detail of the error + pub detail: String, +} + +#[derive(Clone, Debug)] +/// serialized dictionaries which map `xonly_pubkey -> weight` +/// +/// see https://github.com/cashubtc/nuts/blob/a86a4e8ce0b9a76ce9b242d6c2c2ab846b3e1955/dlc.md#payout-structures +pub struct PayoutStructure(HashMap); + +impl PayoutStructure { + /// Create new [`PayoutStructure`] with a single payout + pub fn default(pubkey: String) -> Self { + let pubkey = if pubkey.len() == 64 { + // this way we can use nostr keys + format!("02{}", pubkey) + } else { + pubkey + }; + let pubkey = PublicKey::from_str(&pubkey).unwrap(); + Self(HashMap::from([(pubkey, 1)])) + } + + /// Create new [`PayoutStructure`] with even weight to all pubkeys + pub fn default_timeout(mut pubkeys: Vec) -> Self { + let mut payout = HashMap::new(); + pubkeys.sort(); // Sort pubkeys before creating hashmap + for pubkey in pubkeys { + let pubkey = if pubkey.len() == 64 { + format!("02{}", pubkey) + } else { + pubkey + }; + let pubkey = PublicKey::from_str(&pubkey).unwrap(); + payout.insert(pubkey, 1); + } + Self(payout) + } + + /// Convert the PayoutStructure to a byte representation + pub fn as_bytes(&self) -> Vec { + // Create sorted vector of entries + let mut entries: Vec<_> = self.0.iter().collect(); + entries.sort_by_key(|(pubkey, _)| pubkey.to_string()); + + // Create ordered map and serialize + let mut map = serde_json::Map::new(); + for (pubkey, amount) in entries { + map.insert(pubkey.to_string(), json!(*amount)); + } + + // NOTE: using json so it matches what happens in python + + let json_string = + serde_json::to_string(&map).expect("Failed to serialize PayoutStructure to JSON"); + + json_string.into_bytes() + } +} + +impl Serialize for PayoutStructure { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serde_json::Map::new(); + for (pubkey, amount) in &self.0 { + map.insert(pubkey.to_string(), json!(*amount)); + } + let json_string = serde_json::to_string(&map).map_err(serde::ser::Error::custom)?; + serializer.serialize_str(&json_string) + } +} + +impl<'de> Deserialize<'de> for PayoutStructure { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let temp_map: HashMap = + serde_json::from_str(&s).map_err(serde::de::Error::custom)?; + + let mut map = HashMap::new(); + for (key_str, value) in temp_map { + let pubkey = PublicKey::from_str(&key_str) + .map_err(|e| serde::de::Error::custom(format!("Invalid public key: {}", e)))?; + map.insert(pubkey, value); + } + + Ok(PayoutStructure(map)) + } +} + +#[derive(Serialize, Deserialize, Debug)] +/// DLC outcome +pub struct DLCOutcome { + #[serde(rename = "k")] + /// see https://github.com/cashubtc/nuts/blob/a86a4e8ce0b9a76ce9b242d6c2c2ab846b3e1955/dlc.md#payout-structures + pub blinded_attestation_secret: String, + #[serde(rename = "P")] + /// [`PayoutStructure`] for this outcome + pub payout_structure: PayoutStructure, +} + +#[derive(Serialize, Deserialize, Debug)] +/// Settled DLC +pub struct DLCSettlement { + /// DLC Root + pub dlc_root: String, + /// [`DLCOutcome`] for this settlement + pub outcome: DLCOutcome, + /// Mekrle proof that the outcome is in the root + pub merkle_proof: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +/// POST request body for /v1/dlc/settle +pub struct PostSettleDLCRequest { + /// [`DLCSettlement`]s to settle + pub settlements: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +/// Error for [`DLCSettlement`] +pub struct DLCSettlementError { + /// DLC Root + dlc_root: String, + /// Detail of the error + detail: String, +} + +#[derive(Serialize, Deserialize, Debug)] +/// Settled DLC +pub struct SettledDLC { + /// DLC Root + pub dlc_root: String, +} + +#[derive(Serialize, Deserialize, Debug)] +/// Response for /v1/dlc/settle +pub struct SettleDLCResponse { + /// Settled DLCs + pub settled: Vec, + /// Errors + pub errors: Option>, +} + +/// Response for /v1/dlc/status/{dlc_root} +#[derive(Serialize, Deserialize, Debug)] +pub struct DLCStatusResponse { + /// Whether the DLC is settled + pub settled: bool, + /// If not settled + pub funding_amount: Option, + /// If settled + pub debts: Option>, + /// Unit + pub unit: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +/// Witness to prove ownership of pubkey in [`ClaimDLCPayout`] +pub struct DLCPayoutWitness { + /// discrete log (private key) of `Payout.pubkey` (either parity) + pub secret: Option, + /// BIP-340 signature made by `Payout.pubkey` on `Payout.dlc_root` + pub signature: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +/// ClaimDLCPayout +pub struct ClaimDLCPayout { + /// DLC root hash + pub dlc_root: String, + /// Public key of the payout + pub pubkey: String, + /// Blinded outputs to be signed + pub outputs: Vec, + /// [`DLCPayoutWitness`] + pub witness: DLCPayoutWitness, +} + +#[derive(Serialize, Deserialize, Debug)] +/// Request for /v1/dlc/payout +pub struct PostDLCPayoutRequest { + /// Payouts being claimed + pub payouts: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +/// Successful payout for a DLC +pub struct DLCPayout { + /// DLC root hash + pub dlc_root: String, + /// Blinded signatures on outputs + pub outputs: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +/// Error details for a failed DLC payout +pub struct DLCPayoutError { + /// DLC root hash + pub dlc_root: String, + /// Error details + pub detail: String, +} + +#[derive(Serialize, Deserialize, Debug)] +/// Response for /v1/dlc/payout +pub struct PostDLCPayoutResponse { + /// Successfully paid DLCs + pub paid: Vec, + /// Errors for failed payouts + pub errors: Option>, +} + +// Known Parameters +/* +- The number of possible outcomes `n` + +- An outcome blinding secret scalar `b` + +- A vector of `n` outcome locking points `[K1, K2, ... Kn]` + +- A vector of `n` payout structures `[P1, P2, ... Pn]` + +- A vector of `n` payout structures `[P1, P2, ... Pn]` + +- An optional timeout timestamp `t` and timeout payout structure `Pt` +*/ + +// b = random secret scalar +// SecretKey::generate() + +// blinding points +/* +Ki_ = Ki + b*G +*/ diff --git a/crates/cdk/src/nuts/nutdlc/serde_dlc_witness.rs b/crates/cdk/src/nuts/nutdlc/serde_dlc_witness.rs new file mode 100644 index 000000000..83f499c74 --- /dev/null +++ b/crates/cdk/src/nuts/nutdlc/serde_dlc_witness.rs @@ -0,0 +1,22 @@ +//! Serde utils for P2PK Witness + +use serde::{de, ser, Deserialize, Deserializer, Serializer}; + +use super::DLCWitness; + +/// Serialize [DLCWitness] as stringified JSON +pub fn serialize(x: &DLCWitness, s: S) -> Result +where + S: Serializer, +{ + s.serialize_str(&serde_json::to_string(&x).map_err(ser::Error::custom)?) +} + +/// Deserialize [DLCWitness] from stringified JSON +pub fn deserialize<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s: String = String::deserialize(deserializer)?; + serde_json::from_str(&s).map_err(de::Error::custom) +} diff --git a/crates/cdk/src/nuts/nutsct/mod.rs b/crates/cdk/src/nuts/nutsct/mod.rs new file mode 100644 index 000000000..5a5f92883 --- /dev/null +++ b/crates/cdk/src/nuts/nutsct/mod.rs @@ -0,0 +1,447 @@ +//! NUT-SCT: Spending Condition Tree +//! +//! https://github.com/cashubtc/nuts/blob/a86a4e8ce0b9a76ce9b242d6c2c2ab846b3e1955/sct.md + +pub mod serde_sct_witness; + +use bitcoin::hashes::sha256::Hash as Sha256Hash; +use bitcoin::hashes::Hash; +use serde::{Deserialize, Serialize}; + +use crate::secret::Secret; + +use super::{Proof, Witness}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +/// SCT Witness +pub struct SCTWitness { + /// Leaf secret being proven + leaf_secret: String, + /// Merkle proof of the leaf secret + merkle_proof: Vec, +} + +impl Proof { + /// Add SCT witness to proof + pub fn add_sct_witness(&mut self, leaf_secret: String, merkle_proof: Vec) { + self.witness = Some(Witness::SCTWitness(SCTWitness { + leaf_secret, + merkle_proof, + })); + } +} + +/// see https://github.com/cashubtc/nuts/blob/a86a4e8ce0b9a76ce9b242d6c2c2ab846b3e1955/dlc.md#payout-structures +pub fn sorted_merkle_hash(left: &[u8], right: &[u8]) -> [u8; 32] { + // sort the inputs + let (left, right) = if left < right { + (left, right) + } else { + (right, left) + }; + + // concatenate the inputs + let mut to_hash = Vec::new(); + to_hash.extend_from_slice(left); + to_hash.extend_from_slice(right); + + // hash the concatenated inputs + Sha256Hash::hash(&to_hash).to_byte_array() +} + +/// see https://github.com/cashubtc/nuts/blob/a86a4e8ce0b9a76ce9b242d6c2c2ab846b3e1955/sct.md#merkle_rootleaf_hashes-listbytes---bytes +pub fn merkle_root(leaf_hashes: &[[u8; 32]]) -> [u8; 32] { + if leaf_hashes.is_empty() { + return [0; 32]; + } else if leaf_hashes.len() == 1 { + return leaf_hashes[0].to_owned(); + } else { + let split = leaf_hashes.len() / 2; // TODO: will this round? + let left = merkle_root(&leaf_hashes[..split]); + let right = merkle_root(&leaf_hashes[split..]); + sorted_merkle_hash(&left, &right) + } +} + +/// see https://github.com/cashubtc/nuts/blob/a86a4e8ce0b9a76ce9b242d6c2c2ab846b3e1955/sct.md#merkle_verifyroot-bytes-leaf_hash-bytes-proof-listbytes---bool +pub fn merkle_verify(root: &[u8; 32], leaf_hash: &[u8; 32], proof: &Vec) -> bool { + let mut current_hash = *leaf_hash; + for branch_hash_hex in proof { + let branch_hash = crate::util::hex::decode(branch_hash_hex).expect("Invalid hex string"); + current_hash = sorted_merkle_hash(¤t_hash, &branch_hash); + } + + current_hash == *root +} + +/// see https://github.com/cashubtc/nuts/blob/a86a4e8ce0b9a76ce9b242d6c2c2ab846b3e1955/dlc.md#payout-structures +pub fn merkle_prove(leaf_hashes: Vec<[u8; 32]>, position: usize) -> Vec<[u8; 32]> { + if leaf_hashes.len() <= 1 { + return Vec::new(); + } + let split = leaf_hashes.len() / 2; + + if position < split { + let mut proof = merkle_prove(leaf_hashes[..split].to_vec(), position); + proof.push(merkle_root(&leaf_hashes[split..])); + return proof; + } else { + let mut proof = merkle_prove(leaf_hashes[split..].to_vec(), position - split); + proof.push(merkle_root(&leaf_hashes[..split])); + return proof; + } +} + +/// Merkle root of SCT +pub fn sct_root(secrets: Vec) -> [u8; 32] { + let leaf_hashes: Vec<[u8; 32]> = secrets + .iter() + .map(|s| Sha256Hash::hash(&s.to_bytes()).to_byte_array()) + .collect(); + + merkle_root(&leaf_hashes) +} + +/// Hashes of SCT leaves +pub fn sct_leaf_hashes(secrets: Vec) -> Vec<[u8; 32]> { + secrets + .iter() + .map(|s| Sha256Hash::hash(&s.as_bytes()).to_byte_array()) + .collect() +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use crate::util::hex; + + use super::*; + + //https://github.com/cashubtc/nuts/blob/a86a4e8ce0b9a76ce9b242d6c2c2ab846b3e1955/tests/sct-tests.md.md + #[test] + fn test_secret_hash() { + let s = "[\"P2PK\",{\"nonce\":\"ffd73b9125cc07cdbf2a750222e601200452316bf9a2365a071dd38322a098f0\",\"data\":\"028fab76e686161cc6daf78fea08ba29ce8895e34d20322796f35fec8e689854aa\",\"tags\":[[\"sigflag\",\"SIG_INPUTS\"]]}]"; + let secret = Secret::from_str(s).unwrap(); + println!("{:?}", secret.as_bytes()); + + let hasher = Sha256Hash::hash(secret.as_bytes()).to_byte_array(); + + let expected_hash: [u8; 32] = + hex::decode("b43b79ed408d4cc0aa75ad0a97ab21e357ff7ee027300fb573833c568431e808") + .unwrap() + .try_into() + .unwrap(); + + assert_eq!(hasher, expected_hash) + } + + #[test] + fn test_sct_root() { + let s1: [u8; 32] = + hex::decode("b43b79ed408d4cc0aa75ad0a97ab21e357ff7ee027300fb573833c568431e808") + .unwrap() + .try_into() + .unwrap(); + let s2: [u8; 32] = + hex::decode("6bad0d7d596cb9048754ee75daf13ee7e204c6e408b83ee67514369e3f8f3f96") + .unwrap() + .try_into() + .unwrap(); + let s3: [u8; 32] = + hex::decode("8da10ed117cad5e89c6131198ffe271166d68dff9ce961ff117bd84297133b77") + .unwrap() + .try_into() + .unwrap(); + let s4: [u8; 32] = + hex::decode("7ec5a236d308d2c2bf800d81d3e3df89cc98f4f937d0788c302d2754ba28166a") + .unwrap() + .try_into() + .unwrap(); + let s5: [u8; 32] = + hex::decode("e19353a94d1aaf56b150b1399b33cd4ef4096b086665945fbe96bd72c22097a7") + .unwrap() + .try_into() + .unwrap(); + let s6: [u8; 32] = + hex::decode("cc655b7103c8b999b3fc292484bcb5a526e2d0cbf951f17fd7670fc05b1ff947") + .unwrap() + .try_into() + .unwrap(); + let s7: [u8; 32] = + hex::decode("009ea9fae527f7914096da1f1ce2480d2e4cfea62480afb88da9219f1c09767f") + .unwrap() + .try_into() + .unwrap(); + + let leaf_hashes = &[s1, s2, s3, s4, s5, s6, s7]; + + let root = merkle_root(leaf_hashes); + + let expected_root: [u8; 32] = + hex::decode("71655cac0c83c6949169bcd6c82b309810138895f83b967089ffd9f64d109306") + .unwrap() + .try_into() + .unwrap(); + + assert_eq!(root, expected_root); + } + + #[test] + fn test_basic_merkle_proof() { + // Test merkle proof for tree with two nodes. Proof should be other hash. + let hash1: [u8; 32] = [9; 32]; + let hash2: [u8; 32] = [8; 32]; + let leaf_hashes = vec![hash1, hash2]; + + let position = 0; + let proof = merkle_prove(leaf_hashes.clone(), position); + let expected_proof = vec![hash2]; + assert_eq!(proof, expected_proof); + + let position = 1; + let proof = merkle_prove(leaf_hashes.clone(), position); + let expected_proof = vec![hash1]; + assert_eq!(proof, expected_proof); + + let proof = proof + .iter() + .map(|h| hex::encode(h)) + .collect::>(); + + let root = merkle_root(&leaf_hashes); + + let valid = merkle_verify(&root, &leaf_hashes[1], &proof); + assert!(valid); + } + + #[test] + fn test_complex_merkle_proof() { + let s1: [u8; 32] = + hex::decode("b43b79ed408d4cc0aa75ad0a97ab21e357ff7ee027300fb573833c568431e808") + .unwrap() + .try_into() + .unwrap(); + let s2: [u8; 32] = + hex::decode("6bad0d7d596cb9048754ee75daf13ee7e204c6e408b83ee67514369e3f8f3f96") + .unwrap() + .try_into() + .unwrap(); + let s3: [u8; 32] = + hex::decode("8da10ed117cad5e89c6131198ffe271166d68dff9ce961ff117bd84297133b77") + .unwrap() + .try_into() + .unwrap(); + let s4: [u8; 32] = + hex::decode("7ec5a236d308d2c2bf800d81d3e3df89cc98f4f937d0788c302d2754ba28166a") + .unwrap() + .try_into() + .unwrap(); + let s5: [u8; 32] = + hex::decode("e19353a94d1aaf56b150b1399b33cd4ef4096b086665945fbe96bd72c22097a7") + .unwrap() + .try_into() + .unwrap(); + let s6: [u8; 32] = + hex::decode("cc655b7103c8b999b3fc292484bcb5a526e2d0cbf951f17fd7670fc05b1ff947") + .unwrap() + .try_into() + .unwrap(); + let s7: [u8; 32] = + hex::decode("009ea9fae527f7914096da1f1ce2480d2e4cfea62480afb88da9219f1c09767f") + .unwrap() + .try_into() + .unwrap(); + + let s8: [u8; 32] = + hex::decode("7a56977edf9c299c1cfb14dfbeb2ab28d7b3d44b3c9cc6b7854f8a58acb3407d") + .unwrap() + .try_into() + .unwrap(); + let s9: [u8; 32] = + hex::decode("7de4c7c75c8082ed9a2124ce8f027ed9a60f2236b6f50c62748a220086ed367b") + .unwrap() + .try_into() + .unwrap(); + + let s10: [u8; 32] = + hex::decode("b43b79ed408d4cc0aa75ad0a97ab21e357ff7ee027300fb573833c568431e808") + .unwrap() + .try_into() + .unwrap(); + let s11: [u8; 32] = + hex::decode("7de4c7c75c8082ed9a2124ce8f027ed9a60f2236b6f50c62748a220086ed367b") + .unwrap() + .try_into() + .unwrap(); + + let leaf_hashes = &[s1, s2, s3, s4, s5, s6, s7]; + + let position = 0; + let proofs = merkle_prove(leaf_hashes.to_vec(), position); + let expected_proofs = [s8, s9].to_vec(); + assert_eq!(proofs, expected_proofs); + + let position = 1; + let expected_proofs = [s3, s10, s11]; + let proofs = merkle_prove(leaf_hashes.to_vec(), position); + assert_eq!(proofs, expected_proofs); + + let position = 2; + let expected_proofs = [s2, s10, s11]; + let proofs = merkle_prove(leaf_hashes.to_vec(), position); + assert_eq!(proofs, expected_proofs); + assert_eq!(proofs, expected_proofs); + + assert_eq!(proofs, expected_proofs); + } + + //https://github.com/cashubtc/nuts/blob/a86a4e8ce0b9a76ce9b242d6c2c2ab846b3e1955/tests/sct-tests.md.md#proofs + #[test] + //test vector from docs + fn test_valid_sct() { + let s = "9becd3a8ce24b53beaf8ffb20a497b683b55f87ef87e3814be43a5768bcfe69fj"; + + let s1 = String::from("009ea9fae527f7914096da1f1ce2480d2e4cfea62480afb88da9219f1c09767f"); + let s2 = String::from("2628c9759f0cecbb43b297b6eb0c268573d265730c2c9f6e194b4948f43d669d"); + let s3 = String::from("7ea48b9a4ad58f92c4cfa8e006afa98b2b05ac1b4de481e13088d26f672d8edc"); + + let merkle_proof = vec![s1, s2, s3]; + + let root: [u8; 32] = + hex::decode("71655cac0c83c6949169bcd6c82b309810138895f83b967089ffd9f64d109306") + .unwrap() + .try_into() + .unwrap(); + + let leaf_hash = Sha256Hash::hash(s.as_bytes()).to_byte_array(); + + let b = merkle_verify(&root, &leaf_hash, &merkle_proof); + println!("{b}"); + + assert!(b); + } + + #[test] + //test from SCT our program created + fn test_our_valid_sct() { + let s = "[\"DLC\",{\"nonce\":\"aea22dd7c80f0fc87b3ab66b7c910d21d5f27d63f0f0f8164e3dbceed25c7447\",\"data\":\"2c5da07a0542ef3731e254c006d1ecfea7cd951c11cea1c065a12c39e3b1f1a2\",\"tags\":[[\"sigflag\",\"SIG_INPUTS\"],[\"threshold\",\"1\"]]}]"; + + let s1 = String::from("80ebc929bcb51d0ac6ed24d9f9bbb6897494c5bf8c4a4dadad6dca772a1d865a"); + + let merkle_proof = vec![s1]; + + let root: [u8; 32] = + hex::decode("09682b8e375979e68189ff293cbe09038de1d67b5b5fa46961814dc8747d8a7b") + .unwrap() + .try_into() + .unwrap(); + + let leaf_hash = Sha256Hash::hash(s.as_bytes()).to_byte_array(); + + let b = merkle_verify(&root, &leaf_hash, &merkle_proof); + + assert!(b); + } + + //https://github.com/cashubtc/nuts/blob/a86a4e8ce0b9a76ce9b242d6c2c2ab846b3e1955/tests/sct-tests.md.md#invalid + + #[test] + //test vector from docs + fn test_invalid_sct() { + let s = "9becd3a8ce24b53beaf8ffb20a497b683b55f87ef87e3814be43a5768bcfe69fj"; + + let s1 = String::from("db7a191c4f3c112d7eb3ae9ee8fa9bd940fc4fed6ada9ba9ab2f102c3e3bbe80"); + let s2 = String::from("2628c9759f0cecbb43b297b6eb0c268573d265730c2c9f6e194b4948f43d669d"); + let s3 = String::from("7ea48b9a4ad58f92c4cfa8e006afa98b2b05ac1b4de481e13088d26f672d8edc"); + + let merkle_proof = vec![s1, s2, s3]; + + let root: [u8; 32] = + hex::decode("71655cac0c83c6949169bcd6c82b309810138895f83b967089ffd9f64d109306") + .unwrap() + .try_into() + .unwrap(); + + let leaf_hash = Sha256Hash::hash(s.as_bytes()).to_byte_array(); + + let b = merkle_verify(&root, &leaf_hash, &merkle_proof); + + assert_ne!(b, true); + } + + #[test] + //test from SCT our program created + fn test_nutshell_info() { + let s = "[\"DLC\",{\"nonce\":\"54d5263c9282f22c494b38f2967c23ac54de26502606f2a98b734b318c115250\",\"data\":\"d87010e7e82070c94c28b5e2aedff3275e452b93e6af1fd74e4f4d535e1e35a3\",\"tags\":[[\"sigflag\",\"SIG_INPUTS\"],[\"threshold\",\"1\"]]}]"; + + let s1 = String::from("f737a46eaa37450285f9f9c7bafb653d9f6074614a9339a64a6307bd878b748c"); + + let merkle_proof = vec![s1]; + + let root: [u8; 32] = + hex::decode("345af1eee507016d86d66d022bde5225ab3ac15a183fbb64d8780ef394b2fcc1") + .unwrap() + .try_into() + .unwrap(); + + let leaf_hash = Sha256Hash::hash(s.as_bytes()).to_byte_array(); + + let b = merkle_verify(&root, &leaf_hash, &merkle_proof); + + assert!(b); + } +} + +/* +Proof we created to test + +[ + + +Proof { amount: Amount(1), + +keyset_id: Id { version: Version00, id: [255, 212, 139, 143, 94, 207, 128] }, + +secret: Secret("[\"SCT\",{\"nonce\":\"bebc21ceaccd4aa59c5f19ee98373f88916dc79e204979f0aee043ce0943e05c\",\"data\":\"09682b8e375979e68189ff293cbe09038de1d67b5b5fa46961814dc8747d8a7b\"}]"), + +c: PublicKey { inner: PublicKey(8a4fe273c7ddc7c25a0aeb52039cada076ae928ab04cbfbc1350d6702d7b2b05275ab6e3f3ad091057a2b7436931ad5802b82dced2b675a15025b09e9a878833) }, witness: Some(SCTWitness(SCTWitness { leaf_secret: "[\"DLC\",{\"nonce\":\"aea22dd7c80f0fc87b3ab66b7c910d21d5f27d63f0f0f8164e3dbceed25c7447\",\"data\":\"2c5da07a0542ef3731e254c006d1ecfea7cd951c11cea1c065a12c39e3b1f1a2\",\"tags\":[[\"sigflag\",\"SIG_INPUTS\"],[\"threshold\",\"1\"]]}]", + +merkle_proof: ["80ebc929bcb51d0ac6ed24d9f9bbb6897494c5bf8c4a4dadad6dca772a1d865a"] })), + +dleq: Some(ProofDleq { e: SecretKey { inner: SecretKey(#7564a3ed9461cbba) }, + +s: SecretKey { inner: SecretKey(#4c853763f86d0058) }, + +r: SecretKey { inner: SecretKey(#edb5053fca96a7f9) } }) }, + + + + + + +Proof { + +amount: Amount(4), + + +keyset_id: Id { version: Version00, id: [255, 212, 139, 143, 94, 207, 128] }, + +secret: Secret("[\"SCT\",{\"nonce\":\"d58333d05c1b0d6cd86c93a4b0aa54ba44488fee915439f861befd53bcdc5d6d\",\"data\":\"09682b8e375979e68189ff293cbe09038de1d67b5b5fa46961814dc8747d8a7b\"}]"), + +c: PublicKey { inner: PublicKey(21de97e2fbc742501fc20d79fa900a733c74f38f6298f2f78b8d71bf337d7d7042e99162fac27506ef040a10e9a9b76578f807d5a13c0090e5b0ced0483e1b8d) }, witness: Some(SCTWitness(SCTWitness { leaf_secret: "[\"DLC\",{\"nonce\":\"aea22dd7c80f0fc87b3ab66b7c910d21d5f27d63f0f0f8164e3dbceed25c7447\",\"data\":\"2c5da07a0542ef3731e254c006d1ecfea7cd951c11cea1c065a12c39e3b1f1a2\",\"tags\":[[\"sigflag\",\"SIG_INPUTS\"],[\"threshold\",\"1\"]]}]", + +merkle_proof: ["80ebc929bcb51d0ac6ed24d9f9bbb6897494c5bf8c4a4dadad6dca772a1d865a"] })), + +dleq: Some(ProofDleq { e: SecretKey { inner: SecretKey(#1aa35cc207c967ae) }, + +s: SecretKey { inner: SecretKey(#1426c306e96c16a8) }, + +r: SecretKey { inner: SecretKey(#ed2a1fc21398d714) } }) } + + +] + + + +*/ diff --git a/crates/cdk/src/nuts/nutsct/serde_sct_witness.rs b/crates/cdk/src/nuts/nutsct/serde_sct_witness.rs new file mode 100644 index 000000000..69310725f --- /dev/null +++ b/crates/cdk/src/nuts/nutsct/serde_sct_witness.rs @@ -0,0 +1,22 @@ +//! Serde utils for P2PK Witness + +use serde::{de, ser, Deserialize, Deserializer, Serializer}; + +use super::SCTWitness; + +/// Serialize [sctWitness] as stringified JSON +pub fn serialize(x: &SCTWitness, s: S) -> Result +where + S: Serializer, +{ + s.serialize_str(&serde_json::to_string(&x).map_err(ser::Error::custom)?) +} + +/// Deserialize [sctWitness] from stringified JSON +pub fn deserialize<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s: String = String::deserialize(deserializer)?; + serde_json::from_str(&s).map_err(de::Error::custom) +} diff --git a/crates/cdk/src/wallet/client.rs b/crates/cdk/src/wallet/client.rs index d5b413ce4..21499131a 100644 --- a/crates/cdk/src/wallet/client.rs +++ b/crates/cdk/src/wallet/client.rs @@ -9,6 +9,10 @@ use super::Error; use crate::error::ErrorResponse; use crate::mint_url::MintUrl; use crate::nuts::nut15::Mpp; +use crate::nuts::nutdlc::{ + DLCRegistrationResponse, DLCStatusResponse, PostDLCPayoutRequest, PostDLCPayoutResponse, + PostDLCRegistrationRequest, PostSettleDLCRequest, SettleDLCResponse, +}; use crate::nuts::{ BlindedMessage, CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysResponse, KeysetResponse, MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, @@ -366,4 +370,85 @@ impl HttpClient { Err(_) => Err(ErrorResponse::from_value(res)?.into()), } } + + /// Fund a DLC + pub async fn post_register_dlc( + &self, + mint_url: MintUrl, + fund_dlc_request: PostDLCRegistrationRequest, + ) -> Result { + let url = mint_url.join_paths(&["v1", "dlc", "fund"])?; + let res = self + .inner + .post(url) + .json(&fund_dlc_request) + .send() + .await? + .json::() + .await?; + + match serde_json::from_value::(res.clone()) { + Ok(dlc_response) => Ok(dlc_response), + Err(_) => Err(ErrorResponse::from_value(res)?.into()), + } + } + + /// Settle DLC + pub async fn post_settle_dlc( + &self, + mint_url: MintUrl, + settle_dlc_request: PostSettleDLCRequest, + ) -> Result { + let url = mint_url.join_paths(&["v1", "dlc", "settle"])?; + let res = self + .inner + .post(url) + .json(&settle_dlc_request) + .send() + .await? + .json::() + .await?; + + match serde_json::from_value::(res.clone()) { + Ok(settle_dlc_response) => Ok(settle_dlc_response), + Err(_) => Err(ErrorResponse::from_value(res)?.into()), + } + } + + /// Get status of DLC + pub async fn status( + &self, + mint_url: MintUrl, + dlc_root: &str, + ) -> Result { + let url = mint_url.join_paths(&["v1", "dlc", "status", dlc_root])?; + let res = self.inner.get(url).send().await?.json::().await?; + + match serde_json::from_value::(res.clone()) { + Ok(dlc_status_response) => Ok(dlc_status_response), + Err(_) => Err(ErrorResponse::from_value(res)?.into()), + } + } + + /// Claim payout for DLC + pub async fn payout( + &self, + mint_url: MintUrl, + request: PostDLCPayoutRequest, + ) -> Result { + let url = mint_url.join_paths(&["v1", "dlc", "payout"])?; + let res = self + .inner + .post(url) + .json(&request) + .send() + .await? + .json::() + .await?; + + match serde_json::from_value::(res.clone()) { + Ok(dlc_status_response) => Ok(dlc_status_response), + Err(_) => Err(ErrorResponse::from_value(res)?.into()), + } + } } diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index 85045eaa8..c535c7c33 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/crates/cdk/src/wallet/mod.rs @@ -15,9 +15,14 @@ use crate::error::Error; use crate::fees::calculate_fee; use crate::mint_url::MintUrl; use crate::nuts::nut00::token::Token; +use crate::nuts::nutdlc::{ + ClaimDLCPayout, DLCOutcome, DLCPayout, DLCPayoutWitness, DLCRegistrationResponse, + DLCSettlement, DLCStatusResponse, PostDLCPayoutRequest, PostDLCRegistrationRequest, + PostSettleDLCRequest, DLC, +}; use crate::nuts::{ - nut10, CurrencyUnit, Id, Keys, MintInfo, MintQuoteState, PreMintSecrets, Proof, Proofs, - RestoreRequest, SpendingConditions, State, + nut10, BlindedMessage, CurrencyUnit, Id, Keys, MintInfo, MintQuoteState, PreMintSecrets, Proof, + Proofs, RestoreRequest, SpendingConditions, State, }; use crate::types::ProofInfo; use crate::{Amount, HttpClient}; @@ -383,6 +388,13 @@ impl Wallet { ), None => (None, None, None, None), }, + SpendingConditions::DLCConditions { .. } => { + todo!() + } + + SpendingConditions::SCTConditions { .. } => { + todo!() + } }; if refund_keys.is_some() && locktime.is_none() { @@ -516,4 +528,149 @@ impl Wallet { Ok(()) } + + /// Register a DLC + #[instrument(skip(self))] + pub async fn register_dlc(&self, dlc: DLC) -> Result<(), Error> { + let fund_dlc_request = PostDLCRegistrationRequest { + registrations: vec![dlc], + }; + + // TODO: the matching on the response seems to always be `Success` even if there are errors + let fund_dlc_response = match self + .client + .post_register_dlc(self.mint_url.clone(), fund_dlc_request) + .await? + { + DLCRegistrationResponse::Success { funded } => funded, + DLCRegistrationResponse::Error { errors, .. } => { + println!("Error registering DLC: {:?}", errors); + tracing::error!("Error registering DLC: {:?}", errors); + return Err(Error::Custom("Error registering DLC".to_string())); + } + }; + + // we are not properly catching the error, so if `funded` is empty, we know the registration failed + assert!(!fund_dlc_response.is_empty(), "DLC registration failed"); + + for funded_dlc in fund_dlc_response { + let dlc_root = funded_dlc.dlc_root; + let funding_proof = funded_dlc.funding_proof; + println!("Funded DLC: {:?}", dlc_root); + println!("Funding Proof: {:?}", funding_proof); + } + + Ok(()) + } + + /// Settle DLC + pub async fn settle_dlc( + &self, + dlc_root: &String, + outcome: DLCOutcome, + merkle_proof: Vec<[u8; 32]>, + ) -> Result<(), Error> { + let merkle_proof_string = merkle_proof + .iter() + .map(|p| crate::util::hex::encode(p)) + .collect::>(); + let settle_dlc_request = PostSettleDLCRequest { + settlements: vec![DLCSettlement { + dlc_root: dlc_root.clone(), + outcome, + merkle_proof: merkle_proof_string, + }], + }; + + match self + .client + .post_settle_dlc(self.mint_url.clone(), settle_dlc_request) + .await + { + Ok(settle_dlc_response) => { + println!("Settled DLC: {:?}", settle_dlc_response); + if settle_dlc_response.settled.is_empty() { + tracing::error!("No settled DLCs"); + return Err(Error::Custom("No settled DLCs".to_string())); + } + let mut has_root = false; + for settled_dlc in settle_dlc_response.settled { + if settled_dlc.dlc_root == *dlc_root { + has_root = true; + break; + } + } + if !has_root { + tracing::error!("No settled DLC with root with root: {:?}", dlc_root); + return Err(Error::Custom("No settled DLC with root".to_string())); + } else { + return Ok(()); + } + } + Err(err) => { + println!("Error settling DLC: {:?}", err); + tracing::error!("Error settling DLC: {:?}", err); + return Err(Error::Custom("Error settling DLC".to_string())); + } + }; + } + + /// Get status of DLC + pub async fn dlc_status(&self, dlc_root: String) -> Result { + let dlc_status_response = match self.client.status(self.mint_url.clone(), &dlc_root).await { + Ok(dlc_status_response) => dlc_status_response, + Err(err) => { + println!("Error: {:?}", err); + tracing::error!("Error getting DLC status: {:?}", err); + return Err(Error::Custom("Error getting DLC status".to_string())); + } + }; + + Ok(dlc_status_response) + } + + /// Claim payout for DLC + pub async fn claim_dlc_payout( + &self, + dlc_root: String, + pubkey: String, + outputs: Vec, + signature: Option, + ) -> Result { + let payout = ClaimDLCPayout { + dlc_root, + pubkey, + outputs, + witness: DLCPayoutWitness { + secret: None, + signature: signature.clone(), + }, + }; + + let payout_request = PostDLCPayoutRequest { + payouts: vec![payout], + }; + + let payout_response = match self + .client + .payout(self.mint_url.clone(), payout_request) + .await + { + Ok(payout_response) => payout_response, + Err(err) => { + println!("Error: {:?}", err); + tracing::error!("Error claiming DLC payout: {:?}", err); + return Err(Error::Custom("Error claiming DLC payout".to_string())); + } + }; + + if let Some(errors) = payout_response.errors { + for error in errors { + println!("Error: {:?}", error); + } + return Err(Error::Custom("Error claiming DLC payout".to_string())); + } + + Ok(payout_response.paid[0].clone()) + } } diff --git a/crates/cdk/src/wallet/receive.rs b/crates/cdk/src/wallet/receive.rs index d5999232a..02e866344 100644 --- a/crates/cdk/src/wallet/receive.rs +++ b/crates/cdk/src/wallet/receive.rs @@ -93,6 +93,12 @@ impl Wallet { .ok_or(Error::PreimageNotProvided)?; proof.add_preimage(preimage.to_string()); } + Kind::DLC => { + todo!() + } + Kind::SCT => { + todo!() + } } for pubkey in pubkeys { if let Some(signing) = p2pk_signing_keys.get(&pubkey.x_only_public_key()) {