1use std::sync::Arc;
2
3use crate::cli_opts::{CliCommand, OracleCommand, WalletCommand};
4use crate::ddkrpc::ddk_rpc_client::DdkRpcClient;
5use crate::ddkrpc::{
6 sign_request, AcceptOfferRequest, ConnectRequest, CreateEnumRequest, CreateNumericRequest,
7 GetWalletTransactionsRequest, InfoRequest, ListContractsRequest, ListOffersRequest,
8 ListPeersRequest, ListUtxosRequest, NewAddressRequest, OracleAnnouncementsRequest,
9 SendOfferRequest, SendRequest, SignRequest, SyncRequest, WalletBalanceRequest,
10 WalletSyncRequest,
11};
12use anyhow::anyhow;
13use bitcoin::{Amount, Transaction};
14use chrono::TimeDelta;
15use ddk::json::*;
16use ddk::logger::{LogLevel, Logger};
17use ddk::oracle::kormir::KormirOracleClient;
18use ddk::util;
19use ddk::wallet::LocalOutput;
20use ddk_dlc::{EnumerationPayout, Payout};
21use ddk_manager::contract::contract_input::{ContractInput, ContractInputInfo, OracleInput};
22use ddk_manager::contract::enum_descriptor::EnumDescriptor;
23use ddk_manager::contract::offered_contract::OfferedContract;
24use ddk_manager::contract::{Contract, ContractDescriptor};
25use ddk_manager::Oracle;
26use ddk_messages::oracle_msgs::{EventDescriptor, OracleAnnouncement};
27use ddk_messages::{AcceptDlc, OfferDlc};
28use inquire::{Select, Text};
29use serde_json::Value;
30use tonic::transport::Channel;
31
32pub async fn cli_command(
33 arg: CliCommand,
34 client: &mut DdkRpcClient<Channel>,
35) -> anyhow::Result<()> {
36 match arg {
37 CliCommand::Info => {
38 let info = client.info(InfoRequest::default()).await?.into_inner();
39 print!("{}", serde_json::to_string_pretty(&info)?);
40 }
41 CliCommand::OfferContract(arg) => {
42 let contract_input = if arg.generate {
43 generate_contract_input().await?
44 } else {
45 interactive_contract_input(client).await?
46 };
47 let contract_input = serde_json::to_vec(&contract_input)?;
48 let offer = client
49 .send_offer(SendOfferRequest {
50 contract_input,
51 counter_party: arg.counter_party,
52 })
53 .await?
54 .into_inner();
55 let offer_dlc: OfferDlc = serde_json::from_slice(&offer.offer_dlc)?;
56 let offer = serde_json::to_string_pretty(&offer_dlc)?;
57 print!("{offer}");
58 }
59 CliCommand::Offers => {
60 let offers_request = client.list_offers(ListOffersRequest {}).await?.into_inner();
61 let offers: Vec<OfferedContract> = offers_request
62 .offers
63 .iter()
64 .map(|offer| serde_json::from_slice(offer).unwrap())
65 .collect();
66 let pretty_offer = offers
67 .iter()
68 .map(|offer| offered_contract_to_value(offer, "offer"))
69 .collect::<Vec<Value>>();
70 print!("{}", serde_json::to_string_pretty(&pretty_offer).unwrap());
71 }
72 CliCommand::AcceptOffer(accept) => {
73 let accept = client
74 .accept_offer(AcceptOfferRequest {
75 contract_id: accept.contract_id,
76 })
77 .await?
78 .into_inner();
79 let accept_dlc: AcceptDlc = serde_json::from_slice(&accept.accept_dlc)?;
80 let accept_dlc = serde_json::to_string_pretty(&accept_dlc)?;
81 print!("{accept_dlc}");
82 }
83 CliCommand::Contracts => {
84 let contracts = client
85 .list_contracts(ListContractsRequest {})
86 .await?
87 .into_inner()
88 .contracts;
89 let contract_values = contracts
90 .iter()
91 .map(|c| {
92 let contract: Contract = util::ser::deserialize_contract(c).unwrap();
93 contract_to_value(&contract)
94 })
95 .collect::<Vec<Value>>();
96 print!("{}", serde_json::to_string_pretty(&contract_values)?)
97 }
98 CliCommand::Balance => {
99 let balance = client
100 .wallet_balance(WalletBalanceRequest::default())
101 .await?
102 .into_inner();
103 let pretty_string = serde_json::to_string_pretty(&balance)?;
104 println!("{pretty_string}");
105 }
106 CliCommand::Wallet(wallet) => match wallet {
107 WalletCommand::NewAddress => {
108 let address = client
109 .new_address(NewAddressRequest::default())
110 .await?
111 .into_inner();
112 let pretty_string = serde_json::to_string_pretty(&address)?;
113 print!("{pretty_string}");
114 }
115 WalletCommand::Transactions => {
116 let transactions = client
117 .get_wallet_transactions(GetWalletTransactionsRequest::default())
118 .await?
119 .into_inner();
120 let txns = transactions
121 .transactions
122 .iter()
123 .map(|txn| serde_json::from_slice(txn).unwrap())
124 .collect::<Vec<Transaction>>();
125 let txns = serde_json::to_string_pretty(&txns)?;
126 print!("{txns}");
127 }
128 WalletCommand::Utxos => {
129 let utxos = client
130 .list_utxos(ListUtxosRequest::default())
131 .await?
132 .into_inner();
133 let local_outputs = utxos
134 .utxos
135 .iter()
136 .map(|utxo| serde_json::from_slice(utxo).unwrap())
137 .collect::<Vec<LocalOutput>>();
138 print!("{}", serde_json::to_string_pretty(&local_outputs).unwrap())
139 }
140 WalletCommand::Send {
141 address,
142 amount,
143 fee_rate,
144 } => {
145 let txid = client
146 .send(SendRequest {
147 address,
148 amount,
149 fee_rate,
150 })
151 .await?
152 .into_inner();
153 print!("{}", serde_json::to_string_pretty(&txid)?)
154 }
155 WalletCommand::Sync => {
156 let _ = client.wallet_sync(WalletSyncRequest {}).await?.into_inner();
157 println!("Wallet synced.")
158 }
159 },
160 CliCommand::Oracle(command) => match command {
161 OracleCommand::Announcements { event_id } => {
162 let announcements = client
163 .oracle_announcements(OracleAnnouncementsRequest { event_id })
164 .await?
165 .into_inner();
166 let oracle_announcement: OracleAnnouncement =
167 serde_json::from_slice(&announcements.announcement)?;
168 print!(
169 "{}",
170 serde_json::to_string_pretty(&oracle_announcement).unwrap()
171 )
172 }
173 OracleCommand::CreateEnum { maturity, outcomes } => {
174 let response = client
175 .create_enum(CreateEnumRequest { maturity, outcomes })
176 .await?
177 .into_inner();
178 let oracle_announcement: OracleAnnouncement =
179 serde_json::from_slice(&response.announcement)?;
180 print!(
181 "{}",
182 serde_json::to_string_pretty(&oracle_announcement).unwrap()
183 )
184 }
185 OracleCommand::CreateNumeric {
186 maturity,
187 nb_digits,
188 } => {
189 let response = client
190 .create_numeric(CreateNumericRequest {
191 maturity,
192 nb_digits,
193 })
194 .await?
195 .into_inner();
196 let oracle_announcement: OracleAnnouncement =
197 serde_json::from_slice(&response.announcement)?;
198 print!(
199 "{}",
200 serde_json::to_string_pretty(&oracle_announcement).unwrap()
201 )
202 }
203 OracleCommand::Sign {
204 r#enum: enum_flag,
205 numeric,
206 outcome,
207 event_id,
208 } => {
209 if enum_flag && numeric {
210 return Err(anyhow!("Cannot specify both --enum and --numeric"));
211 }
212 if !enum_flag && !numeric {
213 return Err(anyhow!("Must specify either --enum or --numeric"));
214 }
215 let outcome_variant = if enum_flag {
216 sign_request::Outcome::EnumOutcome(outcome)
217 } else {
218 let numeric_outcome = outcome.parse::<i64>().map_err(|_| {
219 anyhow!("Outcome must be a valid integer for numeric events")
220 })?;
221 sign_request::Outcome::NumericOutcome(numeric_outcome)
222 };
223 let request = SignRequest {
224 event_id,
225 outcome: Some(outcome_variant),
226 };
227 let response = client.sign_announcement(request).await?.into_inner();
228 print!("{}", serde_json::to_string_pretty(&response.signature)?);
229 }
230 },
231 CliCommand::Peers => {
232 let peers_response = client
233 .list_peers(ListPeersRequest::default())
234 .await?
235 .into_inner();
236 let peers = serde_json::to_string_pretty(&peers_response.peers)?;
237 print!("{peers}");
238 }
239 CliCommand::Connect { connect_string } => {
240 let parts = connect_string.split("@").collect::<Vec<&str>>();
241 client
242 .connect_peer(ConnectRequest {
243 pubkey: parts[0].to_string(),
244 host: parts[1].to_string(),
245 })
246 .await?;
247 print!("Connected to {}", parts[0])
248 }
249 CliCommand::Sync => {
250 let _ = client.sync(SyncRequest {}).await?.into_inner();
251 println!("Synced.")
252 }
253 }
254 Ok(())
255}
256
257async fn generate_contract_input() -> anyhow::Result<ContractInput> {
258 let contract_descriptor = ContractDescriptor::Enum(EnumDescriptor {
259 outcome_payouts: vec![
260 EnumerationPayout {
261 outcome: "CTV".to_string(),
262 payout: Payout {
263 offer: Amount::from_sat(21_000_000),
264 accept: Amount::ZERO,
265 },
266 },
267 EnumerationPayout {
268 outcome: "CAT".to_string(),
269 payout: Payout {
270 offer: Amount::ZERO,
271 accept: Amount::from_sat(21_000_000),
272 },
273 },
274 ],
275 });
276 let logger = Arc::new(Logger::console(
277 "generate_contract_input".to_string(),
278 LogLevel::Info,
279 ));
280
281 let kormir = KormirOracleClient::new("https://kormir.dlcdevkit.com", None, logger).await?;
282
283 let expiry = (chrono::Utc::now()
284 .checked_add_signed(TimeDelta::minutes(15))
285 .unwrap()
286 .timestamp()) as u32;
287
288 let announcement = kormir
289 .create_enum_event(vec!["CTV".to_string(), "CAT".to_string()], expiry)
290 .await?;
291
292 let oracle_input = OracleInput {
293 public_keys: vec![kormir.get_public_key()],
294 event_id: announcement.oracle_event.event_id,
295 threshold: 1,
296 };
297
298 Ok(ContractInput {
299 offer_collateral: Amount::from_sat(10_500_000),
300 accept_collateral: Amount::from_sat(10_500_000),
301 fee_rate: 1,
302 contract_infos: vec![ContractInputInfo {
303 contract_descriptor,
304 oracles: oracle_input,
305 }],
306 })
307}
308
309async fn interactive_contract_input(
310 client: &mut DdkRpcClient<Channel>,
311) -> anyhow::Result<ContractInput> {
312 let contract_type =
313 Select::new("Select type of contract.", vec!["enum", "numerical"]).prompt()?;
314
315 let event_id = Text::new("Oracle announcement event id:").prompt()?;
316
317 let announcement = client
318 .oracle_announcements(OracleAnnouncementsRequest { event_id })
319 .await?
320 .into_inner();
321
322 let selected_announcement: OracleAnnouncement =
323 serde_json::from_slice(&announcement.announcement)?;
324
325 let contract_input = match contract_type {
326 "numerical" => {
327 let offer_collateral: u64 =
328 Text::new("Collateral from you (sats):").prompt()?.parse()?;
329 let accept_collateral: u64 = Text::new("Collateral from counterparty (sats):")
330 .prompt()?
331 .parse()?;
332 let fee_rate: u64 = Text::new("Fee rate (sats/vbyte):").prompt()?.parse()?;
333 let min_price: u64 = Text::new("Minimum Bitcoin price:").prompt()?.parse()?;
334 let max_price: u64 = Text::new("Maximum Bitcoin price:").prompt()?.parse()?;
335 let num_steps: u64 = Text::new("Number of rounding steps:").prompt()?.parse()?;
336 let oracle_pubkey = Text::new("Oracle public key:").prompt()?;
337 let event_id = Text::new("Oracle event id:").prompt()?;
338 ddk_payouts::create_contract_input(
339 min_price,
340 max_price,
341 num_steps,
342 Amount::from_sat(offer_collateral),
343 Amount::from_sat(accept_collateral),
344 fee_rate,
345 oracle_pubkey,
346 event_id,
347 )
348 }
349 "enum" => {
350 let offer_collateral: u64 =
351 Text::new("Collateral from you (sats):").prompt()?.parse()?;
352 let accept_collateral: u64 = Text::new("Collateral from your counterparty (sats):")
353 .prompt()?
354 .parse()?;
355 let outcomes = match &selected_announcement.oracle_event.event_descriptor {
356 EventDescriptor::EnumEvent(e) => e.outcomes.clone(),
357 _ => return Err(anyhow!("Not an enum event from announcement.")),
358 };
359 let mut outcome_payouts = Vec::with_capacity(outcomes.len());
360 println!("Specify the payouts for each outcome.");
361 for outcome in outcomes {
362 println!("> Event outcome: {outcome}");
363 let offer: u64 = Text::new("Your payout:").prompt()?.parse()?;
364 let accept: u64 = Text::new("Counterparty payout:").prompt()?.parse()?;
365 let outcome_payout = EnumerationPayout {
366 outcome,
367 payout: Payout {
368 offer: Amount::from_sat(offer),
369 accept: Amount::from_sat(accept),
370 },
371 };
372 outcome_payouts.push(outcome_payout);
373 }
374 let fee_rate: u64 = Text::new("Fee rate (sats/vbyte):").prompt()?.parse()?;
375 ddk_payouts::enumeration::create_contract_input(
377 outcome_payouts,
378 Amount::from_sat(offer_collateral),
379 Amount::from_sat(accept_collateral),
380 fee_rate,
381 selected_announcement.oracle_public_key.to_string(),
382 selected_announcement.oracle_event.event_id.clone(),
383 )
384 }
385 _ => return Err(anyhow!("Invalid contract type.")),
386 };
387
388 Ok(contract_input)
389}