use std::sync::Arc;
use crate::cli_opts::{CliCommand, OracleCommand, WalletCommand};
use crate::ddkrpc::ddk_rpc_client::DdkRpcClient;
use crate::ddkrpc::{
sign_request, AcceptOfferRequest, ConnectRequest, CreateEnumRequest, CreateNumericRequest,
GetWalletTransactionsRequest, InfoRequest, ListContractsRequest, ListOffersRequest,
ListPeersRequest, ListUtxosRequest, NewAddressRequest, OracleAnnouncementsRequest,
SendOfferRequest, SendRequest, SignRequest, SyncRequest, WalletBalanceRequest,
WalletSyncRequest,
};
use anyhow::anyhow;
use bitcoin::{Amount, Transaction};
use chrono::TimeDelta;
use ddk::json::*;
use ddk::logger::{LogLevel, Logger};
use ddk::oracle::kormir::KormirOracleClient;
use ddk::util;
use ddk::wallet::LocalOutput;
use ddk_dlc::{EnumerationPayout, Payout};
use ddk_manager::contract::contract_input::{ContractInput, ContractInputInfo, OracleInput};
use ddk_manager::contract::enum_descriptor::EnumDescriptor;
use ddk_manager::contract::offered_contract::OfferedContract;
use ddk_manager::contract::{Contract, ContractDescriptor};
use ddk_manager::Oracle;
use ddk_messages::oracle_msgs::{EventDescriptor, OracleAnnouncement};
use ddk_messages::{AcceptDlc, OfferDlc};
use inquire::{Select, Text};
use serde_json::Value;
use tonic::transport::Channel;
pub async fn cli_command(
arg: CliCommand,
client: &mut DdkRpcClient<Channel>,
) -> anyhow::Result<()> {
match arg {
CliCommand::Info => {
let info = client.info(InfoRequest::default()).await?.into_inner();
print!("{}", serde_json::to_string_pretty(&info)?);
}
CliCommand::OfferContract(arg) => {
let contract_input = if arg.generate {
generate_contract_input().await?
} else {
interactive_contract_input(client).await?
};
let contract_input = serde_json::to_vec(&contract_input)?;
let offer = client
.send_offer(SendOfferRequest {
contract_input,
counter_party: arg.counter_party,
})
.await?
.into_inner();
let offer_dlc: OfferDlc = serde_json::from_slice(&offer.offer_dlc)?;
let offer = serde_json::to_string_pretty(&offer_dlc)?;
print!("{offer}");
}
CliCommand::Offers => {
let offers_request = client.list_offers(ListOffersRequest {}).await?.into_inner();
let offers: Vec<OfferedContract> = offers_request
.offers
.iter()
.map(|offer| serde_json::from_slice(offer).unwrap())
.collect();
let pretty_offer = offers
.iter()
.map(|offer| offered_contract_to_value(offer, "offer"))
.collect::<Vec<Value>>();
print!("{}", serde_json::to_string_pretty(&pretty_offer).unwrap());
}
CliCommand::AcceptOffer(accept) => {
let accept = client
.accept_offer(AcceptOfferRequest {
contract_id: accept.contract_id,
})
.await?
.into_inner();
let accept_dlc: AcceptDlc = serde_json::from_slice(&accept.accept_dlc)?;
let accept_dlc = serde_json::to_string_pretty(&accept_dlc)?;
print!("{accept_dlc}");
}
CliCommand::Contracts => {
let contracts = client
.list_contracts(ListContractsRequest {})
.await?
.into_inner()
.contracts;
let contract_values = contracts
.iter()
.map(|c| {
let contract: Contract = util::ser::deserialize_contract(c).unwrap();
contract_to_value(&contract)
})
.collect::<Vec<Value>>();
print!("{}", serde_json::to_string_pretty(&contract_values)?)
}
CliCommand::Balance => {
let balance = client
.wallet_balance(WalletBalanceRequest::default())
.await?
.into_inner();
let pretty_string = serde_json::to_string_pretty(&balance)?;
println!("{pretty_string}");
}
CliCommand::Wallet(wallet) => match wallet {
WalletCommand::NewAddress => {
let address = client
.new_address(NewAddressRequest::default())
.await?
.into_inner();
let pretty_string = serde_json::to_string_pretty(&address)?;
print!("{pretty_string}");
}
WalletCommand::Transactions => {
let transactions = client
.get_wallet_transactions(GetWalletTransactionsRequest::default())
.await?
.into_inner();
let txns = transactions
.transactions
.iter()
.map(|txn| serde_json::from_slice(txn).unwrap())
.collect::<Vec<Transaction>>();
let txns = serde_json::to_string_pretty(&txns)?;
print!("{txns}");
}
WalletCommand::Utxos => {
let utxos = client
.list_utxos(ListUtxosRequest::default())
.await?
.into_inner();
let local_outputs = utxos
.utxos
.iter()
.map(|utxo| serde_json::from_slice(utxo).unwrap())
.collect::<Vec<LocalOutput>>();
print!("{}", serde_json::to_string_pretty(&local_outputs).unwrap())
}
WalletCommand::Send {
address,
amount,
fee_rate,
} => {
let txid = client
.send(SendRequest {
address,
amount,
fee_rate,
})
.await?
.into_inner();
print!("{}", serde_json::to_string_pretty(&txid)?)
}
WalletCommand::Sync => {
let _ = client.wallet_sync(WalletSyncRequest {}).await?.into_inner();
println!("Wallet synced.")
}
},
CliCommand::Oracle(command) => match command {
OracleCommand::Announcements { event_id } => {
let announcements = client
.oracle_announcements(OracleAnnouncementsRequest { event_id })
.await?
.into_inner();
let oracle_announcement: OracleAnnouncement =
serde_json::from_slice(&announcements.announcement)?;
print!(
"{}",
serde_json::to_string_pretty(&oracle_announcement).unwrap()
)
}
OracleCommand::CreateEnum { maturity, outcomes } => {
let response = client
.create_enum(CreateEnumRequest { maturity, outcomes })
.await?
.into_inner();
let oracle_announcement: OracleAnnouncement =
serde_json::from_slice(&response.announcement)?;
print!(
"{}",
serde_json::to_string_pretty(&oracle_announcement).unwrap()
)
}
OracleCommand::CreateNumeric {
maturity,
nb_digits,
} => {
let response = client
.create_numeric(CreateNumericRequest {
maturity,
nb_digits,
})
.await?
.into_inner();
let oracle_announcement: OracleAnnouncement =
serde_json::from_slice(&response.announcement)?;
print!(
"{}",
serde_json::to_string_pretty(&oracle_announcement).unwrap()
)
}
OracleCommand::Sign {
r#enum: enum_flag,
numeric,
outcome,
event_id,
} => {
if enum_flag && numeric {
return Err(anyhow!("Cannot specify both --enum and --numeric"));
}
if !enum_flag && !numeric {
return Err(anyhow!("Must specify either --enum or --numeric"));
}
let outcome_variant = if enum_flag {
sign_request::Outcome::EnumOutcome(outcome)
} else {
let numeric_outcome = outcome.parse::<i64>().map_err(|_| {
anyhow!("Outcome must be a valid integer for numeric events")
})?;
sign_request::Outcome::NumericOutcome(numeric_outcome)
};
let request = SignRequest {
event_id,
outcome: Some(outcome_variant),
};
let response = client.sign_announcement(request).await?.into_inner();
print!("{}", serde_json::to_string_pretty(&response.signature)?);
}
},
CliCommand::Peers => {
let peers_response = client
.list_peers(ListPeersRequest::default())
.await?
.into_inner();
let peers = serde_json::to_string_pretty(&peers_response.peers)?;
print!("{peers}");
}
CliCommand::Connect { connect_string } => {
let parts = connect_string.split("@").collect::<Vec<&str>>();
client
.connect_peer(ConnectRequest {
pubkey: parts[0].to_string(),
host: parts[1].to_string(),
})
.await?;
print!("Connected to {}", parts[0])
}
CliCommand::Sync => {
let _ = client.sync(SyncRequest {}).await?.into_inner();
println!("Synced.")
}
}
Ok(())
}
async fn generate_contract_input() -> anyhow::Result<ContractInput> {
let contract_descriptor = ContractDescriptor::Enum(EnumDescriptor {
outcome_payouts: vec![
EnumerationPayout {
outcome: "CTV".to_string(),
payout: Payout {
offer: Amount::from_sat(21_000_000),
accept: Amount::ZERO,
},
},
EnumerationPayout {
outcome: "CAT".to_string(),
payout: Payout {
offer: Amount::ZERO,
accept: Amount::from_sat(21_000_000),
},
},
],
});
let logger = Arc::new(Logger::console(
"generate_contract_input".to_string(),
LogLevel::Info,
));
let kormir = KormirOracleClient::new("https://kormir.dlcdevkit.com", None, logger).await?;
let expiry = (chrono::Utc::now()
.checked_add_signed(TimeDelta::minutes(15))
.unwrap()
.timestamp()) as u32;
let announcement = kormir
.create_enum_event(vec!["CTV".to_string(), "CAT".to_string()], expiry)
.await?;
let oracle_input = OracleInput {
public_keys: vec![kormir.get_public_key()],
event_id: announcement.oracle_event.event_id,
threshold: 1,
};
Ok(ContractInput {
offer_collateral: Amount::from_sat(10_500_000),
accept_collateral: Amount::from_sat(10_500_000),
fee_rate: 1,
contract_flags: 0,
contract_infos: vec![ContractInputInfo {
contract_descriptor,
oracles: oracle_input,
}],
})
}
async fn interactive_contract_input(
client: &mut DdkRpcClient<Channel>,
) -> anyhow::Result<ContractInput> {
let contract_type =
Select::new("Select type of contract.", vec!["enum", "numerical"]).prompt()?;
let event_id = Text::new("Oracle announcement event id:").prompt()?;
let announcement = client
.oracle_announcements(OracleAnnouncementsRequest { event_id })
.await?
.into_inner();
let selected_announcement: OracleAnnouncement =
serde_json::from_slice(&announcement.announcement)?;
let contract_input = match contract_type {
"numerical" => {
let offer_collateral: u64 =
Text::new("Collateral from you (sats):").prompt()?.parse()?;
let accept_collateral: u64 = Text::new("Collateral from counterparty (sats):")
.prompt()?
.parse()?;
let fee_rate: u64 = Text::new("Fee rate (sats/vbyte):").prompt()?.parse()?;
let min_price: u64 = Text::new("Minimum Bitcoin price:").prompt()?.parse()?;
let max_price: u64 = Text::new("Maximum Bitcoin price:").prompt()?.parse()?;
let num_steps: u64 = Text::new("Number of rounding steps:").prompt()?.parse()?;
let oracle_pubkey = Text::new("Oracle public key:").prompt()?;
let event_id = Text::new("Oracle event id:").prompt()?;
ddk_payouts::create_contract_input(
min_price,
max_price,
num_steps,
Amount::from_sat(offer_collateral),
Amount::from_sat(accept_collateral),
fee_rate,
oracle_pubkey,
event_id,
)
}
"enum" => {
let offer_collateral: u64 =
Text::new("Collateral from you (sats):").prompt()?.parse()?;
let accept_collateral: u64 = Text::new("Collateral from your counterparty (sats):")
.prompt()?
.parse()?;
let outcomes = match &selected_announcement.oracle_event.event_descriptor {
EventDescriptor::EnumEvent(e) => e.outcomes.clone(),
_ => return Err(anyhow!("Not an enum event from announcement.")),
};
let mut outcome_payouts = Vec::with_capacity(outcomes.len());
println!("Specify the payouts for each outcome.");
for outcome in outcomes {
println!("> Event outcome: {outcome}");
let offer: u64 = Text::new("Your payout:").prompt()?.parse()?;
let accept: u64 = Text::new("Counterparty payout:").prompt()?.parse()?;
let outcome_payout = EnumerationPayout {
outcome,
payout: Payout {
offer: Amount::from_sat(offer),
accept: Amount::from_sat(accept),
},
};
outcome_payouts.push(outcome_payout);
}
let fee_rate: u64 = Text::new("Fee rate (sats/vbyte):").prompt()?.parse()?;
ddk_payouts::enumeration::create_contract_input(
outcome_payouts,
Amount::from_sat(offer_collateral),
Amount::from_sat(accept_collateral),
fee_rate,
selected_announcement.oracle_public_key.to_string(),
selected_announcement.oracle_event.event_id.clone(),
)
}
_ => return Err(anyhow!("Invalid contract type.")),
};
Ok(contract_input)
}