1 module tagion.tools.wallet.geldbeutel;
2 import core.thread;
3 import std.algorithm;
4 import std.array;
5 import std.file : exists, mkdir;
6 import std.format;
7 import std.getopt;
8 import std.path;
9 import std.range;
10 import std.stdio;
11 import std.typecons;
12 import std.exception : ifThrown;
13 import core.stdc.stdio : printf;
14 import tagion.basic.Message;
15 import tagion.basic.Types : FileExtension, hasExtension;
16 import tagion.basic.tagionexceptions;
17 import tagion.hibon.Document;
18 import tagion.hibon.HiBONFile : fread, fwrite;
19 import tagion.hibon.HiBONRecord : isHiBONRecord;
20 import tagion.communication.HiRPC;
21 import tagion.network.ReceiveBuffer;
22 import tagion.script.TagionCurrency;
23 import tagion.tools.Basic;
24 import tagion.tools.revision;
25 import tagion.tools.wallet.WalletInterface;
26 import tagion.tools.wallet.WalletOptions;
27 import tagion.utils.Term;
28 import tagion.wallet.AccountDetails;
29 import tagion.wallet.KeyRecover;
30 import tagion.wallet.SecureWallet;
31 import tagion.wallet.WalletRecords;
32 import tagion.wallet.BIP39;
33 import tagion.basic.Types : encodeBase64;
34 import tagion.hibon.HiBONException;
35 
36 mixin Main!(_main, "wallet");
37 
38 import tagion.crypto.SecureNet;
39 import Wallet = tagion.wallet.SecureWallet;
40 
41 /**
42  * @brief build file path if needed file with folder long path
43  * @param file - input/output parameter with filename
44  * @param path - forlders destination to file
45  */
46 @safe
47 static void set_path(ref string file, string path) {
48     file = buildPath(path, file.baseName);
49 }
50 
51 int _main(string[] args) {
52     import tagion.wallet.SecureWallet : check;
53 
54     immutable program = args[0];
55     bool version_switch;
56     bool overwrite_switch; /// Overwrite the config file
57     bool create_account;
58     bool change_pin;
59 
60     string path;
61     string pincode;
62     uint bip39;
63     bool bip39_recover;
64     bool wallet_ui;
65     bool show_info;
66     bool pubkey_info;
67     bool list;
68     bool sum;
69     string _passphrase;
70     string _salt;
71     char[] passphrase;
72     char[] salt;
73     string account_name;
74     scope (exit) {
75         passphrase[] = 0;
76         salt[] = 0;
77     }
78     GetoptResult main_args;
79     WalletOptions options;
80     WalletInterface.Switch wallet_switch;
81     const user_config_file = args
82         .countUntil!(file => file.hasExtension(FileExtension.json) && file.exists);
83     auto config_file = (user_config_file < 0) ? default_wallet_config_filename : args[user_config_file];
84     if (config_file.exists) {
85         options.load(config_file);
86     }
87     else {
88         options.setDefault;
89     }
90 
91     try {
92         main_args = getopt(args, std.getopt.config.caseSensitive,
93                 std.getopt.config.bundling,
94                 "version", "display the version", &version_switch,
95                 "v|verbose", "Enable verbose print-out", &__verbose_switch,
96                 "O|overwrite", "Overwrite the config file and exits", &overwrite_switch,
97                 "path", format("Set the path for the wallet files : default %s", path), &path,
98                 "wallet", format("Wallet file : default %s", options.walletfile), &options.walletfile,
99                 "device", format("Device file : default %s", options.devicefile), &options.devicefile,
100                 "quiz", format("Quiz file : default %s", options.quizfile), &options.quizfile,
101                 "C|create", "Create a new account", &create_account,
102                 "c|changepin", "Change pin-code", &change_pin,
103                 "o|output", "Output filename", &wallet_switch.output_filename,
104                 "l|list", "List wallet content", &list,
105                 "s|sum", "Sum of the wallet", &sum,
106                 "send", "Send a contract to the shell", &wallet_switch.send,
107                 "sendkernel", "Send a contract to the kernel", &wallet_switch.sendkernel,
108                 "P|passphrase", "Set the wallet passphrase", &_passphrase,
109                 "create-invoice", "Create invoice by format LABEL:PRICE. Example: Foreign_invoice:1000", &wallet_switch
110                 .invoice,
111                 "x|pin", "Pincode", &pincode,
112                 "amount", "Create an payment request in tagion", &wallet_switch.amount,
113                 "force", "Force input bill", &wallet_switch.force,
114                 "pay", "Creates a payment contract", &wallet_switch.pay,
115                 "dry", "Dry-run this will not save the wallet", &__dry_switch,
116                 "req", "List all requested bills", &wallet_switch.request,
117                 "update", "Request a wallet updated", &wallet_switch.update,
118                 "trt-update", "Request a update on all derivers", &wallet_switch.trt_update,
119 
120                 "address", format(
121                     "Sets the address default: %s", options.contract_address),
122                 &options.addr,
123                 "faucet", "request money from the faucet", &wallet_switch.faucet,
124                 "bip39", "Generate bip39 set the number of words", &bip39,
125                 "recover", "Recover bip39 from word list", &bip39_recover,
126                 "name", "Sets the account name", &account_name,
127                 "info", "Prints the public key and the name of the account", &show_info,
128                 "pubkey", "Prints the public key", &pubkey_info,
129 
130         );
131         if (version_switch) {
132             revision_text.writeln;
133             return 0;
134         }
135         if (main_args.helpWanted) {
136             //            writeln(logo);
137             defaultGetoptPrinter(
138                     [
139                     // format("%s version %s", program, REVNO),
140                     "Documentation: https://tagion.org/",
141                     "",
142                     "Usage:",
143                     format("%s [<option>...] <config.json> <files>", program),
144                     "",
145 
146                     "<option>:",
147 
148                     ].join("\n"),
149                     main_args.options);
150             return 0;
151         }
152 
153         verbose("Config file %s", config_file);
154         const new_config = (!config_file.exists || overwrite_switch);
155         if (path) {
156             if (!new_config) {
157                 writefln("To change the path you need to use the overwrite switch -O");
158                 return 10;
159             }
160             options.walletfile.set_path(path);
161             options.quizfile.set_path(path);
162             options.devicefile.set_path(path);
163             options.accountfile.set_path(path);
164             options.billsfile.set_path(path);
165             options.paymentrequestsfile.set_path(path);
166             const dir = options.walletfile.dirName;
167             if (!dir.exists) {
168                 dir.mkdir;
169             }
170         }
171         if (new_config) {
172             const new_config_file = args
173                 .countUntil!(file => file.hasExtension(FileExtension.json) && !file.exists);
174             config_file = (new_config_file < 0) ? config_file : args[new_config_file];
175             options.save(config_file);
176             if (overwrite_switch) {
177                 return 0;
178             }
179         }
180         auto wallet_interface = WalletInterface(options);
181         if (!_salt.empty) {
182             auto salt_tmp = (() @trusted => cast(char[]) _salt)();
183             scope (exit) {
184                 salt_tmp[] = 0;
185             }
186             salt ~= WordList.presalt ~ _salt;
187         }
188         if (bip39 > 0 || bip39_recover) {
189             wallet_interface.load;
190             import std.uni;
191             import tagion.tools.secretinput;
192             import tagion.wallet.bip39_english : words;
193 
194             if (bip39_recover) {
195                 auto line = stdin.readln().dup;
196                 toLowerInPlace(line);
197                 auto word_list = line.split;
198                 passphrase = word_list.join(" ");
199                 bip39 = cast(uint) word_list.length;
200             }
201             const number_of_words = [12, 24];
202             check(number_of_words.canFind(bip39), format("Invalid number of word %d should be (%(%d, %))", bip39, number_of_words));
203             char[] pincode_1;
204             char[] pincode_2;
205             scope (exit) {
206                 pincode_1[] = 0;
207                 pincode_2[] = 0;
208             }
209 
210             if (!dry_switch) {
211                 info("Press ctrl-C to break");
212                 info("Press ctrl-A to show the pincode");
213                 while (pincode_1.length == 0) {
214                     const keycode = getSecret("Pincode: ", pincode_1);
215                     if (keycode == KeyStroke.KeyCode.CTRL_C) {
216                         error("Wallet has not been created");
217                         return 1;
218                     }
219                 }
220                 info("Repeat the pincode");
221                 for (;;) {
222                     const keycode = getSecret("Pincode: ", pincode_2);
223                     if (keycode == KeyStroke.KeyCode.CTRL_C) {
224                         error("Wallet has not been created");
225                         return 1;
226                     }
227                     if (pincode_1 == pincode_2) {
228                         break;
229                     }
230                     error("Pincode did not match");
231                 }
232                 good("Pin-codes matches");
233             }
234             if (!bip39_recover) {
235                 const wordlist = WordList(words);
236                 passphrase = wordlist.passphrase(bip39);
237 
238                 good("This is the recovery words");
239                 printf("%.*s\n", cast(int) passphrase.length, &passphrase[0]);
240                 good("Write them down");
241             }
242             const recovered = wallet_interface.generateSeedFromPassphrase(passphrase, pincode_1, _salt);
243             check(recovered, "Wallet was not recovered");
244             good("Wallet was recovered");
245             if (!dry_switch) {
246                 wallet_interface.save(false);
247             }
248             return 0;
249         }
250         else {
251             (() @trusted { passphrase = cast(char[]) _passphrase; }());
252         }
253         if (!passphrase.empty) {
254             check(!pincode.empty, "Missing pincode");
255             wallet_interface.generateSeedFromPassphrase(passphrase, pincode);
256             wallet_interface.save(false);
257             return 0;
258         }
259         if (!wallet_interface.load) {
260             create_account = true;
261             writefln("Wallet dont't exists");
262             WalletInterface.pressKey;
263         }
264         bool info_only;
265         if (show_info) {
266             if (wallet_interface.secure_wallet.account.name.empty) {
267                 writefln("%sAccount name has not been set (use --name)%s", YELLOW, RESET);
268                 return 0;
269             }
270             writefln("%s,%s",
271                     wallet_interface.secure_wallet.account.name,
272                     wallet_interface.secure_wallet.account.owner.encodeBase64);
273             info_only = true;
274         }
275         if (pubkey_info) {
276             if (wallet_interface.secure_wallet.account.owner.empty) {
277                 warn("Account pubkey has not been set (use --name)");
278                 return 0;
279             }
280             writefln("%s",
281                     wallet_interface.secure_wallet.account.owner.encodeBase64);
282             info_only = true;
283         }
284         if (list) {
285             const hash_net = new StdHashNet;
286             wallet_interface.listAccount(vout, hash_net);
287             wallet_interface.listInvoices(vout);
288             sum = true;
289         }
290         if (sum) {
291             wallet_interface.sumAccount(vout);
292             info_only = true;
293         }
294         if (info_only) {
295             return 0;
296         }
297 
298         if (create_account) {
299             wallet_interface.generateSeed(wallet_interface.quiz.questions, false);
300             return 0;
301         }
302         else if (change_pin) {
303             wallet_interface.loginPincode(changepin : true);
304             check(wallet_interface.secure_wallet.isLoggedin, "Failed to login");
305             good("Pincode correct");
306             return 0;
307         }
308 
309         if (wallet_interface.secure_wallet !is WalletInterface.StdSecureWallet.init) {
310             if (!pincode.empty) {
311                 const flag = wallet_interface.secure_wallet.login(pincode);
312 
313                 if (!flag) {
314                     error("Wrong pincode");
315                     return 3;
316                 }
317                 good("Loggedin");
318             }
319             else if (!wallet_interface.loginPincode(changepin : false)) {
320                 wallet_ui = true;
321                 warn("Wallet not loggedin");
322                 return 4;
323             }
324         }
325         foreach (file; args.filter!(file => file.hasExtension(FileExtension.hibon))) {
326             check(file.exists, format("File %s not found", file));
327 
328             const hirpc_response = file.fread!(HiRPC.Receiver).ifThrown!HiBONException(HiRPC.Receiver.init);
329             if (hirpc_response is HiRPC.Receiver.init) { continue; }
330             writefln("File %s %s", file, hirpc_response.toPretty);
331             const ok = wallet_interface.secure_wallet.setResponseUpdateWallet(hirpc_response)
332                 .ifThrown!HiBONException(
333                         wallet_interface.secure_wallet.setResponseCheckRead(hirpc_response)
334             );
335 
336             check(ok, format("HiPRC %s is not a valid response", file));
337             wallet_switch.save_wallet = true;
338         }
339         if (!account_name.empty) {
340             wallet_interface.secure_wallet.account.name = account_name;
341             wallet_interface.secure_wallet.account.owner = wallet_interface.secure_wallet.net.pubkey;
342             wallet_switch.save_wallet = true;
343 
344         }
345 
346         wallet_interface.operate(wallet_switch, args);
347     }
348     catch (GetOptException e) {
349         error(e.msg);
350         return 1;
351     }
352     catch (Exception e) {
353         error(e);
354         return 1;
355     }
356     return 0;
357 }