ddk_node/
command.rs

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