1 module tagion.tools.auszahlung.auszahlung;
2 import core.thread;
3 import std.algorithm;
4 import std.array;
5 import std.file : exists, mkdir, mkdirRecurse, setAttributes;
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.conv : to, octal;
13 import std.array;
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.network.ReceiveBuffer;
20 import tagion.script.TagionCurrency;
21 import tagion.tools.Basic;
22 import tagion.tools.revision;
23 import tagion.tools.wallet.WalletInterface;
24 import tagion.tools.wallet.WalletOptions;
25 import tagion.utils.Term;
26 import tagion.utils.Term;
27 import tagion.wallet.AccountDetails;
28 import tagion.wallet.KeyRecover;
29 import tagion.wallet.SecureWallet;
30 import tagion.wallet.WalletRecords;
31 import tagion.wallet.BIP39;
32 import tagion.basic.Types : encodeBase64, Buffer;
33 import tagion.basic.range : eatOne;
34 import tagion.basic.basic : isinit;
35 import tagion.crypto.SecureNet;
36 import tagion.crypto.Types;
37 import tagion.wallet.SecureWallet;
38 import tagion.hibon.BigNumber;
39 import tagion.hibon.HiBONtoText;
40 import tagion.script.common;
41 import tagion.crypto.random.random;
42 import tagion.utils.StdTime;
43 import tagion.communication.HiRPC;
44 import std.csv;
45 import std.string : representation;
46 
47 mixin Main!(_main, "payout");
48 
49 import tagion.crypto.SecureNet;
50 import Wallet = tagion.wallet.SecureWallet;
51 
52 /**
53  * @brief build file path if needed file with folder long path
54  * @param file - input/output parameter with filename
55  * @param path - forlders destination to file
56  */
57 @safe
58 static void set_path(ref string file, string path) {
59     file = buildPath(path, file.baseName);
60 }
61 
62 enum file_protect = octal!444;
63 enum MIN_WALLETS = 3;
64 int _main(string[] args) {
65     import tagion.wallet.SecureWallet : check;
66 
67     immutable program = args[0];
68     bool version_switch;
69     bool list;
70     bool sum;
71     bool force;
72     bool update;
73     string response_name;
74     //   string path;
75     uint confidence;
76     double amount;
77     GetoptResult main_args;
78     WalletOptions options;
79     WalletInterface[] wallet_interfaces;
80     auto config_files = args
81         .filter!(file => file.hasExtension(FileExtension.json));
82     auto config_file = default_wallet_config_filename;
83     try {
84         if (!config_files.empty) {
85             config_file = config_files.eatOne;
86         }
87         if (config_file.exists) {
88             options.load(config_file);
89         }
90         else {
91             options.setDefault;
92         }
93 
94         main_args = getopt(args, std.getopt.config.caseSensitive,
95                 std.getopt.config.bundling,
96                 "version", "display the version", &version_switch,
97                 "v|verbose", "Enable verbose print-out", &__verbose_switch,
98                 "dry", "Dry-run this will not save the wallet", &__dry_switch,
99                 "C|create", "Create the wallet an set the confidence", &confidence,
100                 "l|list", "List wallet content", &list,
101                 "s|sum", "Sum of the wallet", &sum,
102                 "amount", "Create an payment request in tagion", &amount, //"path", "File path", &path,
103                 "update", "Update wallet", &update,
104                 "response", "Response from update (response.hibon)", &response_name,
105                 "force", "Force input bill", &force,
106 
107         );
108         if (version_switch) {
109             revision_text.writeln;
110             return 0;
111         }
112         if (main_args.helpWanted) {
113             //            writeln(logo);
114             defaultGetoptPrinter(
115                     [
116                     // format("%s version %s", program, REVNO),
117                     "Documentation: https://tagion.org/",
118                     "",
119                     "Usage:",
120                     format("%s [<option>...] <wallet.json> [<bill.hibon>] ", program),
121                     "",
122 
123                     "<option>:",
124 
125                     ].join("\n"),
126                     main_args.options);
127             return 0;
128         }
129         check(config_file.exists, format("Wallet config %s not found", config_file));
130         const(HashNet) hash_net = new StdHashNet;
131         WalletOptions[] all_options;
132         auto common_wallet_interface = WalletInterface(options);
133         common_wallet_interface.load;
134         if (list) {
135             common_wallet_interface.listAccount(stdout, hash_net);
136             common_wallet_interface.listInvoices(stdout);
137             sum = true;
138         }
139         if (sum) {
140             common_wallet_interface.sumAccount(stdout);
141             return 0;
142         }
143         if (config_files.empty) {
144             return 0;
145         }
146         foreach (file; config_files) {
147             verbose("file %s", file);
148             check(file.hasExtension(FileExtension.json), format("%s is not a %s file", file, FileExtension.json));
149             check(file.exists, format("File %s not found", file));
150             WalletOptions wallet_options;
151             wallet_options.load(file);
152             all_options ~= wallet_options;
153         }
154         verbose("Number of wallets %d", all_options.length);
155 
156         foreach (wallet_option; all_options) {
157             auto wallet_interface = WalletInterface(wallet_option);
158             wallet_interface.load;
159             wallet_interfaces ~= wallet_interface;
160         }
161         import tagion.tools.secretinput;
162 
163         info("Press ctrl-C to break");
164         info("Press ctrl-D to skip the wallet");
165         info("Press ctrl-A to show the pincode");
166         {
167             auto wallets = wallet_interfaces[];
168             while (!wallets.empty) {
169 
170                 writefln("Name %s", wallets.front.secure_wallet.account.name);
171                 char[] pincode;
172                 scope (exit) {
173                     pincode[] = 0;
174                 }
175                 const keycode = getSecret("Pincode: ", pincode);
176                 with (KeyStroke.KeyCode) {
177                     switch (keycode) {
178                     case CTRL_C:
179                         error("Break the wallet login");
180                         return 1;
181                     case CTRL_D:
182                         warn("Skip %s", wallets.front.secure_wallet.account.name);
183                         wallets.popFront;
184                         continue;
185                     default:
186                         if (wallets.front.secure_wallet.login(pincode)) {
187                             good("Pincode correct");
188                             wallets.popFront;
189                         }
190                         else {
191                             error("Incorrect pincode");
192                         }
193                     }
194                 }
195             }
196         }
197         if (!confidence.isinit) {
198             check(common_wallet_interface.secure_wallet.wallet.isinit,
199                     "Common wallet has already been created");
200             check(wallet_interfaces.length >= MIN_WALLETS, format("More than %d wallets needed", MIN_WALLETS));
201             check(wallet_interfaces.all!(wallet => wallet.secure_wallet.isLoggedin),
202                     "The pincode of some of the wallet is not correct");
203             check(confidence <= wallet_interfaces.length, format(
204                     "Confidence can not be greater than number of wallets %d", wallet_interfaces.length));
205             verbose("Confidence is %d", confidence);
206             Buffer[] answers;
207             foreach (ref wallet; wallet_interfaces) {
208                 ubyte[] privkey;
209                 scope (exit) {
210                     privkey[] = 0;
211                 }
212                 const __net = cast(StdSecureNet)(wallet.secure_wallet.net);
213                 __net.__expose(privkey);
214                 answers ~= hash_net.rawCalcHash(privkey);
215             }
216             auto key_recover = KeyRecover(hash_net);
217             key_recover.createKey(answers, confidence);
218             common_wallet_interface.secure_wallet =
219                 WalletInterface.StdSecureWallet(DevicePIN.init, key_recover.generator);
220             common_wallet_interface.secure_wallet.recover(answers);
221 
222             verbose("Write wallet %s", common_wallet_interface.secure_wallet.isLoggedin);
223             options.questions = null;
224 
225             common_wallet_interface.save(true);
226             options.save(config_file);
227             return 0;
228         }
229         {
230             common_wallet_interface.load;
231             confidence = common_wallet_interface.secure_wallet.wallet.confidence;
232             const number_of_loggins = wallet_interfaces
233                 .count!((wallet_iface) => wallet_iface.secure_wallet.isLoggedin);
234             verbose("Loggedin %d", number_of_loggins);
235             check(confidence <= number_of_loggins, format("At least %d wallet need to open the transaction", confidence));
236 
237             Buffer[] answers;
238             foreach (wallet; wallet_interfaces) {
239                 ubyte[] privkey;
240                 scope (exit) {
241                     privkey[] = 0;
242                 }
243                 const __net = cast(StdSecureNet)(wallet.secure_wallet.net);
244                 if (__net) {
245                     __net.__expose(privkey);
246                 }
247                 answers ~= hash_net.rawCalcHash(privkey);
248             }
249             common_wallet_interface.secure_wallet.recover(answers);
250             check(common_wallet_interface.secure_wallet.isLoggedin, "Wallet could not be activated");
251             good("Wallet activated");
252         }
253         if (!response_name.empty) {
254             verbose("Response %s", response_name);
255             scope (success) {
256                 if (!dry_switch) {
257                     common_wallet_interface.save(false);
258                 }
259             }
260             const received = response_name.fread!(HiRPC.Receiver);
261             common_wallet_interface.secure_wallet.setResponseCheckRead(received);
262             return 0;
263         }
264         if (amount > 0) {
265             verbose("amount %s", amount);
266             scope (success) {
267                 if (!dry_switch) {
268                     common_wallet_interface.save(false);
269                 }
270             }
271             check(wallet_interfaces.all!(wiface => wiface.secure_wallet.isLoggedin),
272                     "All wallets must be loggedin to add amount");
273             const amount_tgn = TagionCurrency(amount);
274             const bill = common_wallet_interface.secure_wallet.requestBill(amount_tgn);
275             string bill_path = buildPath(options.billsfile.dirName, "bills");
276             mkdirRecurse(bill_path);
277             string filename;
278             uint bill_no;
279             do {
280                 filename = buildPath(bill_path, format("bill_%s", bill_no)).setExtension(FileExtension.hibon);
281                 bill_no++;
282             }
283             while (filename.exists);
284             /*
285             const filename = buildPath(bill_path, format("bill_%s", common_wallet_interface.secure_wallet.account.name))
286                 .setExtension(FileExtension.hibon);
287             */
288             good("bill file %s", filename);
289             filename.fwrite(bill);
290             scope (success) {
291                 if (!dry_switch) {
292                     filename.setAttributes(file_protect);
293                 }
294             }
295             return 0;
296         }
297         if (force) {
298             scope (success) {
299                 if (!dry_switch) {
300                     common_wallet_interface.save(false);
301                 }
302             }
303             check(wallet_interfaces.all!(wiface => wiface.secure_wallet.isLoggedin),
304                     "All wallets must be loggedin to force the bill");
305             foreach (arg; args[1 .. $].filter!(file => file.hasExtension(FileExtension.hibon))) {
306                 const bill = arg.fread!TagionBill;
307                 with (common_wallet_interface) {
308                     good("%s", toText(hash_net, bill));
309                     verbose("%s", show(bill));
310                     const added = secure_wallet.addBill(bill);
311                     check(added, "Bill was not found");
312                 }
313             }
314             common_wallet_interface.listAccount(stdout, hash_net);
315             common_wallet_interface.listInvoices(stdout);
316 
317             return 0;
318         }
319         auto csv_files = args[1 .. $].filter!(file => file.hasExtension(FileExtension.csv));
320         const contracts = buildPath(options.accountfile.dirName, "contracts");
321         if (!csv_files.empty) {
322             mkdirRecurse(contracts);
323         }
324         foreach (filename; csv_files) {
325             scope (success) {
326                 if (!dry_switch) {
327                     common_wallet_interface.save(false);
328                 }
329             }
330             verbose("CVS %s", filename);
331             auto fin = File(filename, "r");
332             scope (exit) {
333                 fin.close;
334             }
335             enum payee_name = "Name";
336             enum pubkey_name = "PUBKey";
337             enum amount_name = "Amount";
338             TagionBill[] to_pay;
339             TagionCurrency total_amount;
340             foreach (record; csvReader!(string[string])(fin.byLine.joiner("\n"), null, ';')) {
341                 const pubkey = Pubkey(record[pubkey_name].decode);
342                 const amount_tgn = record[amount_name].to!double.TGN;
343                 total_amount += amount_tgn;
344                 auto nonce = new ubyte[4];
345                 getRandom(nonce);
346                 to_pay ~= TagionBill(amount_tgn, currentTime, pubkey, nonce.idup);
347                 info("%10s %37s %-14sTGN", record[payee_name], pubkey.encodeBase64, amount_tgn);
348             }
349             SignedContract signed_contract;
350             TagionCurrency fees;
351             with (common_wallet_interface) {
352                 secure_wallet.createPayment(to_pay, signed_contract, fees);
353                 const contract_filename = buildPath(contracts, filename.baseName).setExtension(FileExtension.hibon);
354                 const message = secure_wallet.net.calcHash(signed_contract);
355                 const contract_net = secure_wallet.net.derive(message);
356                 const hirpc = HiRPC(contract_net);
357                 const hirpc_submit = hirpc.submit(signed_contract);
358                 verbose("submit\n%s", show(hirpc_submit));
359                 secure_wallet.account.hirpcs ~= hirpc_submit.toDoc;
360                 contract_filename.fwrite(hirpc_submit);
361                 scope (success) {
362                     if (!dry_switch) {
363                         contract_filename.setAttributes(file_protect);
364                     }
365                 }
366             }
367             good("Total %sTGN", total_amount);
368             update = true;
369         }
370         if (update) {
371             verbose("Update");
372             check(common_wallet_interface.secure_wallet.isLoggedin, "Wallet should be loggedin");
373             auto basename = "update";
374             if (!csv_files.empty) {
375                 basename = csv_files.front.baseName;
376             }
377             with (common_wallet_interface) {
378                 const message = secure_wallet.net.calcHash(WalletInterface.update_tag.representation);
379                 const update_net = secure_wallet.net.derive(message);
380                 const hirpc = HiRPC(update_net);
381                 const req = secure_wallet.getRequestCheckWallet(hirpc);
382                 const update_file = [basename, update_tag].join("_");
383                 const update_req = update_file.setExtension(FileExtension.hibon);
384 
385                 update_req.fwrite(req);
386                 scope (success) {
387                     if (!dry_switch) {
388                         update_req.setAttributes(file_protect);
389                     }
390                 }
391             }
392         }
393     }
394 
395     catch (GetOptException e) {
396         error(e.msg);
397         return 1;
398     }
399     catch (Exception e) {
400         error("%s", e.msg);
401         verbose("%s", e.toString);
402         return 1;
403     }
404     return 0;
405 }