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_flags: 0,
303 contract_infos: vec![ContractInputInfo {
304 contract_descriptor,
305 oracles: oracle_input,
306 }],
307 })
308}
309
310async fn interactive_contract_input(
311 client: &mut DdkRpcClient<Channel>,
312) -> anyhow::Result<ContractInput> {
313 let contract_type =
314 Select::new("Select type of contract.", vec!["enum", "numerical"]).prompt()?;
315
316 let event_id = Text::new("Oracle announcement event id:").prompt()?;
317
318 let announcement = client
319 .oracle_announcements(OracleAnnouncementsRequest { event_id })
320 .await?
321 .into_inner();
322
323 let selected_announcement: OracleAnnouncement =
324 serde_json::from_slice(&announcement.announcement)?;
325
326 let contract_input = match contract_type {
327 "numerical" => {
328 let offer_collateral: u64 =
329 Text::new("Collateral from you (sats):").prompt()?.parse()?;
330 let accept_collateral: u64 = Text::new("Collateral from counterparty (sats):")
331 .prompt()?
332 .parse()?;
333 let fee_rate: u64 = Text::new("Fee rate (sats/vbyte):").prompt()?.parse()?;
334 let min_price: u64 = Text::new("Minimum Bitcoin price:").prompt()?.parse()?;
335 let max_price: u64 = Text::new("Maximum Bitcoin price:").prompt()?.parse()?;
336 let num_steps: u64 = Text::new("Number of rounding steps:").prompt()?.parse()?;
337 let oracle_pubkey = Text::new("Oracle public key:").prompt()?;
338 let event_id = Text::new("Oracle event id:").prompt()?;
339 ddk_payouts::create_contract_input(
340 min_price,
341 max_price,
342 num_steps,
343 Amount::from_sat(offer_collateral),
344 Amount::from_sat(accept_collateral),
345 fee_rate,
346 oracle_pubkey,
347 event_id,
348 )
349 }
350 "enum" => {
351 let offer_collateral: u64 =
352 Text::new("Collateral from you (sats):").prompt()?.parse()?;
353 let accept_collateral: u64 = Text::new("Collateral from your counterparty (sats):")
354 .prompt()?
355 .parse()?;
356 let outcomes = match &selected_announcement.oracle_event.event_descriptor {
357 EventDescriptor::EnumEvent(e) => e.outcomes.clone(),
358 _ => return Err(anyhow!("Not an enum event from announcement.")),
359 };
360 let mut outcome_payouts = Vec::with_capacity(outcomes.len());
361 println!("Specify the payouts for each outcome.");
362 for outcome in outcomes {
363 println!("> Event outcome: {outcome}");
364 let offer: u64 = Text::new("Your payout:").prompt()?.parse()?;
365 let accept: u64 = Text::new("Counterparty payout:").prompt()?.parse()?;
366 let outcome_payout = EnumerationPayout {
367 outcome,
368 payout: Payout {
369 offer: Amount::from_sat(offer),
370 accept: Amount::from_sat(accept),
371 },
372 };
373 outcome_payouts.push(outcome_payout);
374 }
375 let fee_rate: u64 = Text::new("Fee rate (sats/vbyte):").prompt()?.parse()?;
376 ddk_payouts::enumeration::create_contract_input(
378 outcome_payouts,
379 Amount::from_sat(offer_collateral),
380 Amount::from_sat(accept_collateral),
381 fee_rate,
382 selected_announcement.oracle_public_key.to_string(),
383 selected_announcement.oracle_event.event_id.clone(),
384 )
385 }
386 _ => return Err(anyhow!("Invalid contract type.")),
387 };
388
389 Ok(contract_input)
390}