drk/
cli_util.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
/* This file is part of DarkFi (https://dark.fi)
 *
 * Copyright (C) 2020-2024 Dyne.org foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
use std::{
    io::{stdin, Cursor, Read},
    process::exit,
    str::FromStr,
};

use rodio::{source::Source, Decoder, OutputStream};
use structopt_toml::clap::{App, Arg, Shell, SubCommand};

use darkfi::{
    cli_desc,
    system::sleep,
    tx::Transaction,
    util::{encoding::base64, parse::decode_base10},
    Error, Result,
};
use darkfi_money_contract::model::TokenId;
use darkfi_serial::deserialize_async;

use crate::{money::BALANCE_BASE10_DECIMALS, Drk};

/// Auxiliary function to parse a base64 encoded transaction from stdin.
pub async fn parse_tx_from_stdin() -> Result<Transaction> {
    let mut buf = String::new();
    stdin().read_to_string(&mut buf)?;
    let Some(bytes) = base64::decode(buf.trim()) else {
        eprintln!("Failed to decode transaction");
        exit(2);
    };

    Ok(deserialize_async(&bytes).await?)
}

/// Auxiliary function to parse provided string into a values pair.
pub fn parse_value_pair(s: &str) -> Result<(u64, u64)> {
    let v: Vec<&str> = s.split(':').collect();
    if v.len() != 2 {
        eprintln!("Invalid value pair. Use a pair such as 13.37:11.0");
        exit(2);
    }

    let val0 = decode_base10(v[0], BALANCE_BASE10_DECIMALS, true);
    let val1 = decode_base10(v[1], BALANCE_BASE10_DECIMALS, true);

    if val0.is_err() || val1.is_err() {
        eprintln!("Invalid value pair. Use a pair such as 13.37:11.0");
        exit(2);
    }

    Ok((val0.unwrap(), val1.unwrap()))
}

/// Auxiliary function to parse provided string into a tokens pair.
pub async fn parse_token_pair(drk: &Drk, s: &str) -> Result<(TokenId, TokenId)> {
    let v: Vec<&str> = s.split(':').collect();
    if v.len() != 2 {
        eprintln!("Invalid token pair. Use a pair such as:");
        eprintln!("WCKD:MLDY");
        eprintln!("or");
        eprintln!("A7f1RKsCUUHrSXA7a9ogmwg8p3bs6F47ggsW826HD4yd:FCuoMii64H5Ee4eVWBjP18WTFS8iLUJmGi16Qti1xFQ2");
        exit(2);
    }

    let tok0 = drk.get_token(v[0].to_string()).await;
    let tok1 = drk.get_token(v[1].to_string()).await;

    if tok0.is_err() || tok1.is_err() {
        eprintln!("Invalid token pair. Use a pair such as:");
        eprintln!("WCKD:MLDY");
        eprintln!("or");
        eprintln!("A7f1RKsCUUHrSXA7a9ogmwg8p3bs6F47ggsW826HD4yd:FCuoMii64H5Ee4eVWBjP18WTFS8iLUJmGi16Qti1xFQ2");
        exit(2);
    }

    Ok((tok0.unwrap(), tok1.unwrap()))
}

/// Fun police go away
pub async fn kaching() {
    const WALLET_MP3: &[u8] = include_bytes!("../wallet.mp3");

    let cursor = Cursor::new(WALLET_MP3);

    let Ok((_stream, stream_handle)) = OutputStream::try_default() else { return };

    let Ok(source) = Decoder::new(cursor) else { return };

    if stream_handle.play_raw(source.convert_samples()).is_err() {
        return
    }

    sleep(2).await;
}

/// Auxiliary function to generate provided shell completions.
pub fn generate_completions(shell: &str) -> Result<()> {
    // Sub-commands

    // Kaching
    let kaching = SubCommand::with_name("kaching").about("Fun");

    // Ping
    let ping =
        SubCommand::with_name("ping").about("Send a ping request to the darkfid RPC endpoint");

    // Completions
    let shell_arg = Arg::with_name("shell").help("The Shell you want to generate script for");

    let completions = SubCommand::with_name("completions")
        .about("Generate a SHELL completion script and print to stdout")
        .arg(shell_arg);

    // Wallet
    let initialize =
        Arg::with_name("initialize").long("initialize").help("Initialize wallet database");

    let keygen =
        Arg::with_name("keygen").long("keygen").help("Generate a new keypair in the wallet");

    let balance =
        Arg::with_name("balance").long("balance").help("Query the wallet for known balances");

    let address =
        Arg::with_name("address").long("address").help("Get the default address in the wallet");

    let addresses =
        Arg::with_name("addresses").long("addresses").help("Print all the addresses in the wallet");

    let default_address = Arg::with_name("default-address")
        .long("default-address")
        .takes_value(true)
        .help("Set the default address in the wallet");

    let secrets =
        Arg::with_name("secrets").long("secrets").help("Print all the secret keys from the wallet");

    let import_secrets = Arg::with_name("import-secrets")
        .long("import-secrets")
        .help("Import secret keys from stdin into the wallet, separated by newlines");

    let tree = Arg::with_name("tree").long("tree").help("Print the Merkle tree in the wallet");

    let coins = Arg::with_name("coins").long("coins").help("Print all the coins in the wallet");

    let wallet = SubCommand::with_name("wallet").about("Wallet operations").args(&vec![
        initialize,
        keygen,
        balance,
        address,
        addresses,
        default_address,
        secrets,
        import_secrets,
        tree,
        coins,
    ]);

    // Spend
    let spend = SubCommand::with_name("spend")
        .about("Read a transaction from stdin and mark its input coins as spent");

    // Unspend
    let coin = Arg::with_name("coin").help("base58-encoded coin to mark as unspent");

    let unspend = SubCommand::with_name("unspend").about("Unspend a coin").arg(coin);

    // Transfer
    let amount = Arg::with_name("amount").help("Amount to send");

    let token = Arg::with_name("token").help("Token ID to send");

    let recipient = Arg::with_name("recipient").help("Recipient address");

    let spend_hook = Arg::with_name("spend-hook").help("Optional contract spend hook to use");

    let user_data = Arg::with_name("user-data").help("Optional user data to use");

    let half_split = Arg::with_name("half-split")
        .long("half-split")
        .help("Split the output coin into two equal halves");

    let transfer =
        SubCommand::with_name("transfer").about("Create a payment transaction").args(&vec![
            amount.clone(),
            token.clone(),
            recipient.clone(),
            spend_hook.clone(),
            user_data.clone(),
            half_split,
        ]);

    // Otc
    let value_pair = Arg::with_name("value-pair")
        .short("v")
        .long("value-pair")
        .takes_value(true)
        .help("Value pair to send:recv (11.55:99.42)");

    let token_pair = Arg::with_name("token-pair")
        .short("t")
        .long("token-pair")
        .takes_value(true)
        .help("Token pair to send:recv (f00:b4r)");

    let init = SubCommand::with_name("init")
        .about("Initialize the first half of the atomic swap")
        .args(&vec![value_pair, token_pair]);

    let join =
        SubCommand::with_name("join").about("Build entire swap tx given the first half from stdin");

    let inspect = SubCommand::with_name("inspect")
        .about("Inspect a swap half or the full swap tx from stdin");

    let sign = SubCommand::with_name("sign").about("Sign a swap transaction given from stdin");

    let otc = SubCommand::with_name("otc")
        .about("OTC atomic swap")
        .subcommands(vec![init, join, inspect, sign]);

    // AttachFee
    let attach_fee = SubCommand::with_name("attach-fee")
        .about("Attach the fee call to a transaction given from stdin");

    // Inspect
    let inspect = SubCommand::with_name("inspect").about("Inspect a transaction from stdin");

    // Broadcast
    let broadcast =
        SubCommand::with_name("broadcast").about("Read a transaction from stdin and broadcast it");

    // Subscribe
    let subscribe = SubCommand::with_name("subscribe").about(
        "This subscription will listen for incoming blocks from darkfid and look \
                    through their transactions to see if there's any that interest us. \
                    With `drk` we look at transactions calling the money contract so we can \
                    find coins sent to us and fill our wallet with the necessary metadata.",
    );

    // DAO
    let proposer_limit = Arg::with_name("proposer-limit")
        .help("The minimum amount of governance tokens needed to open a proposal for this DAO");

    let quorum = Arg::with_name("quorum")
        .help("Minimal threshold of participating total tokens needed for a proposal to pass");

    let approval_ratio = Arg::with_name("approval-ratio")
        .help("The ratio of winning votes/total votes needed for a proposal to pass (2 decimals)");

    let gov_token_id = Arg::with_name("gov-token-id").help("DAO's governance token ID");

    let create = SubCommand::with_name("create").about("Create DAO parameters").args(&vec![
        proposer_limit,
        quorum,
        approval_ratio,
        gov_token_id,
    ]);

    let view = SubCommand::with_name("view").about("View DAO data from stdin");

    let name = Arg::with_name("name").help("Name identifier for the DAO");

    let import = SubCommand::with_name("import")
        .about("Import DAO data from stdin")
        .args(&vec![name.clone()]);

    let opt_name = Arg::with_name("dao-alias").help("Name identifier for the DAO (optional)");

    let list = SubCommand::with_name("list")
        .about("List imported DAOs (or info about a specific one)")
        .args(&vec![opt_name]);

    let balance = SubCommand::with_name("balance")
        .about("Show the balance of a DAO")
        .args(&vec![name.clone()]);

    let mint = SubCommand::with_name("mint")
        .about("Mint an imported DAO on-chain")
        .args(&vec![name.clone()]);

    let duration = Arg::with_name("duration").help("Duration of the proposal, in block windows");

    let propose_transfer = SubCommand::with_name("propose-transfer")
        .about("Create a transfer proposal for a DAO")
        .args(&vec![
            name.clone(),
            duration,
            amount,
            token,
            recipient,
            spend_hook.clone(),
            user_data.clone(),
        ]);

    let proposals =
        SubCommand::with_name("proposals").about("List DAO proposals").args(&vec![name]);

    let bulla = Arg::with_name("bulla").help("Bulla identifier for the proposal");

    let export = Arg::with_name("export").help("Encrypt the proposal and encode it to base64");

    let mint_proposal = Arg::with_name("mint-proposal").help("Create the proposal transaction");

    let proposal = SubCommand::with_name("proposal").about("View a DAO proposal data").args(&vec![
        bulla.clone(),
        export,
        mint_proposal,
    ]);

    let proposal_import = SubCommand::with_name("proposal-import")
        .about("Import a base64 encoded and encrypted proposal from stdin");

    let vote = Arg::with_name("vote").help("Vote (0 for NO, 1 for YES)");

    let vote_weight =
        Arg::with_name("vote-weight").help("Optional vote weight (amount of governance tokens)");

    let vote = SubCommand::with_name("vote").about("Vote on a given proposal").args(&vec![
        bulla.clone(),
        vote,
        vote_weight,
    ]);

    let exec = SubCommand::with_name("exec").about("Execute a DAO proposal").args(&vec![bulla]);

    let spend_hook_cmd = SubCommand::with_name("spend-hook")
        .about("Print the DAO contract base58-encoded spend hook");

    let dao = SubCommand::with_name("dao").about("DAO functionalities").subcommands(vec![
        create,
        view,
        import,
        list,
        balance,
        mint,
        propose_transfer,
        proposals,
        proposal,
        proposal_import,
        vote,
        exec,
        spend_hook_cmd,
    ]);

    // Scan
    let reset = Arg::with_name("reset")
        .long("reset")
        .help("Reset wallet state to provided block height and start scanning");

    let scan = SubCommand::with_name("scan")
        .about("Scan the blockchain and parse relevant transactions")
        .args(&vec![reset]);

    // Explorer
    let tx_hash = Arg::with_name("tx-hash").help("Transaction hash");

    let full = Arg::with_name("full").long("full").help("Print the full transaction information");

    let encode = Arg::with_name("encode").long("encode").help("Encode transaction to base58");

    let fetch_tx = SubCommand::with_name("fetch-tx")
        .about("Fetch a blockchain transaction by hash")
        .args(&vec![tx_hash, full, encode]);

    let simulate_tx =
        SubCommand::with_name("simulate-tx").about("Read a transaction from stdin and simulate it");

    let tx_hash = Arg::with_name("tx-hash").help("Fetch specific history record (optional)");

    let encode = Arg::with_name("encode")
        .long("encode")
        .help("Encode specific history record transaction to base58");

    let txs_history = SubCommand::with_name("txs-history")
        .about("Fetch broadcasted transactions history")
        .args(&vec![tx_hash, encode]);

    let clear_reverted =
        SubCommand::with_name("clear-reverted").about("Remove reverted transactions from history");

    let height = Arg::with_name("height").help("Fetch specific height record (optional)");

    let scanned_blocks = SubCommand::with_name("scanned-blocks")
        .about("Fetch scanned blocks records")
        .args(&vec![height]);

    let explorer = SubCommand::with_name("explorer")
        .about("Explorer related subcommands")
        .subcommands(vec![fetch_tx, simulate_tx, txs_history, clear_reverted, scanned_blocks]);

    // Alias
    let alias = Arg::with_name("alias").help("Token alias");

    let token = Arg::with_name("token").help("Token to create alias for");

    let add = SubCommand::with_name("add").about("Create a Token alias").args(&vec![alias, token]);

    let alias = Arg::with_name("alias")
        .short("a")
        .long("alias")
        .takes_value(true)
        .help("Token alias to search for");

    let token = Arg::with_name("token")
        .short("t")
        .long("token")
        .takes_value(true)
        .help("Token to search alias for");

    let show = SubCommand::with_name("show")
        .about(
            "Print alias info of optional arguments. \
                    If no argument is provided, list all the aliases in the wallet.",
        )
        .args(&vec![alias, token]);

    let alias = Arg::with_name("alias").help("Token alias to remove");

    let remove = SubCommand::with_name("remove").about("Remove a Token alias").arg(alias);

    let alias = SubCommand::with_name("alias")
        .about("Manage Token aliases")
        .subcommands(vec![add, show, remove]);

    // Token
    let secret_key = Arg::with_name("secret-key").help("Mint authority secret key");

    let token_blind = Arg::with_name("token-blind").help("Mint authority token blind");

    let import = SubCommand::with_name("import")
        .about("Import a mint authority")
        .args(&vec![secret_key, token_blind]);

    let generate_mint =
        SubCommand::with_name("generate-mint").about("Generate a new mint authority");

    let list =
        SubCommand::with_name("list").about("List token IDs with available mint authorities");

    let token = Arg::with_name("token").help("Token ID to mint");

    let amount = Arg::with_name("amount").help("Amount to mint");

    let recipient = Arg::with_name("recipient").help("Recipient of the minted tokens");

    let mint = SubCommand::with_name("mint")
        .about("Mint tokens")
        .args(&vec![token, amount, recipient, spend_hook, user_data]);

    let token = Arg::with_name("token").help("Token ID to freeze");

    let freeze = SubCommand::with_name("freeze").about("Freeze a token mint").arg(token);

    let token = SubCommand::with_name("token").about("Token functionalities").subcommands(vec![
        import,
        generate_mint,
        list,
        mint,
        freeze,
    ]);

    // Main arguments
    let config = Arg::with_name("config")
        .short("c")
        .long("config")
        .takes_value(true)
        .help("Configuration file to use");

    let network = Arg::with_name("network")
        .long("network")
        .takes_value(true)
        .help("Blockchain network to use");

    let command = vec![
        kaching,
        ping,
        completions,
        wallet,
        spend,
        unspend,
        transfer,
        otc,
        attach_fee,
        inspect,
        broadcast,
        subscribe,
        dao,
        scan,
        explorer,
        alias,
        token,
    ];

    let fun = Arg::with_name("fun")
        .short("f")
        .long("fun")
        .help("Flag indicating whether you want some fun in your life");

    let log = Arg::with_name("log")
        .short("l")
        .long("log")
        .takes_value(true)
        .help("Set log file to ouput into");

    let verbose = Arg::with_name("verbose")
        .short("v")
        .multiple(true)
        .help("Increase verbosity (-vvv supported)");

    let mut app = App::new("drk")
        .about(cli_desc!())
        .args(&vec![config, network, fun, log, verbose])
        .subcommands(command);

    let shell = match Shell::from_str(shell) {
        Ok(s) => s,
        Err(e) => return Err(Error::Custom(e)),
    };

    app.gen_completions_to("./drk", shell, &mut std::io::stdout());

    Ok(())
}