Skip to main content

ddk_node/
command.rs

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            // TODO: list possible events.
376            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}