kaspa_cli_lib/modules/
account.rs

1use kaspa_wallet_core::account::BIP32_ACCOUNT_KIND;
2use kaspa_wallet_core::account::LEGACY_ACCOUNT_KIND;
3use kaspa_wallet_core::account::MULTISIG_ACCOUNT_KIND;
4
5use crate::imports::*;
6use crate::wizards;
7
8#[derive(Default, Handler)]
9#[help("Account management operations")]
10pub struct Account;
11
12impl Account {
13    async fn main(self: Arc<Self>, ctx: &Arc<dyn Context>, mut argv: Vec<String>, _cmd: &str) -> Result<()> {
14        let ctx = ctx.clone().downcast_arc::<KaspaCli>()?;
15        let wallet = ctx.wallet();
16
17        if !wallet.is_open() {
18            return Err(Error::WalletIsNotOpen);
19        }
20
21        if argv.is_empty() {
22            return self.display_help(ctx, argv).await;
23        }
24
25        let action = argv.remove(0);
26
27        match action.as_str() {
28            "name" => {
29                if argv.len() != 1 {
30                    tprintln!(ctx, "usage: 'account name <name>' or 'account name remove'");
31                    return Ok(());
32                } else {
33                    let (wallet_secret, _) = ctx.ask_wallet_secret(None).await?;
34                    let _ = ctx.notifier().show(Notification::Processing).await;
35                    let account = ctx.select_account().await?;
36                    let name = argv.remove(0);
37                    if name == "remove" {
38                        account.rename(&wallet_secret, None).await?;
39                    } else {
40                        account.rename(&wallet_secret, Some(name.as_str())).await?;
41                    }
42                }
43            }
44            "create" => {
45                let account_kind = if argv.is_empty() {
46                    BIP32_ACCOUNT_KIND.into()
47                } else {
48                    let kind = argv.remove(0);
49                    kind.parse::<AccountKind>()?
50                };
51
52                let account_name = if argv.is_empty() {
53                    None
54                } else {
55                    let name = argv.remove(0);
56                    let name = name.trim().to_string();
57
58                    Some(name)
59                };
60
61                let prv_key_data_info = ctx.select_private_key().await?;
62
63                let account_name = account_name.as_deref();
64                wizards::account::create(&ctx, prv_key_data_info, account_kind, account_name).await?;
65            }
66            "import" => {
67                if argv.is_empty() {
68                    tprintln!(ctx, "usage: 'account import <import-type> <key-type> [extra keys]'");
69                    tprintln!(ctx, "");
70                    tprintln!(ctx, "examples:");
71                    tprintln!(ctx, "");
72                    ctx.term().help(
73                        &[
74                            ("account import legacy-data", "Import KDX keydata file or kaspanet web wallet data on the same domain"),
75                            (
76                                "account import mnemonic bip32",
77                                "Import Bip32 (12 or 24 word mnemonics used by kaspawallet, kaspium, onekey, tangem etc.)",
78                            ),
79                            (
80                                "account import mnemonic legacy",
81                                "Import accounts 12 word mnemonic used by legacy applications (KDX and kaspanet web wallet)",
82                            ),
83                            (
84                                "account import mnemonic multisig [additional keys]",
85                                "Import mnemonic and additional keys for a multisig account",
86                            ),
87                        ],
88                        None,
89                    )?;
90
91                    return Ok(());
92                }
93
94                let import_kind = argv.remove(0);
95                match import_kind.as_ref() {
96                    "legacy-data" => {
97                        if !argv.is_empty() {
98                            tprintln!(ctx, "usage: 'account import legacy-data'");
99                            tprintln!(ctx, "too many arguments: {}\r\n", argv.join(" "));
100                            return Ok(());
101                        }
102
103                        if exists_legacy_v0_keydata().await? {
104                            let import_secret = Secret::new(
105                                ctx.term()
106                                    .ask(true, "Enter the password for the account you are importing: ")
107                                    .await?
108                                    .trim()
109                                    .as_bytes()
110                                    .to_vec(),
111                            );
112                            let wallet_secret =
113                                Secret::new(ctx.term().ask(true, "Enter wallet password: ").await?.trim().as_bytes().to_vec());
114                            let ctx_ = ctx.clone();
115                            wallet
116                                .import_legacy_keydata(
117                                    &import_secret,
118                                    &wallet_secret,
119                                    None,
120                                    Some(Arc::new(move |processed: usize, _, balance, txid| {
121                                        if let Some(txid) = txid {
122                                            tprintln!(
123                                                ctx_,
124                                                "Scan detected {} KAS at index {}; transfer txid: {}",
125                                                sompi_to_kaspa_string(balance),
126                                                processed,
127                                                txid
128                                            );
129                                        } else if processed > 0 {
130                                            tprintln!(
131                                                ctx_,
132                                                "Scanned {} derivations, found {} KAS",
133                                                processed,
134                                                sompi_to_kaspa_string(balance)
135                                            );
136                                        } else {
137                                            tprintln!(ctx_, "Please wait... scanning for account UTXOs...");
138                                        }
139                                    })),
140                                )
141                                .await?;
142                        } else if application_runtime::is_web() {
143                            return Err("'kaspanet' web wallet storage not found at this domain name".into());
144                        } else {
145                            return Err("KDX keydata file not found".into());
146                        }
147                    }
148                    "mnemonic" => {
149                        if argv.is_empty() {
150                            tprintln!(ctx, "usage: 'account import mnemonic <bip32|legacy|multisig>'");
151                            tprintln!(ctx, "please specify the mnemonic type");
152                            tprintln!(ctx, "please use 'legacy' for 12-word KDX and kaspanet web wallet mnemonics\r\n");
153                            return Ok(());
154                        }
155
156                        let account_kind = argv.remove(0);
157                        let account_kind = account_kind.parse::<AccountKind>()?;
158
159                        match account_kind.as_ref() {
160                            LEGACY_ACCOUNT_KIND | BIP32_ACCOUNT_KIND => {
161                                if !argv.is_empty() {
162                                    tprintln!(ctx, "too many arguments: {}\r\n", argv.join(" "));
163                                    return Ok(());
164                                }
165                                crate::wizards::import::import_with_mnemonic(&ctx, account_kind, &argv).await?;
166                            }
167                            MULTISIG_ACCOUNT_KIND => {
168                                crate::wizards::import::import_with_mnemonic(&ctx, account_kind, &argv).await?;
169                            }
170                            _ => {
171                                tprintln!(ctx, "account import is not supported for this account type: '{account_kind}'\r\n");
172                                return Ok(());
173                            }
174                        }
175
176                        return Ok(());
177                    }
178                    _ => {
179                        tprintln!(ctx, "unknown account import type: '{import_kind}'");
180                        tprintln!(ctx, "supported import types are: 'mnemonic', 'legacy-data' or 'multisig-watch'\r\n");
181                        return Ok(());
182                    }
183                }
184            }
185            "watch" => {
186                if argv.is_empty() {
187                    tprintln!(ctx, "usage: 'account watch <watch-type> [account name]'");
188                    tprintln!(ctx, "");
189                    tprintln!(ctx, "examples:");
190                    tprintln!(ctx, "");
191                    ctx.term().help(
192                        &[
193                            ("account watch bip32", "Import a extended public key for a watch-only bip32 account"),
194                            ("account watch multisig", "Import extended public keys for a watch-only multisig account"),
195                        ],
196                        None,
197                    )?;
198
199                    return Ok(());
200                }
201
202                let watch_kind = argv.remove(0);
203
204                let account_name = argv.first().map(|name| name.trim()).filter(|name| !name.is_empty()).map(|name| name.to_string());
205
206                let account_name = account_name.as_deref();
207
208                match watch_kind.as_ref() {
209                    "bip32" => {
210                        wizards::account::bip32_watch(&ctx, account_name).await?;
211                    }
212                    "multisig" => {
213                        wizards::account::multisig_watch(&ctx, account_name).await?;
214                    }
215                    _ => {
216                        tprintln!(ctx, "unknown account watch type: '{watch_kind}'");
217                        tprintln!(ctx, "supported watch types are: 'bip32' or 'multisig'\r\n");
218                        return Ok(());
219                    }
220                }
221            }
222            "scan" | "sweep" => {
223                let len = argv.len();
224                let mut start = 0;
225                let mut count = 100_000;
226                let window = 128;
227                if len >= 2 {
228                    start = argv.remove(0).parse::<usize>()?;
229                    count = argv.remove(0).parse::<usize>()?;
230                } else if len == 1 {
231                    count = argv.remove(0).parse::<usize>()?;
232                }
233
234                count = count.max(1);
235
236                let sweep = action.eq("sweep");
237
238                self.derivation_scan(&ctx, start, count, window, sweep).await?;
239            }
240            v => {
241                tprintln!(ctx, "unknown command: '{v}'\r\n");
242                return self.display_help(ctx, argv).await;
243            }
244        }
245
246        Ok(())
247    }
248
249    async fn display_help(self: Arc<Self>, ctx: Arc<KaspaCli>, _argv: Vec<String>) -> Result<()> {
250        ctx.term().help(
251            &[
252                ("create [<type>] [<name>]", "Create a new account (types: 'bip32' (default), 'legacy', 'multisig')"),
253                (
254                    "import <import-type> [<key-type> [extra keys]]",
255                    "Import accounts from a private key using 24 or 12 word mnemonic or legacy data \
256                (KDX and kaspanet web wallet). Use 'account import' for additional help.",
257                ),
258                ("name <name>", "Name or rename the selected account (use 'remove' to remove the name"),
259                ("scan [<derivations>] or scan [<start>] [<derivations>]", "Scan extended address derivation chain (legacy accounts)"),
260                (
261                    "sweep [<derivations>] or sweep [<start>] [<derivations>]",
262                    "Sweep extended address derivation chain (legacy accounts)",
263                ),
264                // ("purge", "Purge an account from the wallet"),
265            ],
266            None,
267        )?;
268
269        Ok(())
270    }
271
272    async fn derivation_scan(
273        self: &Arc<Self>,
274        ctx: &Arc<KaspaCli>,
275        start: usize,
276        count: usize,
277        window: usize,
278        sweep: bool,
279    ) -> Result<()> {
280        let account = ctx.account().await?;
281        let (wallet_secret, payment_secret) = ctx.ask_wallet_secret(Some(&account)).await?;
282        let _ = ctx.notifier().show(Notification::Processing).await;
283        let abortable = Abortable::new();
284        let ctx_ = ctx.clone();
285
286        let account = account.as_derivation_capable()?;
287
288        account
289            .derivation_scan(
290                wallet_secret,
291                payment_secret,
292                start,
293                start + count,
294                window,
295                sweep,
296                &abortable,
297                Some(Arc::new(move |processed: usize, _, balance, txid| {
298                    if let Some(txid) = txid {
299                        tprintln!(
300                            ctx_,
301                            "Scan detected {} KAS at index {}; transfer txid: {}",
302                            sompi_to_kaspa_string(balance),
303                            processed,
304                            txid
305                        );
306                    } else {
307                        tprintln!(ctx_, "Scanned {} derivations, found {} KAS", processed, sompi_to_kaspa_string(balance));
308                    }
309                })),
310            )
311            .await?;
312
313        Ok(())
314    }
315}