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 ],
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}